PrefectHQ/fastmcp · error · ValueError
Failed to fetch JWKS: {e}
Error message
Failed to fetch JWKS: {e} What it means
This ValueError from JWTVerifier._get_jwks_key means fetching the JWKS document from the issuer's jwks_uri was blocked by SSRF protection (SSRFError/SSRFFetchError). When ssrf_safe mode is enabled, the fetcher rejects URLs resolving to private/loopback/CGNAT/link-local IPs or oversized responses, to stop attackers from pointing the verifier at internal services. The library wraps the SSRF failure in a generic 'Failed to fetch JWKS' ValueError while logging details at debug level.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/providers/jwt.py:436
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:
raise ValueError(f"Invalid JWKS JSON: {e}") from e
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
async def _fetch_jwks(self) -> dict[str, Any]:
"""Fetch JWKS data, using SSRF-safe or standard fetch based on config."""
if not self.jwks_uri:
raise ValueError("JWKS URI not configured")
if self.ssrf_safe:
content = await ssrf_safe_fetch(
self.jwks_uri,
max_size=65536,
timeout=10.0,View on GitHub (pinned to 1f02114297)
Solutions
- Point jwks_uri at a publicly resolvable HTTPS endpoint for the authorization server's JWKS.
- If you control the environment and trust the URI, disable SSRF-safe mode (e.g. construct JWTVerifier with ssrf_safe=False) so the standard httpx fetch path is used.
- Ensure the JWKS response is under 65536 bytes and the IdP responds within the 10s per-request / 30s overall timeout.
- Enable debug logging on this provider's logger to see the specific SSRFError reason (blocked IP category, size, or timeout).
Example fix
// before verifier = JWTVerifier(jwks_uri="http://localhost:8080/realms/master/protocol/openid-connect/certs", ssrf_safe=True) // after verifier = JWTVerifier(jwks_uri="https://idp.example.com/realms/master/protocol/openid-connect/certs", ssrf_safe=True) // or, for trusted internal IdPs: verifier = JWTVerifier(jwks_uri="http://keycloak.internal:8080/...", ssrf_safe=False)
Defensive patterns
Strategy: validation
Validate before calling
import ipaddress, socket
from urllib.parse import urlparse
def is_public_jwks_uri(uri: str) -> bool:
host = urlparse(uri).hostname or ""
try:
infos = socket.getaddrinfo(host, None)
except socket.gaierror:
return False
for info in infos:
ip = ipaddress.ip_address(info[4][0])
if not ip.is_global:
return False
return urlparse(uri).scheme == "https"
# assert is_public_jwks_uri(jwks_uri) before constructing a ssrf_safe verifier Type guard
def is_safe_jwks_uri(uri: str | None) -> bool:
return bool(uri) and urlparse(uri).scheme in ("https", "http") and is_public_jwks_uri(uri) Try / catch
from fastmcp.server.auth.providers.jwt import JWTVerifier
try:
claims = await verifier.verify_token(token)
except ValueError as e:
if "Failed to fetch JWKS" in str(e):
logger.warning("JWKS fetch blocked (SSRF or network): %s", e)
return None # reject token, fail closed Prevention
- Use publicly reachable HTTPS JWKS endpoints in production.
- Only disable ssrf_safe for URIs you fully control and trust.
- Keep JWKS documents small (<64KB) and the IdP responsive (<10s).
- Log at debug level in staging to surface SSRF block reasons early.
When it happens
Trigger: Calling verify_token (via _get_verification_key -> _get_jwks_key) on a JWTVerifier constructed with ssrf_safe=True (or SSRF-safe config) whose jwks_uri resolves to a private IP (10.x, 192.168.x), loopback (127.0.0.1), CGNAT (100.64.x), or link-local address; also triggered when the fetch exceeds the 65536-byte max_size or 10s/30s timeouts enforced by ssrf_safe_fetch.
Common situations: Developers testing locally against a self-hosted IdP (Keycloak/Auth0-dev on localhost) with SSRF protection on; Docker-compose setups where the JWKS host is an internal service IP; jwks_uri set to an internal metadata or staging endpoint; oversized or slow JWKS endpoints behind VPNs.
Related errors
- {str(e) from SSRFError/SSRFFetchError}
- CIMD jwks_uri failed SSRF validation: {e}
- Failed to extract key ID from token: {e}
- JWKS URI not configured
- Key ID '{kid}' found in JWKS but its key type is unsupported
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/1433fbf57e155e47.
Report an issue: GitHub.