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_keyView on GitHub (pinned to 77b7c6c40c)
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
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
- 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
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
- No matching public key found. keys={resolved_jwks_url}, 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/ff0d3fb510a8ec83.
Report an issue: GitHub.