From 730175910cb09ba3741b14a445891b47a327feaf Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Feb 2026 20:34:29 +0000 Subject: [PATCH] Raise error when http_client and ssrf_safe=True are both provided MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with Claude Code https://claude.ai/code/session_012QKWmKd21vypDmxWbwuE4e --- docs/servers/auth/token-verification.mdx | 4 ++++ src/fastmcp/server/auth/providers/jwt.py | 11 ++++++++-- .../server/auth/providers/test_http_client.py | 20 ++++++++----------- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/docs/servers/auth/token-verification.mdx b/docs/servers/auth/token-verification.mdx index 5925a2d43..a9146135f 100644 --- a/docs/servers/auth/token-verification.mdx +++ b/docs/servers/auth/token-verification.mdx @@ -364,6 +364,10 @@ verifier = JWTVerifier( ) ``` + +`JWTVerifier` does not support `http_client` when `ssrf_safe=True`. SSRF-safe mode requires a hardened transport that validates DNS resolution and connection targets, which cannot be guaranteed with a user-provided client. Attempting to use both will raise a `ValueError`. + + When you provide an `http_client`, you are responsible for its lifecycle. The verifier will not close it. Use the server's `lifespan` to manage client cleanup: diff --git a/src/fastmcp/server/auth/providers/jwt.py b/src/fastmcp/server/auth/providers/jwt.py index 90e1d608f..783499faa 100644 --- a/src/fastmcp/server/auth/providers/jwt.py +++ b/src/fastmcp/server/auth/providers/jwt.py @@ -189,10 +189,11 @@ class JWTVerifier(TokenVerifier): http_client: Optional httpx.AsyncClient for connection pooling. When provided, the client is reused for JWKS fetches and the caller is responsible for its lifecycle. When None (default), a fresh client is created per fetch. - Only used when ssrf_safe is False; SSRF-safe fetches use their own transport. + Cannot be used with ssrf_safe=True. Raises: - ValueError: If neither or both of `public_key` and `jwks_uri` are provided, or if `algorithm` is unsupported. + ValueError: If neither or both of `public_key` and `jwks_uri` are provided, + if `algorithm` is unsupported, or if `http_client` is provided with `ssrf_safe=True`. """ if not public_key and not jwks_uri: raise ValueError("Either public_key or jwks_uri must be provided") @@ -200,6 +201,12 @@ class JWTVerifier(TokenVerifier): if public_key and jwks_uri: raise ValueError("Provide either public_key or jwks_uri, not both") + if ssrf_safe and http_client is not None: + raise ValueError( + "http_client cannot be used with ssrf_safe=True; " + "SSRF-safe mode requires its own hardened transport" + ) + algorithm = algorithm or "RS256" if algorithm not in { "HS256", diff --git a/tests/server/auth/providers/test_http_client.py b/tests/server/auth/providers/test_http_client.py index 39c8d51b0..8beb443e2 100644 --- a/tests/server/auth/providers/test_http_client.py +++ b/tests/server/auth/providers/test_http_client.py @@ -180,21 +180,17 @@ class TestJWTVerifierHttpClient: assert result is not None assert not shared_client.is_closed - async def test_ssrf_safe_ignores_http_client( + def test_ssrf_safe_rejects_http_client( self, shared_client: httpx.AsyncClient, ): - """When ssrf_safe=True, the custom http_client should NOT be used.""" - verifier = JWTVerifier( - jwks_uri="https://auth.example.com/.well-known/jwks.json", - ssrf_safe=True, - http_client=shared_client, - ) - - # ssrf_safe uses ssrf_safe_fetch instead of httpx.AsyncClient - # The http_client is stored but not used in this code path - assert verifier._http_client is shared_client - assert verifier.ssrf_safe is True + """ssrf_safe=True and http_client cannot be used together.""" + with pytest.raises(ValueError, match="cannot be used with ssrf_safe=True"): + JWTVerifier( + jwks_uri="https://auth.example.com/.well-known/jwks.json", + ssrf_safe=True, + http_client=shared_client, + ) class TestGitHubHttpClient: