feat(oidc): Mobile-Scopes serverseitig konfigurierbar (offline_access)

Die Apps duerfen keine Scopes erfinden: ein Scope, den der Provider nicht
kennt, laesst ihn die gesamte Authorization-Anfrage ablehnen. Daher legt
jetzt der Server fest, was die nativen Clients anfragen.

OIDC_<KEY>_MOBILE_SCOPES ist frei setzbar; ohne Angabe sind es die
Web-Scopes plus offline_access, damit Authentik ab 2024.2 ueberhaupt ein
Refresh-Token ausstellt. Der Browser-Flow bleibt unveraendert, der braucht
kein offline_access. Ausgeliefert wird der Wert als mobile_scopes in
/api/auth/oidc/providers, aber nur wenn eine MOBILE_CLIENT_ID konfiguriert
ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-09 18:24:59 +02:00
co-authored by Claude Opus 5
parent 25fde49cee
commit 07235fd4c9
3 changed files with 34 additions and 1 deletions
+15 -1
View File
@@ -43,6 +43,7 @@ class OIDCProvider:
scopes: str = DEFAULT_SCOPES
redirect_uri: str = "" # empty => derived from public_base()
mobile_client_ids: tuple = () # public native clients (AppAuth)
mobile_scopes: str = "" # scopes the apps request (see below)
mobile_issuer: str = "" # only if mobile uses a 2nd application
allow_signup: bool = False # just-in-time provisioning
allowed_domains: tuple = () # email domain allowlist for signup
@@ -90,13 +91,26 @@ def _build_provider(key: str) -> Optional[OIDCProvider]:
p.strip() for p in _env(key, "MOBILE_CLIENT_ID").split(",") if p.strip()
)
scopes = _env(key, "SCOPES") or DEFAULT_SCOPES
# The native apps need a refresh token, which requires `offline_access`.
# Since Authentik 2024.2 that scope must be mapped explicitly, and some
# other providers reject an unknown scope outright — so it is the server,
# not the app, that decides. Overridable per provider without a code change.
mobile_scopes = _env(key, "MOBILE_SCOPES")
if not mobile_scopes:
wanted = scopes.split()
if "offline_access" not in wanted:
wanted.append("offline_access")
mobile_scopes = " ".join(wanted)
return OIDCProvider(
key=key,
name=_env(key, "NAME") or key.capitalize(),
issuer=issuer,
client_id=client_id,
client_secret=_env(key, "CLIENT_SECRET"),
scopes=_env(key, "SCOPES") or DEFAULT_SCOPES,
scopes=scopes,
mobile_scopes=mobile_scopes,
redirect_uri=_env(key, "REDIRECT_URI"),
mobile_client_ids=mobile_ids,
mobile_issuer=_env(key, "MOBILE_ISSUER"),