{"record":{"id":"1433fbf57e155e47","repo":"PrefectHQ/fastmcp","slug":"failed-to-fetch-jwks-e","errorCode":null,"errorMessage":"Failed to fetch JWKS: {e}","messagePattern":"Failed to fetch JWKS: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/providers/jwt.py","lineNumber":436,"sourceCode":"                    self.logger.debug(\n                        \"JWKS key lookup failed: key ID '%s' not found\", kid\n                    )\n                    raise ValueError(f\"Key ID '{kid}' not found in JWKS\")\n                return self._jwks_cache[kid]\n            else:\n                # No kid in token - only allow if there's exactly one key\n                if len(self._jwks_cache) == 1:\n                    return next(iter(self._jwks_cache.values()))\n                elif len(self._jwks_cache) > 1:\n                    raise ValueError(\n                        \"Multiple keys in JWKS but no key ID (kid) in token\"\n                    )\n                else:\n                    raise ValueError(\"No keys found in JWKS\")\n\n        except (SSRFError, SSRFFetchError) as e:\n            self.logger.debug(\"JWKS fetch blocked by SSRF protection: %s\", e)\n            raise ValueError(f\"Failed to fetch JWKS: {e}\") from e\n        except httpx2.HTTPError as e:\n            raise ValueError(f\"Failed to fetch JWKS: {e}\") from e\n        except json.JSONDecodeError as e:\n            raise ValueError(f\"Invalid JWKS JSON: {e}\") from e\n        except (JoseError, TypeError, KeyError, ValueError) as e:\n            self.logger.debug(\"JWKS key processing failed: %s\", e)\n            raise ValueError(f\"Failed to process JWKS: {e}\") from e\n\n    async def _fetch_jwks(self) -> dict[str, Any]:\n        \"\"\"Fetch JWKS data, using SSRF-safe or standard fetch based on config.\"\"\"\n        if not self.jwks_uri:\n            raise ValueError(\"JWKS URI not configured\")\n\n        if self.ssrf_safe:\n            content = await ssrf_safe_fetch(\n                self.jwks_uri,\n                max_size=65536,\n                timeout=10.0,","sourceCodeStart":418,"sourceCodeEnd":454,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/providers/jwt.py#L418-L454","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\nverifier = JWTVerifier(jwks_uri=\"http://localhost:8080/realms/master/protocol/openid-connect/certs\", ssrf_safe=True)\n// after\nverifier = JWTVerifier(jwks_uri=\"https://idp.example.com/realms/master/protocol/openid-connect/certs\", ssrf_safe=True)\n// or, for trusted internal IdPs:\nverifier = JWTVerifier(jwks_uri=\"http://keycloak.internal:8080/...\", ssrf_safe=False)","handlingStrategy":"validation","validationCode":"import ipaddress, socket\nfrom urllib.parse import urlparse\n\ndef is_public_jwks_uri(uri: str) -> bool:\n    host = urlparse(uri).hostname or \"\"\n    try:\n        infos = socket.getaddrinfo(host, None)\n    except socket.gaierror:\n        return False\n    for info in infos:\n        ip = ipaddress.ip_address(info[4][0])\n        if not ip.is_global:\n            return False\n    return urlparse(uri).scheme == \"https\"\n\n# assert is_public_jwks_uri(jwks_uri) before constructing a ssrf_safe verifier","typeGuard":"def is_safe_jwks_uri(uri: str | None) -> bool:\n    return bool(uri) and urlparse(uri).scheme in (\"https\", \"http\") and is_public_jwks_uri(uri)","tryCatchPattern":"from fastmcp.server.auth.providers.jwt import JWTVerifier\n\ntry:\n    claims = await verifier.verify_token(token)\nexcept ValueError as e:\n    if \"Failed to fetch JWKS\" in str(e):\n        logger.warning(\"JWKS fetch blocked (SSRF or network): %s\", e)\n        return None  # reject token, fail closed","preventionTips":["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."],"tags":["network","ssrf","jwt","jwks"],"backgroundTag":"jwks-fetch-blocked-ssrf","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}