BerriAI/litellm · error · HTTPException

MCPJWTSigner: incoming token verification failed: {exc}

Error message

MCPJWTSigner: incoming token verification failed: {exc}

What it means

MCPJWTSigner wraps the entire incoming-token verification step in a try/except; any exception (PyJWT signature/expiry/audience errors, JWKS fetch failures, the missing-jwks_uri ValueError, introspection errors) is logged and re-raised as HTTPException 401 with the original exception text embedded. The inner message is the real diagnosis; this 401 is the uniform 'unauthenticated' surface.

Source

Thrown at litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py:820

            # Three-dot pattern → JWT;  otherwise opaque.
            is_jwt: Final = raw_token.count(".") == 2
            try:
                if is_jwt:
                    jwt_claims = await self._verify_incoming_jwt(raw_token)
                elif self.token_introspection_endpoint:
                    jwt_claims = await self._introspect_opaque_token(raw_token)
                else:
                    verbose_proxy_logger.warning(
                        "MCPJWTSigner: access_token_discovery_uri is set but the "
                        "incoming token appears to be opaque and no "
                        "token_introspection_endpoint is configured. "
                        "Proceeding without incoming token verification."
                    )
            except Exception as exc:
                verbose_proxy_logger.error("MCPJWTSigner: incoming token verification failed: %s", exc)
                from fastapi import HTTPException

                raise HTTPException(
                    status_code=401,
                    detail={"error": (f"MCPJWTSigner: incoming token verification failed: {exc}")},
                )
        elif not raw_token and self.access_token_discovery_uri:
            verbose_proxy_logger.debug(
                "MCPJWTSigner: access_token_discovery_uri configured but no Bearer "
                "token found in request (API-key auth request — skipping verification)."
            )

        # Fall back to LiteLLM-decoded JWT claims (available when proxy uses JWT auth).
        if jwt_claims is None:
            jwt_claims = user_api_key_dict.jwt_claims

        # ------------------------------------------------------------------
        # FR-15: Validate required claims
        # ------------------------------------------------------------------
        self._validate_required_claims(jwt_claims)

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Read the {exc} text in the 401 detail - it names the actual failure (expired, audience, jwks_uri, network) and dictates the fix
  2. For exp/iat failures, sync the proxy host clock (NTP) or account for clock skew
  3. For issuer/audience failures, align verify_issuer/verify_audience with the claims the IdP actually mints
  4. For fetch failures, ensure the proxy can reach the discovery and JWKS URLs (no firewall, valid TLS, correct DNS)

Example fix

# before - verification config does not match the token's claims
litellm_params:
  access_token_discovery_uri: https://idp.example.com/.well-known/openid-configuration
  verify_issuer: https://wrong-issuer.example.com
  verify_audience: my-mcp-api

# after - matches the IdP's issued claims (check with jwt.io)
litellm_params:
  access_token_discovery_uri: https://idp.example.com/.well-known/openid-configuration
  verify_issuer: https://idp.example.com
  verify_audience: mcp-gateway
Defensive patterns

Strategy: try-catch

Validate before calling

import httpx, jwt as pyjwt  
  
def verify_token_will_pass(token: str, discovery_uri: str, issuer: str, audience: str) -> None:  
    doc = httpx.get(discovery_uri, timeout=10).json()  
    assert doc.get("jwks_uri"), "discovery doc lacks jwks_uri"  
    jwks = httpx.get(doc["jwks_uri"], timeout=10).json()  
    header = pyjwt.get_unverified_header(token)  
    assert any(k.get("kid") == header.get("kid") for k in jwks.get("keys", [])), "signing kid not in JWKS"  
    claims = pyjwt.decode(token, options={"verify_signature": False})  
    assert issuer in (claims.get("iss"), None) or claims.get("iss") == issuer, "issuer mismatch"  
    assert audience in claims.get("aud", []), "audience mismatch"

Try / catch

import openai  
  
try:  
    resp = client.responses.create(model=deployment, tools=mcp_tools, input=prompt)  
except openai.AuthenticationError as e:  
    msg = getattr(e, "body", {}).get("error", "") if isinstance(getattr(e, "body", None), dict) else str(e)  
    if msg.startswith("MCPJWTSigner: incoming token verification failed"):  
        reason = msg.rsplit(":", 1)[-1].strip()  
        if "expired" in reason:  
            token = refresh_access_token()  
            return retry_with(token)  
        log_and_surface_auth_diagnostic(reason)  
    raise

Prevention

When it happens

Trigger: An MCP request with a Bearer token that fails verification: expired token (exp), wrong issuer/audience vs verify_issuer/verify_audience, signature not matching JWKS keys, unknown kid, clock skew on iat/nbf, unreachable discovery/JWKS endpoint, or the opaque-token path hitting misconfigured introspection.

Common situations: Tokens from a different IdP than the one configured; system clocks drifted so valid tokens read expired; JWKS endpoint blocked by firewall; rotated signing keys with stale cache; test tokens signed with a dev key against a prod discovery URI.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/826a8fbe1f023ac7. Report an issue: GitHub.