BerriAI/litellm · error · NoMatchingJWTPublicKeyError

No matching public key found. keys={keys_url_list}, kid={kid

Error message

No matching public key found. keys={keys_url_list}, kid={kid}

What it means

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.

Source

Thrown at litellm/proxy/auth/handle_jwt.py:696

            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:
        public_key: JWTKeyItem | None = None
        if len(keys) == 1:
            if isinstance(keys, dict) and (keys.get("kid", None) == kid or kid is None):
                public_key = keys
            elif isinstance(keys, list) and (keys[0].get("kid", None) == kid or kid is None):
                public_key = keys[0]
        elif len(keys) > 1:
            for key in keys:
                if isinstance(key, dict):
                    key_kid = key.get("kid", None)
                else:
                    key_kid = None
                if kid is not None and isinstance(key, dict) and key_kid is not None and key_kid == kid:
                    public_key = key

        return public_key

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Extract the token's kid from the header and curl each configured JWKS URL to confirm which one should contain it
  2. Add the missing IdP's JWKS URL to the comma-separated JWT_PUBLIC_KEY_URL value
  3. After a rotation, wait for the JWKS cache TTL to expire (or restart the proxy) so fresh keys are pulled
  4. Check proxy debug logs for the per-URL 'No matching public key found at' lines to see exactly which URLs were tried
Defensive patterns

Strategy: validation

Validate before calling

import httpx, jwt as pyjwt, os

async def kid_exists_in_any_jwks(token: str) -> bool:
    kid = pyjwt.get_unverified_header(token).get("kid")
    urls = [u.strip() for u in os.environ["JWT_PUBLIC_KEY_URL"].split(",") if u.strip()]
    async with httpx.AsyncClient() as c:
        for url in urls:
            data = (await c.get(url)).json()
            keys = data.get("keys", data if isinstance(data, list) else [data])
            if any(k.get("kid") == kid for k in keys if isinstance(k, dict)):
                return True
    return False

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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