Files
mule-image/backend/app/auth_oidc.py
dtoro e8e1adcf37 feat(auth): Authentik OIDC sign-in + Gravatar avatars
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>
2026-04-22 21:06:32 +02:00

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)