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).
Description
JWTVerifier(jwks_uri=...)converts every key in the JWKS to PEM eagerly, with no per-key guard._jwk_to_pemraisesValueErroron 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 bareinvalid_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:Reproduction
With a valid RS256 token from that issuer:
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:
Optionally also skip keys whose
alg/usecan't match the configuredalgorithm. Happy to send a PR.Environment
fastmcp 3.4.4, Python 3.12,
JWTVerifier(jwks_uri=..., issuer=..., audience=...)inRemoteAuthProvider, verified against Rauthy 0.36.0. Workaround: fetch the JWKS yourself, pick the RS256 key, and pass it aspublic_key=<pem>(loses rotation).