App-Client darf eine eigene Anwendung beim Anmeldedienst sein

In Authentik hat jede Anwendung standardmaessig ihren eigenen Issuer, abgeleitet
aus dem Application-Slug. Web- und App-Client brauchen dort aber zwei getrennte
Anwendungen, weil ein Provider entweder vertraulich ODER oeffentlich ist.

Bisher wurde das App-Token gegen den Issuer und den Schluesselsatz der
Web-Anwendung geprueft - die App-Anmeldung waere damit an der iss-Pruefung
gescheitert. Discovery und JWKS werden jetzt je Adresse gepuffert statt je
Anbieter, und OIDC_<KUERZEL>_NATIVE_DISCOVERY_URL benennt die Anwendung des
App-Clients. Ohne die Variable bleibt es bei einer gemeinsamen Adresse.

Ausserdem im README: der Signing Key muss gesetzt sein, sonst signiert Authentik
mit HS256 und das Token wird abgelehnt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-09 22:00:37 +02:00
co-authored by Claude Opus 5
parent 3d0ff54bf5
commit 31b3624cc7
7 changed files with 91 additions and 26 deletions
+24 -16
View File
@@ -76,30 +76,35 @@ def _holen(url: str) -> dict[str, Any]:
) from fehler
def metadaten(provider: OidcProvider, erzwingen: bool = False) -> dict[str, Any]:
"""Discovery-Dokument des Anbieters (gepuffert)."""
eintrag = _metadaten_cache.get(provider.slug)
def metadaten(discovery_url: str, erzwingen: bool = False) -> dict[str, Any]:
"""Discovery-Dokument (gepuffert).
Gepuffert wird nach der Adresse, nicht nach dem Anbieter: Browser- und
App-Client koennen bei Authentik zwei getrennte Anwendungen mit je eigenem
Issuer und eigenem Schluesselsatz sein.
"""
eintrag = _metadaten_cache.get(discovery_url)
if not erzwingen and eintrag and eintrag[0] > time.monotonic():
return eintrag[1]
daten = _holen(provider.discovery_url)
daten = _holen(discovery_url)
for pflicht in ("issuer", "authorization_endpoint", "token_endpoint", "jwks_uri"):
if not daten.get(pflicht):
raise OidcFehler(
"discovery_unvollstaendig",
f"Discovery-Dokument von '{provider.slug}' ohne '{pflicht}'.",
f"Discovery-Dokument unter {discovery_url} ohne '{pflicht}'.",
)
_metadaten_cache[provider.slug] = (time.monotonic() + CACHE_SEKUNDEN, daten)
_metadaten_cache[discovery_url] = (time.monotonic() + CACHE_SEKUNDEN, daten)
return daten
def _jwks(provider: OidcProvider, erzwingen: bool = False) -> Any:
eintrag = _jwks_cache.get(provider.slug)
def _jwks(discovery_url: str, erzwingen: bool = False) -> Any:
eintrag = _jwks_cache.get(discovery_url)
if not erzwingen and eintrag and eintrag[0] > time.monotonic():
return eintrag[1]
schluessel = JsonWebKey.import_key_set(_holen(metadaten(provider)["jwks_uri"]))
_jwks_cache[provider.slug] = (time.monotonic() + CACHE_SEKUNDEN, schluessel)
schluessel = JsonWebKey.import_key_set(_holen(metadaten(discovery_url)["jwks_uri"]))
_jwks_cache[discovery_url] = (time.monotonic() + CACHE_SEKUNDEN, schluessel)
return schluessel
@@ -118,7 +123,7 @@ def _client(provider: OidcProvider, redirect_uri: str) -> OAuth2Client:
def anmelde_url(provider: OidcProvider, state: str, nonce: str, code_verifier: str) -> str:
"""Adresse, zu der der Browser fuer die Anmeldung geschickt wird."""
meta = metadaten(provider)
meta = metadaten(provider.discovery_url)
with _client(provider, provider.redirect_uri) as client:
url, _ = client.create_authorization_url(
meta["authorization_endpoint"],
@@ -131,7 +136,7 @@ def anmelde_url(provider: OidcProvider, state: str, nonce: str, code_verifier: s
def code_einloesen(provider: OidcProvider, code: str, code_verifier: str) -> dict[str, Any]:
"""Tauscht den Autorisierungs-Code gegen Tokens (vertraulicher Web-Client)."""
meta = metadaten(provider)
meta = metadaten(provider.discovery_url)
try:
with _client(provider, provider.redirect_uri) as client:
return client.fetch_token(
@@ -154,14 +159,17 @@ def pruefe_id_token(
id_token: str,
nonce: str | None = None,
erlaubte_audiences: tuple[str, ...] | None = None,
discovery_url: str | None = None,
) -> dict[str, Any]:
"""Prueft Signatur, ``iss``, ``aud``, ``exp`` (und ``nonce``, falls bekannt).
``erlaubte_audiences`` schraenkt die zulaessigen Empfaenger ein: der
Web-Rueckweg akzeptiert nur die vertrauliche Client-ID, der App-Weg nur die
der registrierten oeffentlichen Clients.
der registrierten oeffentlichen Clients. ``discovery_url`` bestimmt, gegen
welchen Issuer und welchen Schluesselsatz geprueft wird.
"""
meta = metadaten(provider)
quelle = discovery_url or provider.discovery_url
meta = metadaten(quelle)
audiences = erlaubte_audiences or provider.erlaubte_audiences
if not audiences:
raise OidcFehler(
@@ -187,10 +195,10 @@ def pruefe_id_token(
try:
try:
claims = _entschluesseln(_jwks(provider))
claims = _entschluesseln(_jwks(quelle))
except JoseError:
# Kann ein frisch gedrehter Signaturschluessel sein – einmal neu laden.
claims = _entschluesseln(_jwks(provider, erzwingen=True))
claims = _entschluesseln(_jwks(quelle, erzwingen=True))
claims.validate(leeway=UHREN_SPIELRAUM)
except JoseError as fehler:
raise OidcFehler("id_token_ungueltig", f"ID-Token abgelehnt: {fehler}") from fehler