Adds optional SSO via Authentik (or any OIDC provider) alongside the existing password flow, and pulls profile images from the provider's `picture` claim or Gravatar so the sharing UI stops looking anonymous. Password login stays available as a recovery path; JIT provisioning and admin-group mapping are env-configurable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
78 lines
2.4 KiB
Python
78 lines
2.4 KiB
Python
"""OIDC (OpenID Connect) client setup — used for Authentik SSO today,
|
|
generic enough to register other providers later.
|
|
|
|
Authlib handles the Authorization Code + PKCE flow, including discovery
|
|
via the provider's `.well-known/openid-configuration` document. We keep
|
|
a single registered client named "authentik" regardless of label, so the
|
|
router code always knows where to find it.
|
|
|
|
If OIDC isn't fully configured the module stays inert — `is_enabled()`
|
|
returns False and the registry has no client. Callers must guard.
|
|
"""
|
|
from typing import Optional
|
|
|
|
from authlib.integrations.starlette_client import OAuth
|
|
|
|
from app.config import settings
|
|
|
|
|
|
PROVIDER_NAME = "authentik"
|
|
|
|
|
|
def is_configured() -> bool:
|
|
"""True when every required OIDC setting is present."""
|
|
return bool(
|
|
settings.oidc_issuer
|
|
and settings.oidc_client_id
|
|
and settings.oidc_client_secret
|
|
and settings.oidc_redirect_uri
|
|
)
|
|
|
|
|
|
def is_enabled() -> bool:
|
|
"""True when OIDC is both configured and the admin flipped it on."""
|
|
return bool(settings.oidc_enabled) and is_configured()
|
|
|
|
|
|
def provider_label() -> str:
|
|
return settings.oidc_provider_label or "Authentik"
|
|
|
|
|
|
def _build_oauth() -> OAuth:
|
|
"""Build the Authlib OAuth registry. Always safe to call; only
|
|
registers the provider when credentials are present so importing
|
|
this module never fails on a fresh install.
|
|
"""
|
|
registry = OAuth()
|
|
if not is_configured():
|
|
return registry
|
|
|
|
# Authentik exposes discovery at `{issuer}/.well-known/openid-configuration`.
|
|
# Trailing slash handling varies by Authentik version, so normalise.
|
|
issuer = settings.oidc_issuer.rstrip("/")
|
|
discovery_url = f"{issuer}/.well-known/openid-configuration"
|
|
|
|
registry.register(
|
|
name=PROVIDER_NAME,
|
|
client_id=settings.oidc_client_id,
|
|
client_secret=settings.oidc_client_secret,
|
|
server_metadata_url=discovery_url,
|
|
client_kwargs={
|
|
"scope": settings.oidc_scopes,
|
|
# Force PKCE — cheap win for public clients, harmless for
|
|
# confidential ones.
|
|
"code_challenge_method": "S256",
|
|
},
|
|
)
|
|
return registry
|
|
|
|
|
|
oauth: OAuth = _build_oauth()
|
|
|
|
|
|
def get_client() -> Optional[object]:
|
|
"""Return the registered provider client, or None when not configured."""
|
|
if not is_configured():
|
|
return None
|
|
return oauth.create_client(PROVIDER_NAME)
|