fix: OIDC audience validation - use string not list
python-jose jwt.decode() requires audience as string or None, not list. Now extract and validate audience from unverified claims first, then use token's actual audience for JWT decode. Fixes "audience must be a string or None" error. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
ce761a9d2c
commit
3bb3b01dbd
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.10.8] - 2026-01-08
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **OIDC audience validation** - python-jose requires string audience, not list
|
||||||
|
- Extract and validate audience from unverified claims first
|
||||||
|
- Use token's actual audience for JWT decode (after validating it's allowed)
|
||||||
|
- Fixes "audience must be a string or None" error
|
||||||
|
|
||||||
## [1.10.7] - 2026-01-08
|
## [1.10.7] - 2026-01-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "core-api"
|
name = "core-api"
|
||||||
version = "1.10.7"
|
version = "1.10.8"
|
||||||
description = "Core Code API - Infrastructure management and tools API"
|
description = "Core Code API - Infrastructure management and tools API"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
+11
-6
@@ -131,15 +131,21 @@ async def get_current_user(
|
|||||||
token = credentials.credentials
|
token = credentials.credentials
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# First, extract issuer from unverified claims to know which JWKS to use
|
# First, extract issuer and audience from unverified claims
|
||||||
unverified_claims = jwt.get_unverified_claims(token)
|
unverified_claims = jwt.get_unverified_claims(token)
|
||||||
token_issuer = unverified_claims.get("iss", "")
|
token_issuer = unverified_claims.get("iss", "")
|
||||||
|
token_audience = unverified_claims.get("aud", "")
|
||||||
|
|
||||||
# Validate issuer is in our allowed list
|
# Validate issuer is in our allowed list
|
||||||
if not oidc_config.is_valid_issuer(token_issuer):
|
if not oidc_config.is_valid_issuer(token_issuer):
|
||||||
logger.warning(f"Invalid token issuer: {token_issuer}")
|
logger.warning(f"Invalid token issuer: {token_issuer}")
|
||||||
raise HTTPException(status_code=401, detail="Invalid token issuer")
|
raise HTTPException(status_code=401, detail="Invalid token issuer")
|
||||||
|
|
||||||
|
# Validate audience is in our allowed list
|
||||||
|
if token_audience not in oidc_config.audiences:
|
||||||
|
logger.warning(f"Invalid token audience: {token_audience}")
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid token audience")
|
||||||
|
|
||||||
# Decode token header to get key ID
|
# Decode token header to get key ID
|
||||||
unverified_header = jwt.get_unverified_header(token)
|
unverified_header = jwt.get_unverified_header(token)
|
||||||
kid = unverified_header.get("kid")
|
kid = unverified_header.get("kid")
|
||||||
@@ -160,18 +166,17 @@ async def get_current_user(
|
|||||||
logger.warning(f"No matching key found for kid: {kid}")
|
logger.warning(f"No matching key found for kid: {kid}")
|
||||||
raise HTTPException(status_code=401, detail="Invalid token key")
|
raise HTTPException(status_code=401, detail="Invalid token key")
|
||||||
|
|
||||||
# Verify and decode token (accepts any of the configured audiences)
|
# Verify and decode token using the token's actual issuer and audience
|
||||||
# Use the token's issuer for validation (already verified it's in our allowed list)
|
|
||||||
payload = jwt.decode(
|
payload = jwt.decode(
|
||||||
token,
|
token,
|
||||||
rsa_key,
|
rsa_key,
|
||||||
algorithms=["RS256"],
|
algorithms=["RS256"],
|
||||||
audience=oidc_config.audiences,
|
audience=token_audience, # Use the token's audience (already validated)
|
||||||
issuer=token_issuer,
|
issuer=token_issuer, # Use the token's issuer (already validated)
|
||||||
)
|
)
|
||||||
|
|
||||||
user_email = payload.get("email", "unknown")
|
user_email = payload.get("email", "unknown")
|
||||||
logger.info(f"Authenticated user: {user_email}")
|
logger.info(f"Authenticated user: {user_email} (issuer: {token_issuer})")
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ async def get_current_user(
|
|||||||
|
|
||||||
kid = unverified_header.get("kid")
|
kid = unverified_header.get("kid")
|
||||||
token_issuer = unverified_claims.get("iss", "")
|
token_issuer = unverified_claims.get("iss", "")
|
||||||
|
token_audience = unverified_claims.get("aud", "")
|
||||||
|
|
||||||
if not kid:
|
if not kid:
|
||||||
raise HTTPException(status_code=401, detail="Invalid token format")
|
raise HTTPException(status_code=401, detail="Invalid token format")
|
||||||
@@ -195,6 +196,11 @@ async def get_current_user(
|
|||||||
logger.warning(f"Invalid token issuer: {token_issuer} (allowed: {oidc_config.issuers})")
|
logger.warning(f"Invalid token issuer: {token_issuer} (allowed: {oidc_config.issuers})")
|
||||||
raise HTTPException(status_code=401, detail="Invalid token issuer")
|
raise HTTPException(status_code=401, detail="Invalid token issuer")
|
||||||
|
|
||||||
|
# Validate audience is in allowed list
|
||||||
|
if token_audience not in oidc_config.audiences:
|
||||||
|
logger.warning(f"Invalid token audience: {token_audience} (allowed: {oidc_config.audiences})")
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid token audience")
|
||||||
|
|
||||||
# Get JWKS for this specific issuer
|
# Get JWKS for this specific issuer
|
||||||
jwks = get_jwks_for_issuer(token_issuer)
|
jwks = get_jwks_for_issuer(token_issuer)
|
||||||
rsa_key = None
|
rsa_key = None
|
||||||
@@ -208,13 +214,13 @@ async def get_current_user(
|
|||||||
logger.warning(f"No matching key found for kid: {kid}")
|
logger.warning(f"No matching key found for kid: {kid}")
|
||||||
raise HTTPException(status_code=401, detail="Invalid token key")
|
raise HTTPException(status_code=401, detail="Invalid token key")
|
||||||
|
|
||||||
# Verify and decode token using the token's actual issuer
|
# Verify and decode token using the token's actual issuer and audience
|
||||||
payload = jwt.decode(
|
payload = jwt.decode(
|
||||||
token,
|
token,
|
||||||
rsa_key,
|
rsa_key,
|
||||||
algorithms=["RS256"],
|
algorithms=["RS256"],
|
||||||
audience=oidc_config.audiences,
|
audience=token_audience, # Use the token's audience (already validated)
|
||||||
issuer=token_issuer, # Use the token's issuer for validation
|
issuer=token_issuer, # Use the token's issuer (already validated)
|
||||||
)
|
)
|
||||||
|
|
||||||
user_email = payload.get("email", "unknown")
|
user_email = payload.get("email", "unknown")
|
||||||
|
|||||||
Reference in New Issue
Block a user