Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bb3b01dbd | ||
|
|
ce761a9d2c |
@@ -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,15 +185,22 @@ 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")
|
||||||
|
|
||||||
# Validate issuer is in allowed list
|
# Validate issuer is in allowed list
|
||||||
|
logger.debug(f"Token issuer: {token_issuer}, allowed issuers: {oidc_config.issuers}")
|
||||||
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} (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
|
||||||
@@ -207,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")
|
||||||
@@ -315,12 +322,14 @@ async def get_optional_user(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if not credentials:
|
if not credentials:
|
||||||
|
logger.debug("No credentials provided for optional auth")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await get_current_user(credentials)
|
return await get_current_user(credentials)
|
||||||
except HTTPException:
|
except HTTPException as e:
|
||||||
# Invalid token - return None instead of raising
|
# Invalid token - log and return None instead of raising
|
||||||
|
logger.warning(f"Optional auth failed: {e.detail}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user