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: