{"record":{"id":"ad388f73f7fdb1cf","repo":"BerriAI/litellm","slug":"no-matching-public-key-found-keys-resolved-jwks","errorCode":null,"errorMessage":"No matching public key found. keys={resolved_jwks_url}, kid={kid}","messagePattern":"No matching public key found\\. keys=(.+?), kid=(.+?)","errorType":"exception","errorClass":"NoMatchingJWTPublicKeyError","httpStatus":null,"severity":"error","filePath":"litellm/proxy/auth/handle_jwt.py","lineNumber":680,"sourceCode":"\n            if \"keys\" in response_json:\n                keys: JWKKeyValue = response_json[\"keys\"]\n            else:\n                keys = response_json\n\n            await self.user_api_key_cache.async_set_cache(\n                key=cache_key,\n                value=keys,\n                ttl=self._get_public_key_cache_ttl(),\n            )\n        else:\n            keys = cached_keys\n\n        public_key: Final = self.parse_keys(keys=keys, kid=kid)\n        if public_key is not None:\n            return cast(dict, public_key)\n\n        raise NoMatchingJWTPublicKeyError(f\"No matching public key found. keys={resolved_jwks_url}, kid={kid}\")\n\n    async def get_public_key(self, kid: str | None) -> dict:\n        keys_url: Final = os.getenv(\"JWT_PUBLIC_KEY_URL\")\n\n        if keys_url is None:\n            raise Exception(\"Missing JWT Public Key URL from environment.\")\n\n        keys_url_list: Final = [url.strip() for url in keys_url.split(\",\") if url.strip()]\n\n        for key_url in keys_url_list:\n            try:\n                return await self._get_public_key_from_jwks_url(jwks_url=key_url, kid=kid)\n            except NoMatchingJWTPublicKeyError as e:\n                verbose_proxy_logger.debug(\"JWT Auth: No matching public key found at %s: %s\", key_url, e)\n\n        raise NoMatchingJWTPublicKeyError(f\"No matching public key found. keys={keys_url_list}, kid={kid}\")\n\n    def parse_keys(self, keys: JWKKeyValue, kid: str | None) -> JWTKeyItem | None:","sourceCodeStart":662,"sourceCodeEnd":698,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/auth/handle_jwt.py#L662-L698","documentation":"NoMatchingJWTPublicKeyError raised at the end of _get_public_key_from_jwks_url: the JWKS was fetched and parsed, but parse_keys found no key matching the token's kid header (or the single-key JWKS's kid differs from the token's kid when kid is not None). In auth_jwt's get_public_key loop this exception is caught per-URL and the next configured URL is tried; it only surfaces directly when a single URL is configured.","triggerScenarios":"A JWT whose kid header does not match any kid in the served JWKS - e.g. keys were just rotated on the IdP, the token comes from a different IdP/region than this JWKS, or a stale JWKS is still cached under litellm_jwt_auth_keys_<url> within its TTL.","commonSituations":"Multiple comma-separated URLs in JWT_PUBLIC_KEY_URL where the token's IdP is not (yet) listed; key rotation where the proxy still serves cached old keys until the TTL expires; tokens minted by a pre-prod tenant sent to a prod proxy.","solutions":["Decode the token header (jwt.io or jwt.get_unverified_header) and compare its kid against the kids in curl <jwks_url> output","If the key lives in another JWKS, add that URL to the comma-separated JWT_PUBLIC_KEY_URL list - the loop will try each","After IdP key rotation, remember the JWKS is cached (litellm_jwt_auth_keys_<url>); wait out the TTL or restart/flush the cache","For a single-key JWKS with no kid in the token, ensure the JWK either omits kid or matches - parse_keys requires kid match when the token has one"],"exampleFix":"# before: only one IdP's JWKS configured\nJWT_PUBLIC_KEY_URL=https://idp-a.example.com/protocol/openid-connect/certs\n\n# after: comma-separated list - each is tried until kid matches\nJWT_PUBLIC_KEY_URL=https://idp-a.example.com/protocol/openid-connect/certs,https://idp-b.example.com/protocol/openid-connect/certs","handlingStrategy":"validation","validationCode":"import httpx, jwt as pyjwt\n\nasync def kid_exists_in_jwks(token: str, jwks_url: str) -> bool:\n    kid = pyjwt.get_unverified_header(token).get(\"kid\")\n    async with httpx.AsyncClient() as c:\n        data = (await c.get(jwks_url)).json()\n    keys = data.get(\"keys\", data if isinstance(data, list) else [data])\n    return any(k.get(\"kid\") == kid for k in keys if isinstance(k, dict))","typeGuard":null,"tryCatchPattern":null,"preventionTips":["List every IdP that mints accepted tokens in the comma-separated JWT_PUBLIC_KEY_URL","After IdP key rotation, expect a TTL window of cache-stale misses - flush the key cache or restart","Log each token's kid on auth failures to correlate with JWKS contents quickly"],"tags":["jwt","jwks","kid","key-rotation","authentication"],"backgroundTag":"jwt-signing-key-not-found","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}