{"record":{"id":"3299a72046207b95","repo":"BerriAI/litellm","slug":"mcpjwtsigner-access-token-discovery-uri-discovery","errorCode":null,"errorMessage":"MCPJWTSigner: access_token_discovery_uri discovery document at {self.access_token_discovery_uri!r} has no 'jwks_uri'.","messagePattern":"MCPJWTSigner: access_token_discovery_uri discovery document at (.+?) has no 'jwks_uri'\\.","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py","lineNumber":423,"sourceCode":"            doc: Final = await _fetch_oidc_discovery(self.access_token_discovery_uri)\n            if \"jwks_uri\" in doc:\n                self._oidc_discovery_doc = doc\n                self._oidc_discovery_fetched_at = now\n            else:\n                return doc\n        return self._oidc_discovery_doc or {}\n\n    async def _verify_incoming_jwt(self, raw_token: str) -> dict[str, object]:\n        \"\"\"\n        Verify an incoming Bearer JWT against the configured IdP's JWKS.\n\n        Returns the verified payload claims dict.\n        Raises jwt.PyJWTError (or subclass) if verification fails.\n        \"\"\"\n        discovery: Final = await self._get_oidc_discovery()\n        jwks_uri: Final = discovery.get(\"jwks_uri\")\n        if not jwks_uri:\n            raise ValueError(\n                \"MCPJWTSigner: access_token_discovery_uri discovery document \"\n                f\"at {self.access_token_discovery_uri!r} has no 'jwks_uri'.\"\n            )\n\n        jwks_keys: Final = await _fetch_jwks(jwks_uri)\n\n        # Only read `kid` from the unverified header — never `alg`.\n        # Reading `alg` from an attacker-controlled header enables algorithm\n        # confusion attacks (e.g. alg:none, HS256 with the public key as secret).\n        # The algorithm is determined from the JWKS key entry instead.\n        unverified_header: Final = jwt.get_unverified_header(raw_token)\n        kid: Final = unverified_header.get(\"kid\")\n\n        # Build a JWKS object and pick the matching key.\n        # PyJWT's PyJWKSet handles key-type parsing and kid matching correctly.\n        from jwt import PyJWKSet\n\n        try:","sourceCodeStart":405,"sourceCodeEnd":441,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py#L405-L441","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Point access_token_discovery_uri at the IdP's standard discovery endpoint: https://<idp>/.well-known/openid-configuration","Verify the URL manually: curl it and confirm the JSON contains a non-empty jwks_uri","If the IdP cannot publish jwks_uri, obtain keys another way or drop incoming-token verification (unset access_token_discovery_uri)"],"exampleFix":"# before - resource endpoint, no jwks_uri in the document\nlitellm_params:\n  access_token_discovery_uri: https://idp.example.com/oauth2/default\n\n# after - standard OIDC discovery document\nlitellm_params:\n  access_token_discovery_uri: https://idp.example.com/.well-known/openid-configuration","handlingStrategy":"validation","validationCode":"import httpx  \n  \ndef check_discovery_doc(uri: str) -> None:  \n    doc = httpx.get(uri, timeout=10).json()  \n    assert doc.get(\"jwks_uri\"), f\"discovery document at {uri} has no jwks_uri - wrong endpoint?\"  \n  \ncheck_discovery_doc(config_lp[\"access_token_discovery_uri\"])  # run at deploy time","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always use the IdP's .well-known/openid-configuration URL for access_token_discovery_uri","Add a startup/deploy smoke check that curls the discovery URL and asserts jwks_uri exists and resolves"],"tags":["mcp","jwt","oidc","discovery","jwks"],"backgroundTag":"oidc-discovery-misconfigured","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}