Hi! This module currently fails to validate ID tokens when the aud claim is an array, which is explicitly allowed (and common) per the specifications:
RFC 7519 Section 4.1.3
OpenID Connect Core Section 2 (especially when multiple audiences are present)
Current code (auth_oidc/models/auth_oauth_provider.py:99):python
values = jwt.decode(
id_token,
key,
algorithms=["RS256"],
audience=self.client_id, # ← only a string
access_token=access_token,
)
This works only when aud is a single string that exactly matches client_id. The matching part is perfectly fine, but the spec says you can have multiple audiences in the token, in which case only one of them has to match client_id.
When an IdP returns aud as an array (e.g. ["my-client-id", "https://api.example.com"]), or even an array with just one element in it your validation fails with a JWTError / audience mismatch.
There's a few ways to fix it. Here's one idea: decode the token then
def _decode_id_token(self, access_token, id_token, kid):
keys = self._get_keys(kid)
if len(keys) > 1 and kid is None:
raise JWTError("OpenID Connect requires kid to be set if there is more than one key in the JWKS")
error = None
for key in keys:
try:
# python-jose's audience validation is limited.
# We decode first without audience check, then validate manually.
values = jwt.decode(
id_token,
key,
algorithms=["RS256"],
options={"verify_aud": False}, # disable built-in check
access_token=access_token,
)
# Manual audience validation (compliant with RFC 7519 + OIDC)
aud = values.get("aud")
client_id = self.client_id
# Current allowed behavior - audience is just a string
if isinstance(aud, str):
valid = aud == client_id
# Additionally allowed behavior - audience is a list of strings
elif isinstance(aud, list):
valid = client_id in aud
else:
valid = False
if not valid:
raise JWTError(f"Invalid audience. Expected {client_id} to be in {aud}")
return values
except (JWTError, JWSError) as e:
error = e
if error:
raise error
return {}
Alternative (if you prefer to keep using the library's validation), you could also extract the aud claim first with jwt.get_unverified_claims() and do the check before calling decode, but the approach above is cleaner and more robust.
Side note: if you implement this, OIDC strongly recommends that when there are multiple audiences there be an AZP claim as well. AZP is the client_id of the application that actually requested the token. The spec states this as:
If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.
If you have security concerns about the case where there's multiple audiences you could do that too (per the spec you really should check azp).
# Manual audience + azp validation
aud = values.get("aud")
azp = values.get("azp")
client_id = self.client_id
if isinstance(aud, str):
valid = aud == client_id
elif isinstance(aud, list):
valid = client_id in aud
if valid and len(aud) > 1:
# If multiple audiences, azp should be present and match
if azp and azp != client_id:
raise JWTError(f"azp claim '{azp}' does not match client_id '{client_id}'")
else:
valid = False
if not valid:
raise JWTError(f"Invalid audience. Expected {client_id} in {aud}")
...
server-auth/auth_oidc/models/auth_oauth_provider.py
Line 99 in e14ae60
Hi! This module currently fails to validate ID tokens when the aud claim is an array, which is explicitly allowed (and common) per the specifications:
RFC 7519 Section 4.1.3
OpenID Connect Core Section 2 (especially when multiple audiences are present)
Current code (auth_oidc/models/auth_oauth_provider.py:99):python
This works only when aud is a single string that exactly matches client_id. The matching part is perfectly fine, but the spec says you can have multiple audiences in the token, in which case only one of them has to match client_id.
When an IdP returns aud as an array (e.g. ["my-client-id", "https://api.example.com"]), or even an array with just one element in it your validation fails with a JWTError / audience mismatch.
There's a few ways to fix it. Here's one idea: decode the token then
Alternative (if you prefer to keep using the library's validation), you could also extract the aud claim first with jwt.get_unverified_claims() and do the check before calling decode, but the approach above is cleaner and more robust.
Side note: if you implement this, OIDC strongly recommends that when there are multiple audiences there be an AZP claim as well. AZP is the client_id of the application that actually requested the token. The spec states this as:
If you have security concerns about the case where there's multiple audiences you could do that too (per the spec you really should check azp).