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

  1. Point jwks_uri at a publicly resolvable HTTPS endpoint for the authorization server's JWKS.
  2. 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.
  3. Ensure the JWKS response is under 65536 bytes and the IdP responds within the 10s per-request / 30s overall timeout.
  4. 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

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


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/1433fbf57e155e47. Report an issue: GitHub.