This commit is contained in:
Timothy Jaeryang Baek
2026-08-24 16:29:57 -04:00
parent 336d8841f4
commit aeda6ff13a
+35 -27
View File
@@ -2230,7 +2230,6 @@ class OAuthManager:
sessions via Redis, and deletes their OAuth sessions.
Returns a JSONResponse per the OIDC Back-Channel Logout 1.0 spec.
"""
import jwt as pyjwt
from fastapi.responses import JSONResponse
# 1. Extract logout_token from form body
@@ -2248,7 +2247,7 @@ class OAuthManager:
# 2. Peek at unverified issuer to match against configured providers
try:
unverified_claims = pyjwt.decode(logout_token, options={'verify_signature': False})
unverified_claims = jwt.decode(logout_token, options={'verify_signature': False})
token_issuer = unverified_claims.get('iss')
except Exception as e:
log.warning(f'Back-channel logout: cannot decode logout_token: {e}')
@@ -2265,35 +2264,27 @@ class OAuthManager:
# 3. Find the configured provider whose issuer matches the token
matched_provider = None
matched_client_id = None
matched_client = None
matched_jwks_uri = None
matched_issuer = None
for provider_name in OAUTH_PROVIDERS:
server_metadata_url = self.get_server_metadata_url(provider_name)
if not server_metadata_url:
client = self.get_client(provider_name)
if not client:
continue
try:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(server_metadata_url, ssl=AIOHTTP_CLIENT_SESSION_SSL) as r:
if r.status != 200:
continue
oidc_config = await r.json()
provider_issuer = oidc_config.get('issuer')
if provider_issuer and provider_issuer == token_issuer:
client = self.get_client(provider_name)
matched_provider = provider_name
matched_client_id = client.client_id if client else None
matched_jwks_uri = oidc_config.get('jwks_uri')
matched_issuer = provider_issuer
break
oidc_config = await client.load_server_metadata()
except Exception as e:
log.debug('Back-channel logout: error checking provider %s: %s', provider_name, e)
continue
if not matched_provider or not matched_client_id or not matched_jwks_uri:
if oidc_config.get('issuer') == token_issuer:
matched_provider = provider_name
matched_client = client
matched_jwks_uri = oidc_config.get('jwks_uri')
break
if not matched_provider or not matched_client or not matched_client.client_id or not matched_jwks_uri:
log.warning(f'Back-channel logout: no configured provider matches issuer {token_issuer}')
return JSONResponse(
status_code=400,
@@ -2305,20 +2296,37 @@ class OAuthManager:
# 4. Validate the logout_token signature and claims
try:
jwks_client = pyjwt.PyJWKClient(matched_jwks_uri)
signing_key = jwks_client.get_signing_key_from_jwt(logout_token)
token_kid = jwt.get_unverified_header(logout_token).get('kid')
if not token_kid:
raise jwt.InvalidTokenError('logout_token missing kid header')
claims = pyjwt.decode(
try:
jwk_set = jwt.PyJWKSet.from_dict(await matched_client.fetch_jwk_set())
except jwt.PyJWTError as e:
raise jwt.InvalidTokenError(str(e))
signing_key = next(
(
key
for key in jwk_set.keys
if key.key_id == token_kid and key.public_key_use in ['sig', None]
),
None,
)
if not signing_key:
raise jwt.InvalidTokenError('no signing key matches the token kid')
claims = jwt.decode(
logout_token,
signing_key.key,
algorithms=['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512'],
audience=matched_client_id,
issuer=matched_issuer,
audience=matched_client.client_id,
issuer=token_issuer,
options={
'require': ['iss', 'aud', 'iat', 'events'],
},
)
except pyjwt.InvalidTokenError as e:
except jwt.InvalidTokenError as e:
log.warning(f'Back-channel logout: invalid logout_token: {e}')
return JSONResponse(
status_code=400,