BerriAI/litellm · error · NoMatchingJWTPublicKeyError
No matching public key found. keys={resolved_jwks_url}, kid=
Error message
No matching public key found. keys={resolved_jwks_url}, kid={kid} What it means
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.
Source
Thrown at litellm/proxy/auth/handle_jwt.py:680
if "keys" in response_json:
keys: JWKKeyValue = response_json["keys"]
else:
keys = response_json
await self.user_api_key_cache.async_set_cache(
key=cache_key,
value=keys,
ttl=self._get_public_key_cache_ttl(),
)
else:
keys = cached_keys
public_key: Final = self.parse_keys(keys=keys, kid=kid)
if public_key is not None:
return cast(dict, public_key)
raise NoMatchingJWTPublicKeyError(f"No matching public key found. keys={resolved_jwks_url}, kid={kid}")
async def get_public_key(self, kid: str | None) -> dict:
keys_url: Final = os.getenv("JWT_PUBLIC_KEY_URL")
if keys_url is None:
raise Exception("Missing JWT Public Key URL from environment.")
keys_url_list: Final = [url.strip() for url in keys_url.split(",") if url.strip()]
for key_url in keys_url_list:
try:
return await self._get_public_key_from_jwks_url(jwks_url=key_url, kid=kid)
except NoMatchingJWTPublicKeyError as e:
verbose_proxy_logger.debug("JWT Auth: No matching public key found at %s: %s", key_url, e)
raise NoMatchingJWTPublicKeyError(f"No matching public key found. keys={keys_url_list}, kid={kid}")
def parse_keys(self, keys: JWKKeyValue, kid: str | None) -> JWTKeyItem | None:View on GitHub (pinned to 77b7c6c40c)
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
Example fix
# before: only one IdP's JWKS configured JWT_PUBLIC_KEY_URL=https://idp-a.example.com/protocol/openid-connect/certs # after: comma-separated list - each is tried until kid matches JWT_PUBLIC_KEY_URL=https://idp-a.example.com/protocol/openid-connect/certs,https://idp-b.example.com/protocol/openid-connect/certs
Defensive patterns
Strategy: validation
Validate before calling
import httpx, jwt as pyjwt
async def kid_exists_in_jwks(token: str, jwks_url: str) -> bool:
kid = pyjwt.get_unverified_header(token).get("kid")
async with httpx.AsyncClient() as c:
data = (await c.get(jwks_url)).json()
keys = data.get("keys", data if isinstance(data, list) else [data])
return any(k.get("kid") == kid for k in keys if isinstance(k, dict)) Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- No matching public key found. keys={keys_url_list}, kid={kid
- JWT Auth: OIDC discovery endpoint {url} returned status {res
- JWT Auth: OIDC discovery document at {url} does not contain
- Error parsing response: {e}. Check server logs for original
- Invalid JWT Submitted
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/ad388f73f7fdb1cf.
Report an issue: GitHub.