{"record":{"id":"ff0d3fb510a8ec83","repo":"BerriAI/litellm","slug":"no-matching-public-key-found-keys-keys-url-list","errorCode":null,"errorMessage":"No matching public key found. keys={keys_url_list}, 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":696,"sourceCode":"            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:\n        public_key: JWTKeyItem | None = None\n        if len(keys) == 1:\n            if isinstance(keys, dict) and (keys.get(\"kid\", None) == kid or kid is None):\n                public_key = keys\n            elif isinstance(keys, list) and (keys[0].get(\"kid\", None) == kid or kid is None):\n                public_key = keys[0]\n        elif len(keys) > 1:\n            for key in keys:\n                if isinstance(key, dict):\n                    key_kid = key.get(\"kid\", None)\n                else:\n                    key_kid = None\n                if kid is not None and isinstance(key, dict) and key_kid is not None and key_kid == kid:\n                    public_key = key\n\n        return public_key","sourceCodeStart":678,"sourceCodeEnd":714,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/auth/handle_jwt.py#L678-L714","documentation":"NoMatchingJWTPublicKeyError raised at the end of get_public_key after iterating the entire comma-separated JWT_PUBLIC_KEY_URL list: every listed JWKS was fetched and none contained a key matching the token's kid. Each per-URL miss is logged at debug level ('JWT Auth: No matching public key found at ...') before this final exception.","triggerScenarios":"JWT_PUBLIC_KEY_URL lists one or more JWKS/discovery URLs; a token arrives whose kid header matches no key in any of them - wrong IdP, a freshly rotated key not yet published, or stale cached JWKS entries for all URLs.","commonSituations":"Token minted by an IdP that is not in the configured list (multi-tenant setup missing a tenant); IdP mid-rotation where the new signing key is not yet in the JWKS; cached keys (litellm_jwt_auth_keys_<url>) predating a rotation and TTL not yet expired.","solutions":["Extract the token's kid from the header and curl each configured JWKS URL to confirm which one should contain it","Add the missing IdP's JWKS URL to the comma-separated JWT_PUBLIC_KEY_URL value","After a rotation, wait for the JWKS cache TTL to expire (or restart the proxy) so fresh keys are pulled","Check proxy debug logs for the per-URL 'No matching public key found at' lines to see exactly which URLs were tried"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import httpx, jwt as pyjwt, os\n\nasync def kid_exists_in_any_jwks(token: str) -> bool:\n    kid = pyjwt.get_unverified_header(token).get(\"kid\")\n    urls = [u.strip() for u in os.environ[\"JWT_PUBLIC_KEY_URL\"].split(\",\") if u.strip()]\n    async with httpx.AsyncClient() as c:\n        for url in urls:\n            data = (await c.get(url)).json()\n            keys = data.get(\"keys\", data if isinstance(data, list) else [data])\n            if any(k.get(\"kid\") == kid for k in keys if isinstance(k, dict)):\n                return True\n    return False","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Maintain the full list of minting IdPs in JWT_PUBLIC_KEY_URL as tenants are added","Run a periodic canary that checks each configured JWKS is fetchable and kid-bearing","Treat sudden kid misses right after IdP maintenance as rotation - verify new keys are published"],"tags":["jwt","jwks","kid","multi-tenant","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-14T05:17:10.506Z"}