{"record":{"id":"45220b682b4699f0","repo":"PrefectHQ/fastmcp","slug":"no-matching-key-found-for-kid-kid-in-jwks","errorCode":null,"errorMessage":"No matching key found for kid={kid} in JWKS","messagePattern":"No matching key found for kid=(.+?) in JWKS","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/cimd.py","lineNumber":694,"sourceCode":"        keys = jwks.get(\"keys\", [])\n        if not keys:\n            raise ValueError(\"JWKS document contains no keys\")\n\n        matching_key = None\n        for key in keys:\n            if kid and key.get(\"kid\") == kid:\n                matching_key = key\n                break\n\n        if not matching_key:\n            # If no kid match, try first key as fallback\n            if len(keys) == 1:\n                matching_key = keys[0]\n                self.logger.warning(\n                    \"No matching kid in JWKS, using single available key\"\n                )\n            else:\n                raise ValueError(f\"No matching key found for kid={kid} in JWKS\")\n\n        # Convert JWK to PEM\n        try:\n            return _jwk_to_pem(matching_key)\n        except (JoseError, TypeError, ValueError) as e:\n            raise ValueError(f\"Failed to convert JWK to PEM: {e}\") from e\n\n\nclass CIMDClientManager:\n    \"\"\"Manages all CIMD client operations for OAuth proxy.\n\n    This class encapsulates:\n    - CIMD client detection\n    - Document fetching and validation\n    - Synthetic OAuth client creation\n    - Private key JWT assertion validation\n\n    This allows the OAuth proxy to delegate all CIMD-specific logic to a","sourceCodeStart":676,"sourceCodeEnd":712,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/cimd.py#L676-L712","documentation":"The JWKS document has keys, but none carries a `kid` matching the `kid` header of the incoming client assertion, and there is more than one key so the library refuses to guess. It is raised from `_extract_public_key_from_jwks` during assertion validation.","triggerScenarios":"Token's JWT header `kid` does not equal any `kid` in the JWKS while the JWKS contains 2+ keys. With exactly one key the library falls back to using it (with a warning) instead of raising.","commonSituations":"Issuer rotated signing keys but kept the old key in the JWKS; client signed with a key whose kid isn't published yet; kid string mismatch (case, whitespace) between token header and JWKS entry; multiple keys published and the client library omits kid in the token header.","solutions":["Make the client sign assertions with the key whose kid matches an entry in the JWKS","Ensure the JWKS publishes all currently-valid signing keys with correct `kid` values","If the client omits `kid`, either add the header or reduce the JWKS to a single key so the fallback applies","Verify the kid strings match exactly (no casing/whitespace differences) between token and JWKS"],"exampleFix":"// before (token kid not in JWKS)\n{\"keys\": [{\"kid\": \"key-1\", ...}, {\"kid\": \"key-2\", ...}]}\n// token header: {\"kid\": \"key-old\", \"alg\": \"RS256\"}\n// after — publish/rotate so kid matches\n{\"keys\": [{\"kid\": \"key-old\", ...}, {\"kid\": \"key-1\", ...}]}","handlingStrategy":"validation","validationCode":"import jwt\nheader = jwt.get_unverified_header(assertion)\nkids = {k.get(\"kid\") for k in jwks[\"keys\"]}\nif header.get(\"kid\") not in kids and len(jwks[\"keys\"]) > 1:\n    raise ValueError(f\"kid {header.get('kid')!r} not in JWKS and JWKS is multi-key\")","typeGuard":"def kid_in_jwks(token_kid: str | None, jwks: dict) -> bool:\n    if token_kid is None:\n        return len(jwks.get(\"keys\", [])) == 1  # only single-key fallback works\n    return any(k.get(\"kid\") == token_kid for k in jwks.get(\"keys\", []))","tryCatchPattern":"try:\n    key = extract_public_key(jwks, kid)\nexcept ValueError as e:\n    if str(e).startswith(\"No matching key found for kid=\"):\n        logger.warning(\"Key rotation mismatch: token kid=%s not published\", kid)\n    raise","preventionTips":["Keep the token-signing key's kid present in the published JWKS at all times (publish new key before signing with it)","Ensure the client library always sets the kid header","Use exact string equality for kid values (watch casing/whitespace)","During rotation, keep both old and new keys in the JWKS until all tokens signed with the old key expire"],"tags":["oauth","jwks","kid-mismatch","private-key-jwt"],"backgroundTag":"jwks-kid-not-found","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}