BerriAI/litellm · error · ValueError

MCPJWTSigner: access_token_discovery_uri discovery document

Error message

MCPJWTSigner: access_token_discovery_uri discovery document at {self.access_token_discovery_uri!r} has no 'jwks_uri'.

What it means

When access_token_discovery_uri is configured, MCPJWTSigner fetches the OIDC discovery document to find where verification keys live. _verify_incoming_jwt raises ValueError if the fetched document has no 'jwks_uri' entry, because there is then no way to obtain the JWKS needed to verify incoming JWTs.

Source

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

            doc: Final = await _fetch_oidc_discovery(self.access_token_discovery_uri)
            if "jwks_uri" in doc:
                self._oidc_discovery_doc = doc
                self._oidc_discovery_fetched_at = now
            else:
                return doc
        return self._oidc_discovery_doc or {}

    async def _verify_incoming_jwt(self, raw_token: str) -> dict[str, object]:
        """
        Verify an incoming Bearer JWT against the configured IdP's JWKS.

        Returns the verified payload claims dict.
        Raises jwt.PyJWTError (or subclass) if verification fails.
        """
        discovery: Final = await self._get_oidc_discovery()
        jwks_uri: Final = discovery.get("jwks_uri")
        if not jwks_uri:
            raise ValueError(
                "MCPJWTSigner: access_token_discovery_uri discovery document "
                f"at {self.access_token_discovery_uri!r} has no 'jwks_uri'."
            )

        jwks_keys: Final = await _fetch_jwks(jwks_uri)

        # Only read `kid` from the unverified header — never `alg`.
        # Reading `alg` from an attacker-controlled header enables algorithm
        # confusion attacks (e.g. alg:none, HS256 with the public key as secret).
        # The algorithm is determined from the JWKS key entry instead.
        unverified_header: Final = jwt.get_unverified_header(raw_token)
        kid: Final = unverified_header.get("kid")

        # Build a JWKS object and pick the matching key.
        # PyJWT's PyJWKSet handles key-type parsing and kid matching correctly.
        from jwt import PyJWKSet

        try:

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Point access_token_discovery_uri at the IdP's standard discovery endpoint: https://<idp>/.well-known/openid-configuration
  2. Verify the URL manually: curl it and confirm the JSON contains a non-empty jwks_uri
  3. If the IdP cannot publish jwks_uri, obtain keys another way or drop incoming-token verification (unset access_token_discovery_uri)

Example fix

# before - resource endpoint, no jwks_uri in the document
litellm_params:
  access_token_discovery_uri: https://idp.example.com/oauth2/default

# after - standard OIDC discovery document
litellm_params:
  access_token_discovery_uri: https://idp.example.com/.well-known/openid-configuration
Defensive patterns

Strategy: validation

Validate before calling

import httpx  
  
def check_discovery_doc(uri: str) -> None:  
    doc = httpx.get(uri, timeout=10).json()  
    assert doc.get("jwks_uri"), f"discovery document at {uri} has no jwks_uri - wrong endpoint?"  
  
check_discovery_doc(config_lp["access_token_discovery_uri"])  # run at deploy time

Prevention

When it happens

Trigger: access_token_discovery_uri points at a URL whose JSON response lacks jwks_uri - e.g. a resource-level endpoint instead of the IdP's .well-known/openid-configuration, an IdP that does not advertise JWKS in discovery, or a proxy/gateway returning an error document that parses as JSON.

Common situations: Discovery URL copy-pasted from the issuer base URL without the well-known path; pointing at a Keycloak tenant endpoint that redirects; custom auth servers that omit jwks_uri; intermediate proxies rewriting responses.

Related errors


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