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

  1. Decode the token header (jwt.io or jwt.get_unverified_header) and compare its kid against the kids in curl <jwks_url> output
  2. If the key lives in another JWKS, add that URL to the comma-separated JWT_PUBLIC_KEY_URL list - the loop will try each
  3. After IdP key rotation, remember the JWKS is cached (litellm_jwt_auth_keys_<url>); wait out the TTL or restart/flush the cache
  4. 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

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


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