Skip to content

JWTVerifier: one unsupported JWKS key (OKP/Ed25519) poisons the entire key cache — all tokens rejected even when signed RS256 #4515

Description

@impuls42

Description

JWTVerifier(jwks_uri=...) converts every key in the JWKS to PEM eagerly, with no per-key guard. _jwk_to_pem raises ValueError on any key type it doesn't support (e.g. OKP/Ed25519), and because the conversion happens inside the loop that builds the key cache, one unsupported key aborts the entire key set — including the RSA key the token actually needs.

Any authorization server that publishes an Ed25519 key alongside RSA keys is therefore unusable with jwks_uri, regardless of the algorithm the token is signed with. Every token is rejected with a bare invalid_token.

Ory Hydra and Rauthy both do this; Rauthy publishes RS256/RS384/RS512 and an Ed25519 key by default, so JWTVerifier(jwks_uri=<rauthy>/oidc/certs') can never verify anything.

Code

src/fastmcp/server/auth/providers/jwt.py:

# Cache all keys
self._jwks_cache = {}
for key_data in jwks_data.get("keys", []):
    key_kid = key_data.get("kid")
    public_key = _jwk_to_pem(key_data)      # <-- raises on OKP; kills the whole loop
    if key_kid:
        self._jwks_cache[key_kid] = public_key
    ...
except (JoseError, TypeError, KeyError, ValueError) as e:
    self.logger.debug("JWKS key processing failed: %s", e)
    raise ValueError(f"Failed to process JWKS: {e}") from e

Reproduction

import httpx
from fastmcp.server.auth.providers.jwt import _jwk_to_pem

jwks = httpx.get("https://<rauthy>/auth/v1/oidc/certs").json()
for k in jwks["keys"]:
    try:
        _jwk_to_pem(k); print(k["kty"], k.get("alg"), "-> OK")
    except Exception as e:
        print(k["kty"], k.get("alg"), "-> RAISES:", e)
RSA  RS256 -> OK
RSA  RS384 -> OK
RSA  RS512 -> OK
OKP  EdDSA -> RAISES: Unsupported JWK key type: 'OKP'

With a valid RS256 token from that issuer:

DEBUG JWKS key processing failed: Unsupported JWK key type: 'OKP'
DEBUG Token validation failed: Failed to process JWKS: Unsupported JWK key type: 'OKP'
-> verify_token() returns None -> 401 invalid_token

The same token decodes fine with PyJWT against the RS256 key from the same JWKS, so the token is valid — only the eager conversion of the unrelated OKP key fails.

Expected behavior

An unsupported key in the JWKS should be skipped, not fatal. The verifier should still cache and use the keys it can handle, and only fail if the token's own kid/alg is unsupported.

Suggested fix

Guard the per-key conversion:

for key_data in jwks_data.get("keys", []):
    key_kid = key_data.get("kid")
    try:
        public_key = _jwk_to_pem(key_data)
    except (ValueError, TypeError, KeyError) as e:
        self.logger.debug("Skipping unsupported JWKS key %s: %s", key_kid, e)
        continue
    ...

Optionally also skip keys whose alg/use can't match the configured algorithm. Happy to send a PR.

Environment

fastmcp 3.4.4, Python 3.12, JWTVerifier(jwks_uri=..., issuer=..., audience=...) in RemoteAuthProvider, verified against Rauthy 0.36.0. Workaround: fetch the JWKS yourself, pick the RS256 key, and pass it as public_key=<pem> (loses rotation).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

authRelated to authentication (Bearer, JWT, OAuth, WorkOS) for client or server.bugSomething isn't working. Reports of errors, unexpected behavior, or broken functionality.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions