Add http_client parameter to token verifiers for connection pooling

🤖 Generated with Claude Code

https://claude.ai/code/session_012QKWmKd21vypDmxWbwuE4e
This commit is contained in:
Claude 2026-02-25 17:55:08 +00:00 committed by Jeremiah Lowin
commit 1704ffe88f
9 changed files with 520 additions and 6 deletions

View file

@ -321,6 +321,77 @@ print(f"Test token: {test_token}")
This pattern enables comprehensive testing of JWT validation logic without depending on external token issuers. The generated tokens are cryptographically valid and will pass all standard JWT validation checks.
## HTTP Client Customization
<VersionBadge version="2.18.0" />
All token verifiers that make HTTP calls accept an optional `http_client` parameter. This lets you provide your own `httpx.AsyncClient` for connection pooling, custom TLS configuration, or proxy settings.
### Connection Pooling
By default, each token verification call creates a fresh HTTP client. Under high load, this means repeated TCP connections and TLS handshakes. Providing a shared client enables connection pooling across calls:
```python
import httpx
from fastmcp import FastMCP
from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier
# Create a shared client with connection pooling
http_client = httpx.AsyncClient(
timeout=10,
limits=httpx.Limits(max_connections=20, max_keepalive_connections=10),
)
verifier = IntrospectionTokenVerifier(
introspection_url="https://auth.yourcompany.com/oauth/introspect",
client_id="mcp-resource-server",
client_secret="your-client-secret",
http_client=http_client,
)
mcp = FastMCP(name="Protected API", auth=verifier)
```
The same pattern works for `JWTVerifier` when using JWKS endpoints:
```python
from fastmcp.server.auth.providers.jwt import JWTVerifier
verifier = JWTVerifier(
jwks_uri="https://auth.yourcompany.com/.well-known/jwks.json",
issuer="https://auth.yourcompany.com",
http_client=http_client,
)
```
<Note>
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:
```python
from contextlib import asynccontextmanager
from fastmcp import FastMCP
from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier
http_client = httpx.AsyncClient(timeout=10)
verifier = IntrospectionTokenVerifier(
introspection_url="https://auth.example.com/introspect",
client_id="my-service",
client_secret="secret",
http_client=http_client,
)
@asynccontextmanager
async def lifespan(app):
yield
await http_client.aclose()
mcp = FastMCP(name="My API", auth=verifier, lifespan=lifespan)
```
</Note>
The convenience providers (`GitHubProvider`, `GoogleProvider`, `DiscordProvider`, `WorkOSProvider`, `AzureProvider`) also accept `http_client` and pass it through to their internal token verifier.
## Production Configuration
For production deployments, load sensitive configuration from environment variables:

View file

@ -10,6 +10,7 @@ import hashlib
from collections import OrderedDict
from typing import TYPE_CHECKING, Any, cast
import httpx
from key_value.aio.protocols import AsyncKeyValue
from fastmcp.server.auth.oauth_proxy import OAuthProxy
@ -107,6 +108,7 @@ class AzureProvider(OAuthProxy):
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
base_authority: str = "login.microsoftonline.com",
http_client: httpx.AsyncClient | None = None,
) -> None:
"""Initialize Azure OAuth provider.
@ -151,6 +153,9 @@ class AzureProvider(OAuthProxy):
When True, users see a consent screen before being redirected to Azure.
When False, authorization proceeds directly without user confirmation.
SECURITY WARNING: Only disable for local development or testing environments.
http_client: Optional httpx.AsyncClient for connection pooling in JWKS fetches.
When provided, the client is reused for JWT key fetches and the caller
is responsible for its lifecycle. When None (default), a fresh client is created per fetch.
"""
# Parse scopes if provided as string
parsed_required_scopes = parse_scopes(required_scopes)
@ -202,6 +207,7 @@ class AzureProvider(OAuthProxy):
audience=client_id,
algorithm="RS256",
required_scopes=validation_scopes, # Only validate non-OIDC scopes
http_client=http_client,
)
# Build Azure OAuth endpoints with tenant

View file

@ -21,6 +21,7 @@ Example:
from __future__ import annotations
import contextlib
import time
from datetime import datetime
@ -49,20 +50,29 @@ class DiscordTokenVerifier(TokenVerifier):
*,
required_scopes: list[str] | None = None,
timeout_seconds: int = 10,
http_client: httpx.AsyncClient | None = None,
):
"""Initialize the Discord token verifier.
Args:
required_scopes: Required OAuth scopes (e.g., ['email'])
timeout_seconds: HTTP request timeout
http_client: Optional httpx.AsyncClient for connection pooling. When provided,
the client is reused across calls and the caller is responsible for its
lifecycle. When None (default), a fresh client is created per call.
"""
super().__init__(required_scopes=required_scopes)
self.timeout_seconds = timeout_seconds
self._http_client = http_client
async def verify_token(self, token: str) -> AccessToken | None:
"""Verify Discord OAuth token by calling Discord's tokeninfo API."""
try:
async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
async with (
contextlib.nullcontext(self._http_client)
if self._http_client is not None
else httpx.AsyncClient(timeout=self.timeout_seconds)
) as client:
# Use Discord's tokeninfo endpoint to validate the token
headers = {
"Authorization": f"Bearer {token}",
@ -183,6 +193,7 @@ class DiscordProvider(OAuthProxy):
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
http_client: httpx.AsyncClient | None = None,
):
"""Initialize Discord OAuth provider.
@ -210,6 +221,9 @@ class DiscordProvider(OAuthProxy):
When True, users see a consent screen before being redirected to Discord.
When False, authorization proceeds directly without user confirmation.
SECURITY WARNING: Only disable for local development or testing environments.
http_client: Optional httpx.AsyncClient for connection pooling in token verification.
When provided, the client is reused across verify_token calls and the caller
is responsible for its lifecycle. When None (default), a fresh client is created per call.
"""
# Parse scopes if provided as string
required_scopes_final = (
@ -222,6 +236,7 @@ class DiscordProvider(OAuthProxy):
token_verifier = DiscordTokenVerifier(
required_scopes=required_scopes_final,
timeout_seconds=timeout_seconds,
http_client=http_client,
)
# Initialize OAuth proxy with Discord endpoints

View file

@ -21,6 +21,8 @@ Example:
from __future__ import annotations
import contextlib
import httpx
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl
@ -46,20 +48,29 @@ class GitHubTokenVerifier(TokenVerifier):
*,
required_scopes: list[str] | None = None,
timeout_seconds: int = 10,
http_client: httpx.AsyncClient | None = None,
):
"""Initialize the GitHub token verifier.
Args:
required_scopes: Required OAuth scopes (e.g., ['user:email'])
timeout_seconds: HTTP request timeout
http_client: Optional httpx.AsyncClient for connection pooling. When provided,
the client is reused across calls and the caller is responsible for its
lifecycle. When None (default), a fresh client is created per call.
"""
super().__init__(required_scopes=required_scopes)
self.timeout_seconds = timeout_seconds
self._http_client = http_client
async def verify_token(self, token: str) -> AccessToken | None:
"""Verify GitHub OAuth token by calling GitHub API."""
try:
async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
async with (
contextlib.nullcontext(self._http_client)
if self._http_client is not None
else httpx.AsyncClient(timeout=self.timeout_seconds)
) as client:
# Get token info from GitHub API
response = await client.get(
"https://api.github.com/user",
@ -181,6 +192,7 @@ class GitHubProvider(OAuthProxy):
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
http_client: httpx.AsyncClient | None = None,
):
"""Initialize GitHub OAuth provider.
@ -205,6 +217,9 @@ class GitHubProvider(OAuthProxy):
When True, users see a consent screen before being redirected to GitHub.
When False, authorization proceeds directly without user confirmation.
SECURITY WARNING: Only disable for local development or testing environments.
http_client: Optional httpx.AsyncClient for connection pooling in token verification.
When provided, the client is reused across verify_token calls and the caller
is responsible for its lifecycle. When None (default), a fresh client is created per call.
"""
# Parse scopes if provided as string
required_scopes_final = (
@ -215,6 +230,7 @@ class GitHubProvider(OAuthProxy):
token_verifier = GitHubTokenVerifier(
required_scopes=required_scopes_final,
timeout_seconds=timeout_seconds,
http_client=http_client,
)
# Initialize OAuth proxy with GitHub endpoints

View file

@ -21,6 +21,7 @@ Example:
from __future__ import annotations
import contextlib
import time
import httpx
@ -48,20 +49,29 @@ class GoogleTokenVerifier(TokenVerifier):
*,
required_scopes: list[str] | None = None,
timeout_seconds: int = 10,
http_client: httpx.AsyncClient | None = None,
):
"""Initialize the Google token verifier.
Args:
required_scopes: Required OAuth scopes (e.g., ['openid', 'https://www.googleapis.com/auth/userinfo.email'])
timeout_seconds: HTTP request timeout
http_client: Optional httpx.AsyncClient for connection pooling. When provided,
the client is reused across calls and the caller is responsible for its
lifecycle. When None (default), a fresh client is created per call.
"""
super().__init__(required_scopes=required_scopes)
self.timeout_seconds = timeout_seconds
self._http_client = http_client
async def verify_token(self, token: str) -> AccessToken | None:
"""Verify Google OAuth token by calling Google's tokeninfo API."""
try:
async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
async with (
contextlib.nullcontext(self._http_client)
if self._http_client is not None
else httpx.AsyncClient(timeout=self.timeout_seconds)
) as client:
# Use Google's tokeninfo endpoint to validate the token
response = await client.get(
"https://www.googleapis.com/oauth2/v1/tokeninfo",
@ -198,6 +208,7 @@ class GoogleProvider(OAuthProxy):
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
extra_authorize_params: dict[str, str] | None = None,
http_client: httpx.AsyncClient | None = None,
):
"""Initialize Google OAuth provider.
@ -229,6 +240,9 @@ class GoogleProvider(OAuthProxy):
By default, GoogleProvider sets {"access_type": "offline", "prompt": "consent"} to ensure
refresh tokens are returned. You can override these defaults or add additional parameters.
Example: {"prompt": "select_account"} to let users choose their Google account.
http_client: Optional httpx.AsyncClient for connection pooling in token verification.
When provided, the client is reused across verify_token calls and the caller
is responsible for its lifecycle. When None (default), a fresh client is created per call.
"""
# Parse scopes if provided as string
# Google requires at least one scope - openid is the minimal OIDC scope
@ -240,6 +254,7 @@ class GoogleProvider(OAuthProxy):
token_verifier = GoogleTokenVerifier(
required_scopes=required_scopes_final,
timeout_seconds=timeout_seconds,
http_client=http_client,
)
# Set Google-specific defaults for extra authorize params

View file

@ -24,6 +24,7 @@ Example:
from __future__ import annotations
import base64
import contextlib
import time
from typing import Any, Literal, get_args
@ -80,6 +81,7 @@ class IntrospectionTokenVerifier(TokenVerifier):
timeout_seconds: int = 10,
required_scopes: list[str] | None = None,
base_url: AnyHttpUrl | str | None = None,
http_client: httpx.AsyncClient | None = None,
):
"""
Initialize the introspection token verifier.
@ -93,6 +95,9 @@ class IntrospectionTokenVerifier(TokenVerifier):
timeout_seconds: HTTP request timeout in seconds (default: 10)
required_scopes: Required scopes for all tokens (optional)
base_url: Base URL for TokenVerifier protocol
http_client: Optional httpx.AsyncClient for connection pooling. When provided,
the client is reused across calls and the caller is responsible for its
lifecycle. When None (default), a fresh client is created per call.
"""
# Parse scopes if provided as string
parsed_required_scopes = (
@ -120,6 +125,7 @@ class IntrospectionTokenVerifier(TokenVerifier):
self.client_auth_method: ClientAuthMethod = client_auth_method
self.timeout_seconds = timeout_seconds
self._http_client = http_client
self.logger = get_logger(__name__)
def _create_basic_auth_header(self) -> str:
@ -166,7 +172,11 @@ class IntrospectionTokenVerifier(TokenVerifier):
AccessToken object if valid and active, None if invalid, inactive, or expired
"""
try:
async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
async with (
contextlib.nullcontext(self._http_client)
if self._http_client is not None
else httpx.AsyncClient(timeout=self.timeout_seconds)
) as client:
# Prepare introspection request per RFC 7662
# Build request data with token and token_type_hint
data = {

View file

@ -2,6 +2,7 @@
from __future__ import annotations
import contextlib
import json
import time
from dataclasses import dataclass
@ -168,6 +169,7 @@ class JWTVerifier(TokenVerifier):
required_scopes: list[str] | None = None,
base_url: AnyHttpUrl | str | None = None,
ssrf_safe: bool = False,
http_client: httpx.AsyncClient | None = None,
):
"""
Initialize a JWTVerifier configured to validate JWTs using either a static key or a JWKS endpoint.
@ -184,6 +186,10 @@ class JWTVerifier(TokenVerifier):
public IPs, DNS pinning). Enable when the JWKS URI comes from
untrusted input (e.g. CIMD documents). Defaults to False so
operator-configured JWKS URIs (including localhost) work normally.
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.
Raises:
ValueError: If neither or both of `public_key` and `jwks_uri` are provided, or if `algorithm` is unsupported.
@ -228,6 +234,7 @@ class JWTVerifier(TokenVerifier):
self.public_key = public_key
self.jwks_uri = jwks_uri
self.ssrf_safe = ssrf_safe
self._http_client = http_client
self.jwt = JsonWebToken([self.algorithm])
self.logger = get_logger(__name__)
@ -328,7 +335,11 @@ class JWTVerifier(TokenVerifier):
)
return json.loads(content)
else:
async with httpx.AsyncClient(timeout=httpx.Timeout(10.0)) as client:
async with (
contextlib.nullcontext(self._http_client)
if self._http_client is not None
else httpx.AsyncClient(timeout=httpx.Timeout(10.0))
) as client:
response = await client.get(self.jwks_uri)
response.raise_for_status()
return response.json()

View file

@ -10,6 +10,8 @@ Choose based on your WorkOS setup and authentication requirements.
from __future__ import annotations
import contextlib
import httpx
from key_value.aio.protocols import AsyncKeyValue
from pydantic import AnyHttpUrl
@ -38,6 +40,7 @@ class WorkOSTokenVerifier(TokenVerifier):
authkit_domain: str,
required_scopes: list[str] | None = None,
timeout_seconds: int = 10,
http_client: httpx.AsyncClient | None = None,
):
"""Initialize the WorkOS token verifier.
@ -45,15 +48,23 @@ class WorkOSTokenVerifier(TokenVerifier):
authkit_domain: WorkOS AuthKit domain (e.g., "https://your-app.authkit.app")
required_scopes: Required OAuth scopes
timeout_seconds: HTTP request timeout
http_client: Optional httpx.AsyncClient for connection pooling. When provided,
the client is reused across calls and the caller is responsible for its
lifecycle. When None (default), a fresh client is created per call.
"""
super().__init__(required_scopes=required_scopes)
self.authkit_domain = authkit_domain.rstrip("/")
self.timeout_seconds = timeout_seconds
self._http_client = http_client
async def verify_token(self, token: str) -> AccessToken | None:
"""Verify WorkOS OAuth token by calling userinfo endpoint."""
try:
async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
async with (
contextlib.nullcontext(self._http_client)
if self._http_client is not None
else httpx.AsyncClient(timeout=self.timeout_seconds)
) as client:
# Use WorkOS AuthKit userinfo endpoint to validate token
response = await client.get(
f"{self.authkit_domain}/oauth2/userinfo",
@ -146,6 +157,7 @@ class WorkOSProvider(OAuthProxy):
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool = True,
http_client: httpx.AsyncClient | None = None,
):
"""Initialize WorkOS OAuth provider.
@ -171,6 +183,9 @@ class WorkOSProvider(OAuthProxy):
When True, users see a consent screen before being redirected to WorkOS.
When False, authorization proceeds directly without user confirmation.
SECURITY WARNING: Only disable for local development or testing environments.
http_client: Optional httpx.AsyncClient for connection pooling in token verification.
When provided, the client is reused across verify_token calls and the caller
is responsible for its lifecycle. When None (default), a fresh client is created per call.
"""
# Apply defaults and ensure authkit_domain is a full URL
authkit_domain_str = authkit_domain
@ -186,6 +201,7 @@ class WorkOSProvider(OAuthProxy):
authkit_domain=authkit_domain_final,
required_scopes=scopes_final,
timeout_seconds=timeout_seconds,
http_client=http_client,
)
# Initialize OAuth proxy with WorkOS AuthKit endpoints

View file

@ -0,0 +1,354 @@
"""Tests for http_client parameter on token verifiers.
Verifies that all token verifiers accept an optional httpx.AsyncClient for
connection pooling (issues #3287 and #3293).
"""
import time
import httpx
import pytest
from pytest_httpx import HTTPXMock
from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier
from fastmcp.server.auth.providers.jwt import JWTVerifier, RSAKeyPair
class TestIntrospectionHttpClient:
"""Test http_client parameter on IntrospectionTokenVerifier."""
@pytest.fixture
def shared_client(self) -> httpx.AsyncClient:
return httpx.AsyncClient(timeout=30)
def test_stores_http_client(self, shared_client: httpx.AsyncClient):
verifier = IntrospectionTokenVerifier(
introspection_url="https://auth.example.com/introspect",
client_id="test",
client_secret="secret",
http_client=shared_client,
)
assert verifier._http_client is shared_client
def test_default_http_client_is_none(self):
verifier = IntrospectionTokenVerifier(
introspection_url="https://auth.example.com/introspect",
client_id="test",
client_secret="secret",
)
assert verifier._http_client is None
async def test_uses_provided_client(
self, shared_client: httpx.AsyncClient, httpx_mock: HTTPXMock
):
"""When http_client is provided, it should be used for requests."""
httpx_mock.add_response(
url="https://auth.example.com/introspect",
method="POST",
json={
"active": True,
"client_id": "user-1",
"scope": "read",
"exp": int(time.time()) + 3600,
},
)
verifier = IntrospectionTokenVerifier(
introspection_url="https://auth.example.com/introspect",
client_id="test",
client_secret="secret",
http_client=shared_client,
)
result = await verifier.verify_token("tok")
assert result is not None
assert result.client_id == "user-1"
async def test_client_not_closed_after_call(
self, shared_client: httpx.AsyncClient, httpx_mock: HTTPXMock
):
"""User-provided client must not be closed by the verifier."""
httpx_mock.add_response(
url="https://auth.example.com/introspect",
method="POST",
json={
"active": True,
"client_id": "user-1",
"scope": "read",
"exp": int(time.time()) + 3600,
},
)
verifier = IntrospectionTokenVerifier(
introspection_url="https://auth.example.com/introspect",
client_id="test",
client_secret="secret",
http_client=shared_client,
)
await verifier.verify_token("tok")
# Client should still be open — not closed by the verifier
assert not shared_client.is_closed
async def test_reuses_client_across_calls(
self, shared_client: httpx.AsyncClient, httpx_mock: HTTPXMock
):
"""Same client instance should be reused across multiple verify_token calls."""
for _ in range(3):
httpx_mock.add_response(
url="https://auth.example.com/introspect",
method="POST",
json={
"active": True,
"client_id": "user-1",
"scope": "read",
"exp": int(time.time()) + 3600,
},
)
verifier = IntrospectionTokenVerifier(
introspection_url="https://auth.example.com/introspect",
client_id="test",
client_secret="secret",
http_client=shared_client,
)
for _ in range(3):
result = await verifier.verify_token("tok")
assert result is not None
assert not shared_client.is_closed
class TestJWTVerifierHttpClient:
"""Test http_client parameter on JWTVerifier."""
@pytest.fixture(scope="class")
def rsa_key_pair(self) -> RSAKeyPair:
return RSAKeyPair.generate()
@pytest.fixture
def shared_client(self) -> httpx.AsyncClient:
return httpx.AsyncClient(timeout=30)
def test_stores_http_client(self, shared_client: httpx.AsyncClient):
verifier = JWTVerifier(
jwks_uri="https://auth.example.com/.well-known/jwks.json",
http_client=shared_client,
)
assert verifier._http_client is shared_client
def test_default_http_client_is_none(self):
verifier = JWTVerifier(
jwks_uri="https://auth.example.com/.well-known/jwks.json",
)
assert verifier._http_client is None
async def test_jwks_fetch_uses_provided_client(
self,
rsa_key_pair: RSAKeyPair,
shared_client: httpx.AsyncClient,
httpx_mock: HTTPXMock,
):
"""When http_client is provided, JWKS fetches should use it."""
from authlib.jose import JsonWebKey
# Build a JWKS response from the RSA key pair
public_key_obj = JsonWebKey.import_key(rsa_key_pair.public_key)
jwk_dict = dict(public_key_obj.as_dict())
jwk_dict["kid"] = "test-key-1"
jwk_dict["use"] = "sig"
jwk_dict["alg"] = "RS256"
httpx_mock.add_response(
url="https://auth.example.com/.well-known/jwks.json",
json={"keys": [jwk_dict]},
)
verifier = JWTVerifier(
jwks_uri="https://auth.example.com/.well-known/jwks.json",
issuer="https://auth.example.com",
http_client=shared_client,
)
token = rsa_key_pair.create_token(
issuer="https://auth.example.com",
kid="test-key-1",
)
result = await verifier.verify_token(token)
assert result is not None
assert not shared_client.is_closed
async def test_ssrf_safe_ignores_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
class TestGitHubHttpClient:
"""Test http_client parameter on GitHubTokenVerifier."""
def test_stores_http_client(self):
from fastmcp.server.auth.providers.github import GitHubTokenVerifier
client = httpx.AsyncClient()
verifier = GitHubTokenVerifier(http_client=client)
assert verifier._http_client is client
async def test_uses_provided_client(self, httpx_mock: HTTPXMock):
from fastmcp.server.auth.providers.github import GitHubTokenVerifier
client = httpx.AsyncClient()
httpx_mock.add_response(
url="https://api.github.com/user",
json={"id": 123, "login": "testuser"},
)
httpx_mock.add_response(
url="https://api.github.com/user/repos",
headers={"x-oauth-scopes": "user,repo"},
json=[],
)
verifier = GitHubTokenVerifier(http_client=client)
result = await verifier.verify_token("ghp_test")
assert result is not None
assert not client.is_closed
class TestDiscordHttpClient:
"""Test http_client parameter on DiscordTokenVerifier."""
def test_stores_http_client(self):
from fastmcp.server.auth.providers.discord import DiscordTokenVerifier
client = httpx.AsyncClient()
verifier = DiscordTokenVerifier(http_client=client)
assert verifier._http_client is client
class TestGoogleHttpClient:
"""Test http_client parameter on GoogleTokenVerifier."""
def test_stores_http_client(self):
from fastmcp.server.auth.providers.google import GoogleTokenVerifier
client = httpx.AsyncClient()
verifier = GoogleTokenVerifier(http_client=client)
assert verifier._http_client is client
class TestWorkOSHttpClient:
"""Test http_client parameter on WorkOSTokenVerifier."""
def test_stores_http_client(self):
from fastmcp.server.auth.providers.workos import WorkOSTokenVerifier
client = httpx.AsyncClient()
verifier = WorkOSTokenVerifier(
authkit_domain="https://test.authkit.app",
http_client=client,
)
assert verifier._http_client is client
class TestProviderHttpClientPassthrough:
"""Test that convenience providers pass http_client to their verifiers."""
def test_github_provider_threads_http_client(self):
from fastmcp.server.auth.providers.github import (
GitHubProvider,
GitHubTokenVerifier,
)
client = httpx.AsyncClient()
provider = GitHubProvider(
client_id="test",
client_secret="secret",
base_url="https://example.com",
http_client=client,
)
# OAuthProxy stores token verifier as _token_validator
verifier = provider._token_validator
assert isinstance(verifier, GitHubTokenVerifier)
assert verifier._http_client is client
def test_discord_provider_threads_http_client(self):
from fastmcp.server.auth.providers.discord import (
DiscordProvider,
DiscordTokenVerifier,
)
client = httpx.AsyncClient()
provider = DiscordProvider(
client_id="test",
client_secret="secret",
base_url="https://example.com",
http_client=client,
)
verifier = provider._token_validator
assert isinstance(verifier, DiscordTokenVerifier)
assert verifier._http_client is client
def test_google_provider_threads_http_client(self):
from fastmcp.server.auth.providers.google import (
GoogleProvider,
GoogleTokenVerifier,
)
client = httpx.AsyncClient()
provider = GoogleProvider(
client_id="test",
client_secret="secret",
base_url="https://example.com",
http_client=client,
)
verifier = provider._token_validator
assert isinstance(verifier, GoogleTokenVerifier)
assert verifier._http_client is client
def test_workos_provider_threads_http_client(self):
from fastmcp.server.auth.providers.workos import (
WorkOSProvider,
WorkOSTokenVerifier,
)
client = httpx.AsyncClient()
provider = WorkOSProvider(
client_id="test",
client_secret="secret",
authkit_domain="https://test.authkit.app",
base_url="https://example.com",
http_client=client,
)
verifier = provider._token_validator
assert isinstance(verifier, WorkOSTokenVerifier)
assert verifier._http_client is client
def test_azure_provider_threads_http_client(self):
from fastmcp.server.auth.providers.azure import AzureProvider
from fastmcp.server.auth.providers.jwt import JWTVerifier
client = httpx.AsyncClient()
provider = AzureProvider(
client_id="test-client-id",
client_secret="secret",
tenant_id="test-tenant-id",
required_scopes=["read"],
base_url="https://example.com",
http_client=client,
)
verifier = provider._token_validator
assert isinstance(verifier, JWTVerifier)
assert verifier._http_client is client