{"record":{"id":"751d671b6fa2f038","repo":"PrefectHQ/fastmcp","slug":"multiple-keys-in-jwks-but-no-key-id-kid-in-token","errorCode":null,"errorMessage":"Multiple keys in JWKS but no key ID (kid) in token","messagePattern":"Multiple keys in JWKS but no key ID \\(kid\\) in token","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/providers/jwt.py","lineNumber":428,"sourceCode":"                            \"JWKS key lookup failed: key ID '%s' is present \"\n                            \"but its key type is unsupported\",\n                            kid,\n                        )\n                        raise ValueError(\n                            f\"Key ID '{kid}' found in JWKS but its key type \"\n                            \"is unsupported\"\n                        )\n                    self.logger.debug(\n                        \"JWKS key lookup failed: key ID '%s' not found\", kid\n                    )\n                    raise ValueError(f\"Key ID '{kid}' not found in JWKS\")\n                return self._jwks_cache[kid]\n            else:\n                # No kid in token - only allow if there's exactly one key\n                if len(self._jwks_cache) == 1:\n                    return next(iter(self._jwks_cache.values()))\n                elif len(self._jwks_cache) > 1:\n                    raise ValueError(\n                        \"Multiple keys in JWKS but no key ID (kid) in token\"\n                    )\n                else:\n                    raise ValueError(\"No keys found in JWKS\")\n\n        except (SSRFError, SSRFFetchError) as e:\n            self.logger.debug(\"JWKS fetch blocked by SSRF protection: %s\", e)\n            raise ValueError(f\"Failed to fetch JWKS: {e}\") from e\n        except httpx2.HTTPError as e:\n            raise ValueError(f\"Failed to fetch JWKS: {e}\") from e\n        except json.JSONDecodeError as e:\n            raise ValueError(f\"Invalid JWKS JSON: {e}\") from e\n        except (JoseError, TypeError, KeyError, ValueError) as e:\n            self.logger.debug(\"JWKS key processing failed: %s\", e)\n            raise ValueError(f\"Failed to process JWKS: {e}\") from e\n\n    async def _fetch_jwks(self) -> dict[str, Any]:\n        \"\"\"Fetch JWKS data, using SSRF-safe or standard fetch based on config.\"\"\"","sourceCodeStart":410,"sourceCodeEnd":446,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/providers/jwt.py#L410-L446","documentation":"The token has no kid header, so the library tries to select a key from the JWKS automatically. It only permits this when the JWKS contains exactly one key; with multiple keys it cannot know which one signed the token and raises this error instead of guessing. The fix is on the token issuer side: sign tokens with a kid header.","triggerScenarios":"Calling verify_token on a JWT whose header lacks kid while the configured jwks_uri resolves to a key set with two or more keys.","commonSituations":"IdP rotated to multiple signing keys (rotation implies >1 published key) but a client/token-issuing component still omits kid; hand-rolled token signing without kid; older SDK that didn't set kid, run against a multi-key IdP.","solutions":["Configure the token issuer to include the kid header in signed JWTs (most IdPs do this by default once multiple keys exist)","Ensure only one signing key is active/published at the JWKS if you must support kid-less tokens","If you control signing manually, pass the key id when signing (e.g. jwt.encode(..., headers={\"kid\": key_id}) with PyJWT)","Upgrade the issuing SDK so it sets kid automatically"],"exampleFix":"// before\ntoken = jwt.encode(claims, private_key, algorithm=\"RS256\")\n// after\ntoken = jwt.encode(claims, private_key, algorithm=\"RS256\", headers={\"kid\": \"my-key-id\"})\n","handlingStrategy":"validation","validationCode":"import jwt\nheader = jwt.get_unverified_header(token)\nif not header.get(\"kid\"):\n    raise RuntimeError(\"Token missing kid header; issuer must include kid when JWKS has multiple keys\")","typeGuard":"def token_has_kid(token: str) -> bool:\n    return bool(jwt.get_unverified_header(token).get(\"kid\"))","tryCatchPattern":"try:\n    claims = await verifier.verify_token(token)\nexcept ValueError as e:\n    if \"no key ID (kid) in token\" in str(e):\n        raise RuntimeError(\"Issuing SDK must sign tokens with a kid header\") from e\n    raise","preventionTips":["Ensure the token issuer sets kid in the JWT header, especially before enabling key rotation","Keep only one active signing key if kid-less tokens must be supported","Add a test asserting issued tokens contain a kid header","Upgrade old issuing SDKs that omit kid"],"tags":["auth","jwt","jwks","kid"],"backgroundTag":"missing-kid-header","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}