docs/servers/auth/oidc-proxy.mdx
import { VersionBadge } from "/snippets/version-badge.mdx";
<VersionBadge version="2.12.4" />The OIDC proxy enables FastMCP servers to authenticate with OIDC providers that don't support Dynamic Client Registration (DCR) out of the box. This includes OAuth providers like: Auth0, Google, Azure, AWS, etc. For providers that do support DCR (like WorkOS AuthKit), use RemoteAuthProvider instead.
The OIDC proxy is built upon OAuthProxy so it has all the same functionality under the covers.
Before using the OIDC proxy, you need to register your application with your OAuth provider:
https://your-server.com/auth/callbackhttps://your-server.com/your/custom/path (if you set redirect_path)http://localhost:8000/auth/callbackHere's how to implement the OIDC proxy with any provider:
from fastmcp import FastMCP
from fastmcp.server.auth.oidc_proxy import OIDCProxy
# Create the OIDC proxy
auth = OIDCProxy(
# Provider's configuration URL
config_url="https://provider.com/.well-known/openid-configuration",
# Your registered app credentials
client_id="your-client-id",
client_secret="your-client-secret",
# Your FastMCP server's public URL
base_url="https://your-server.com",
# Optional: customize the callback path (default is "/auth/callback")
# redirect_path="/custom/callback",
)
mcp = FastMCP(name="My Server", auth=auth)
Use this when your OAuth callbacks and operational endpoints need to live under one public URL, but the protected MCP resource should be advertised under another. FastMCP will still append the MCP mount path (for example, /mcp) to this base URL.
</ParamField>
Cannot be used with algorithm or required_scopes parameters - configure these on your verifier instead. The verifier's required_scopes are automatically loaded and advertised.
</ParamField>
These patterns apply to MCP client loopback redirects, NOT the upstream OAuth app redirect URI.
</ParamField> <ParamField body="token_endpoint_auth_method" type="str | None"> Token endpoint authentication method for the upstream OAuth server. Controls how the proxy authenticates when exchanging authorization codes and refresh tokens with the upstream provider. - `"client_secret_basic"`: Send credentials in Authorization header (most common) - `"client_secret_post"`: Send credentials in request body (required by some providers) - `"none"`: No authentication (for public clients) - `None` (default): Uses authlib's default (typically `"client_secret_basic"`)Set this if your provider requires a specific authentication method and the default doesn't work.
</ParamField> <ParamField body="jwt_signing_key" type="str | bytes | None"> <VersionBadge version="2.13.0" /> Secret used to sign FastMCP JWT tokens issued to clients. Accepts any string or bytes - will be derived into a proper 32-byte cryptographic key using HKDF.Default behavior (None):
For production: Provide an explicit secret (e.g., from environment variable) to use a fixed key instead of the auto-generated one. </ParamField>
<ParamField body="client_storage" type="AsyncKeyValue | None"> <VersionBadge version="2.13.0" /> Storage backend for persisting OAuth client registrations and upstream tokens.Default behavior:
platformdirs)By default on Mac/Windows, clients are automatically persisted to encrypted disk storage, allowing them to survive server restarts as long as the filesystem remains accessible. This means MCP clients only need to register once and can reconnect seamlessly. On Linux where keyring isn't available, ephemeral storage is used to match the ephemeral key strategy.
For production deployments with multiple servers or cloud deployments, use a network-accessible storage backend rather than local disk storage. Wrap your storage in FernetEncryptionWrapper to encrypt sensitive OAuth tokens at rest. See Storage Backends for available options.
Testing with in-memory storage (unencrypted):
from key_value.aio.stores.memory import MemoryStore
# Use in-memory storage for testing (clients lost on restart)
auth = OIDCProxy(..., client_storage=MemoryStore())
Production with encrypted Redis storage:
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
import os
auth = OIDCProxy(
...,
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
client_storage=FernetEncryptionWrapper(
key_value=RedisStore(host="redis.example.com", port=6379),
fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
)
)
None (default): Uses the built-in CSP policy with appropriate directives for form submission"": Disables CSP entirely (no meta tag rendered)This is useful for organizations that have their own CSP policies and need to override or disable FastMCP's built-in CSP directives. </ParamField> </Card>
FastMCP includes pre-configured OIDC providers for common services:
from fastmcp.server.auth.providers.auth0 import Auth0Provider
auth = Auth0Provider(
config_url="https://.../.well-known/openid-configuration",
client_id="your-auth0-client-id",
client_secret="your-auth0-client-secret",
audience="https://...",
base_url="https://localhost:8000"
)
mcp = FastMCP(name="My Server", auth=auth)
Available providers include Auth0Provider at present.
OAuth scopes are configured with required_scopes to automatically request the permissions your application needs.
Dynamic clients created by the proxy will automatically include these scopes in their authorization requests.
The OIDC proxy inherits full CIMD (Client ID Metadata Document) support from OAuthProxy. Clients can use HTTPS URLs as their client_id instead of registering dynamically, and the proxy will fetch and validate their metadata document.
See the OAuth Proxy CIMD documentation for complete details on how CIMD works, including private key JWT authentication and security considerations.
The CIMD-related parameters available on OIDCProxy are:
For production deployments, load sensitive credentials from environment variables:
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.auth0 import Auth0Provider
# Load secrets from environment variables
auth = Auth0Provider(
config_url=os.environ.get("AUTH0_CONFIG_URL"),
client_id=os.environ.get("AUTH0_CLIENT_ID"),
client_secret=os.environ.get("AUTH0_CLIENT_SECRET"),
audience=os.environ.get("AUTH0_AUDIENCE"),
base_url=os.environ.get("BASE_URL", "https://localhost:8000")
)
mcp = FastMCP(name="My Server", auth=auth)
@mcp.tool
def protected_tool(data: str) -> str:
"""This tool is now protected by OAuth."""
return f"Processed: {data}"
if __name__ == "__main__":
mcp.run(transport="http", port=8000)
This keeps secrets out of your codebase while maintaining explicit configuration.