Skip to content

aud claim as array is not supported (non-compliant with OIDC / RFC 7519) #938

@wz2b

Description

@wz2b

audience=self.client_id,

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}")
...

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions