PrefectHQ/fastmcp · error · ValueError

Key ID '{kid}' not found in JWKS

Error message

Key ID '{kid}' not found in JWKS

What it means

The token carries a kid whose corresponding key could not be found in the fetched JWKS — either the kid matched no entry, or the only matching entry was skipped. The library raises this so signature verification fails fast instead of attempting verification with the wrong key. It almost always means the token was signed by a key the identity provider no longer publishes (or by a different provider entirely).

Source

Thrown at fastmcp_slim/fastmcp/server/auth/providers/jwt.py:421

            self._jwks_cache_time = current_time

            # Select the appropriate key
            if kid:
                if kid not in self._jwks_cache:
                    if kid in skipped_kids:
                        self.logger.debug(
                            "JWKS key lookup failed: key ID '%s' is present "
                            "but its key type is unsupported",
                            kid,
                        )
                        raise ValueError(
                            f"Key ID '{kid}' found in JWKS but its key type "
                            "is unsupported"
                        )
                    self.logger.debug(
                        "JWKS key lookup failed: key ID '%s' not found", kid
                    )
                    raise ValueError(f"Key ID '{kid}' not found in JWKS")
                return self._jwks_cache[kid]
            else:
                # No kid in token - only allow if there's exactly one key
                if len(self._jwks_cache) == 1:
                    return next(iter(self._jwks_cache.values()))
                elif len(self._jwks_cache) > 1:
                    raise ValueError(
                        "Multiple keys in JWKS but no key ID (kid) in token"
                    )
                else:
                    raise ValueError("No keys found in JWKS")

        except (SSRFError, SSRFFetchError) as e:
            self.logger.debug("JWKS fetch blocked by SSRF protection: %s", e)
            raise ValueError(f"Failed to fetch JWKS: {e}") from e
        except httpx2.HTTPError as e:
            raise ValueError(f"Failed to fetch JWKS: {e}") from e
        except json.JSONDecodeError as e:

View on GitHub (pinned to 1f02114297)

Solutions

  1. Wait for/reduce the JWKS cache TTL (or restart the process) so the verifier re-fetches the current JWKS and picks up the rotated key
  2. Compare the token's kid (decode the JWT header) against the kids currently published at your jwks_uri to confirm the mismatch
  3. Confirm jwks_uri points to the correct issuer/realm that actually issued the token
  4. Re-issue tokens with the current signing key; discard tokens signed by retired keys

Example fix

// before: stale cache after key rotation
verifier = JWTVerifier(jwks_uri=..., cache_ttl=3600)
// after: shorten TTL so rotated keys are picked up quickly
verifier = JWTVerifier(jwks_uri=..., cache_ttl=300)
Defensive patterns

Strategy: retry

Validate before calling

import jwt, httpx
kid = jwt.get_unverified_header(token).get("kid")
published = {k.get("kid") for k in httpx.get(jwks_uri).json()["keys"]}
if kid and kid not in published:
    # token signed by an unpublished/retired key
    raise RuntimeError(f"kid {kid!r} not in current JWKS")

Type guard

def kid_in_jwks(token: str, jwks_keys: list[dict]) -> bool:
    kid = jwt.get_unverified_header(token).get("kid")
    return kid is not None and any(k.get("kid") == kid for k in jwks_keys)

Try / catch

try:
    claims = await verifier.verify_token(token)
except ValueError as e:
    if "not found in JWKS" in str(e):
        # possible key rotation: allow a short retry after cache refresh
        await asyncio.sleep(0.5)
        claims = await verifier.verify_token(token)
    else:
        raise

Prevention

When it happens

Trigger: Calling verify_token on a JWT with a kid that is absent from the JWKS at the configured jwks_uri, including during key rotation before the local JWKS cache (cache TTL) refreshes.

Common situations: IdP rotated signing keys and the verifier's cached JWKS is stale until the TTL expires; token issued by a different environment/tenant (staging token verified against prod IdP); typo in jwks_uri pointing at the wrong realm; tokens minted by a deprecated IdP.

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/db161bdd18f374c4. Report an issue: GitHub.