crewAIInc/crewAI · error · HTTPException

Invalid token issuer

Error message

Invalid token issuer

What it means

Raised by OIDCAuth.authenticate() when jwt.decode() throws InvalidIssuerError: the JWT's `iss` claim does not equal the scheme's configured issuer URL. This guards against tokens signed by a trusted key but minted by a different authority (e.g. a sibling tenant). It maps to HTTP 401 and logs reason='invalid_issuer'.

Source

Thrown at lib/crewai/src/crewai/a2a/auth/server_schemes.py:324

            raise HTTPException(
                status_code=HTTP_401_UNAUTHORIZED,
                detail="Token has expired",
            ) from None
        except jwt.InvalidAudienceError:
            logger.debug(
                "OIDC authentication failed",
                extra={"reason": "invalid_audience", "scheme": "oidc"},
            )
            raise HTTPException(
                status_code=HTTP_401_UNAUTHORIZED,
                detail="Invalid token audience",
            ) from None
        except jwt.InvalidIssuerError:
            logger.debug(
                "OIDC authentication failed",
                extra={"reason": "invalid_issuer", "scheme": "oidc"},
            )
            raise HTTPException(
                status_code=HTTP_401_UNAUTHORIZED,
                detail="Invalid token issuer",
            ) from None
        except jwt.MissingRequiredClaimError as e:
            logger.debug(
                "OIDC authentication failed",
                extra={"reason": "missing_claim", "claim": e.claim, "scheme": "oidc"},
            )
            raise HTTPException(
                status_code=HTTP_401_UNAUTHORIZED,
                detail=f"Missing required claim: {e.claim}",
            ) from None
        except jwt.PyJWKClientError as e:
            logger.error(
                "OIDC authentication failed",
                extra={
                    "reason": "jwks_client_error",
                    "error": str(e),

View on GitHub (pinned to 754d7323be)

Solutions

  1. Decode the token and read its iss claim verbatim, then set the scheme's issuer to exactly that string (including scheme, host, path, trailing slash).
  2. For Azure AD, use the tenant-specific issuer (https://login.microsoftonline.com/<tenant-id>/v2.0) matching the token.
  3. Treat issuer/audience/jwks_url as one unit per environment and validate them together at startup.
  4. Never disable issuer validation to 'make it work'; fix the configured value instead.

Example fix

# before
auth = OIDCAuth(jwks_url=jwks, issuer="https://idp.example.com", ...)  # token has "https://idp.example.com/"

# after
auth = OIDCAuth(jwks_url=jwks, issuer="https://idp.example.com/", ...)  # exact match with `iss` claim
Defensive patterns

Strategy: validation

Validate before calling

import jwt

claims = jwt.decode(access_token, options={"verify_signature": False})
assert claims.get("iss") == configured_issuer, (
    f"iss mismatch: token={claims.get('iss')!r} scheme={configured_issuer!r}"
)

Prevention

When it happens

Trigger: Scheme configured with issuer='https://idp.example.com' while the token's iss is 'https://tenant.idp.example.com/' (or differs only by trailing slash); multi-tenant IdPs where each tenant has a distinct issuer; mixing issuer URLs between prod and staging tenants.

Common situations: Trailing-slash mismatch between the configured issuer and the actual iss claim; Azure AD where the issuer includes a {tenantid} GUID; environment promotion carrying a staging issuer into a prod config.

Understand the failure class

Related errors


AI-assisted analysis of crewAIInc/crewAI@754d7323be (2026-08-15). Data as JSON: /api/errors/94928a27a74315ed. Report an issue: GitHub.