RemoteAuthProvider instead.
MCP clients expect to register automatically and obtain credentials on the fly, but traditional providers require manual app registration through their developer consoles. The OAuth proxy bridges this gap by presenting a DCR-compliant interface to MCP clients while using your pre-registered credentials with the upstream provider. When a client attempts to register, the proxy returns your fixed credentials. When a client initiates authorization, the proxy handles the complexity of callback forwarding—storing the client’s dynamic callback URL, using its own fixed callback with the provider, then forwarding back to the client after token exchange.
This approach enables any MCP client (whether using random localhost ports or fixed URLs like Claude.ai) to authenticate with any traditional OAuth provider, all while maintaining full OAuth 2.1 and PKCE security.
For providers that support OIDC discovery (Auth0, Google with OIDC
configuration, Azure AD), consider using
OIDC Proxy for automatic configuration. OIDC Proxy
extends the OAuth proxy to automatically discover endpoints from the provider’s
/.well-known/openid-configuration URL, simplifying setup.Implementation
Provider Setup Requirements
Before using the OAuth proxy, you need to register your application with your OAuth provider:- Register your application in the provider’s developer console (GitHub Settings, Google Cloud Console, Azure Portal, etc.)
- Configure the redirect URI as your FastMCP server URL plus your chosen callback path:
- Default:
https://your-server.com/auth/callback - Custom:
https://your-server.com/your/custom/path(if you setredirect_path) - Development:
http://localhost:8000/auth/callback
- Default:
- Obtain your credentials: Client ID and Client Secret
- Note the OAuth endpoints: Authorization URL and Token URL (usually found in the provider’s OAuth documentation)
Basic Setup
Here’s how to implement the OAuth proxy with any provider:Configuration Parameters
OAuthProxy Parameters
str
required
URL of your OAuth provider’s authorization endpoint (e.g.,
https://github.com/login/oauth/authorize)str
required
URL of your OAuth provider’s token endpoint (e.g.,
https://github.com/login/oauth/access_token)str
required
Client ID from your registered OAuth application
str | None
Client secret from your registered OAuth application. Optional for PKCE public
clients or when using alternative credentials (e.g., managed identity client
assertions via a subclass). When omitted,
jwt_signing_key must be provided
explicitly since it cannot be derived from the secret.TokenVerifier
required
A
TokenVerifier instance to validate the
provider’s tokensAnyHttpUrl | str
required
Public URL where OAuth endpoints will be accessible, including any mount path (e.g.,
https://your-server.com/api).This URL is used to construct OAuth callback URLs and operational endpoints. When mounting under a path prefix, include that prefix in base_url. Use issuer_url separately to give the server an OAuth identity that differs from where its endpoints are mounted (typically the root level).AnyHttpUrl | str | None
Optional public base URL for the protected resource metadata and token audience.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.str
default:"/auth/callback"
Path for OAuth callbacks. Must match the redirect URI configured in your OAuth
application
str | None
Optional URL of provider’s token revocation endpoint
AnyHttpUrl | str | None
Issuer URL for OAuth authorization server metadata (defaults to When to set explicitly:
Set See the HTTP Deployment guide for complete mounting examples.
base_url).issuer_url is the server’s OAuth identity: it is the issuer field of the authorization server metadata, the iss claim of the tokens the proxy mints, and the RFC 9207 iss parameter on authorization responses. base_url remains the location of the endpoints, so authorization_endpoint, token_endpoint, and the rest of the metadata still point at base_url where the routes are actually mounted.When issuer_url has a path component (either explicitly or by defaulting from base_url), FastMCP creates path-aware discovery routes per RFC 8414. For example, if base_url is http://localhost:8000/api, the authorization server metadata will be at /.well-known/oauth-authorization-server/api.Default behavior (recommended for most cases):issuer_url to root level only if you want multiple MCP servers to share a single discovery endpoint:AnyHttpUrl | str | None
Optional URL to your service documentation
bool
default:"True"
Whether to forward PKCE (Proof Key for Code Exchange) to the upstream OAuth
provider. When enabled and the client uses PKCE, the proxy generates its own
PKCE parameters to send upstream while separately validating the client’s
PKCE. This ensures end-to-end PKCE security at both layers (client-to-proxy
and proxy-to-upstream). -
True (default): Forward PKCE for providers that
support it (Google, Azure, AWS, GitHub, etc.) - False: Disable only if upstream
provider doesn’t support PKCEbool
default:"True"
Whether to forward RFC 8707
resource parameters from MCP clients to the
upstream OAuth provider. When enabled, the proxy includes the resource indicator
in authorization requests, allowing providers that support RFC 8707 to scope
tokens to specific resources. Disable for providers that reject unknown
parameters.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.list[str] | None
List of allowed redirect URI patterns for MCP clients. Patterns support
wildcards (e.g.,
"http://localhost:*", "https://*.example.com/*").None(default): DCR clients use registered redirect URIs, with loopback ports allowed to vary for MCP compatibility. Unsafe browser schemes such asjavascript:,data:,file:, andvbscript:are rejected.- Empty list
[]: No redirect URIs allowed - Custom list: Only matching patterns allowed
redirect_path.list[str] | None
The complete set of scopes clients are allowed to request — the full set of
available scopes (a superset of
required_scopes). These are advertised to
clients through the /.well-known endpoints and enforced at Dynamic Client
Registration. Defaults to required_scopes from your TokenVerifier if not
specified.dict[str, str] | None
Additional parameters to forward to the upstream authorization endpoint. Useful for provider-specific parameters that aren’t part of the standard OAuth2 flow.For example, Auth0 requires an These parameters are added to every authorization request sent to the upstream provider.
audience parameter to issue JWT tokens:dict[str, str] | None
Additional parameters to forward to the upstream token endpoint during code exchange and token refresh. Useful for provider-specific requirements during token operations.For example, some providers require additional context during token exchange:These parameters are included in all token requests to the upstream provider.
AsyncKeyValue | None
Storage backend for persisting OAuth client registrations and upstream tokens.Default behavior:
By default, clients are automatically persisted to an encrypted disk store, 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. The disk store is encrypted using a key derived from the JWT Signing Key (which is derived from the upstream client secret by default). For client registrations to survive upstream client secret rotation, you should provide a JWT Signing Key or your own client_storage.For production deployments with multiple servers or cloud deployments, see Storage Backends for available options.Testing with in-memory storage (unencrypted):
str | bytes | None
Secret used to sign FastMCP JWT tokens issued to clients. How the key is derived depends on what you pass:See HTTP Deployment - OAuth Token Security for complete production setup.
bytesare used as-is, with no stretching, so supply at least 32 bytes of high-entropy key material. With the default file-backed client storage, the bytes must also decode as UTF-8; usesecrets.token_urlsafe(32).encode()instead of rawsecrets.token_bytes(), or configureclient_storageexplicitly.- A string is stretched into a 32-byte key with PBKDF2 (1,000,000 iterations), since a supplied string may be low-entropy. Strings shorter than 12 characters also log a warning.
None(the default) derives a 32-byte key from the upstream client secret using HKDF.
bool | Literal["remember", "external"]
default:"True"
Consent screen behavior for authorization requests. The consent page displays which client is requesting access, defending against confused deputy and AS-in-the-middle attacks by requiring explicit user approval.
True (default) — always prompt:
Users see the consent screen on every authorization. Strongest protection against AS-in-the-middle attacks where a malicious MCP server redirects the victim’s browser into a legitimate proxy and relies on a previously-remembered approval to silently complete the flow."remember" — silent consent on return:
Users see the consent screen on first authorization; subsequent flows from the same browser for the same (client_id, redirect_uri) are silently approved via a signed cookie. Cross-site navigations (detected via Sec-Fetch-Site) fall back to the prompt. Sec-Fetch-Site is a browser-level heuristic rather than a protocol guarantee: an attacker who finds a way to initiate a non-cross-site navigation (XSS on a sibling origin, a same-site redirect chain, etc.) can reach the silent-consent path. True does not depend on this signal. See Confused Deputy Attacks for the underlying attack class."external" — externally managed:
Follows the same authorization path as False: FastMCP skips its consent page and associated browser-binding protections, then redirects directly to the upstream provider. The difference is logging. False emits a security warning, while "external" suppresses that warning as an explicit acknowledgment that the operator is enforcing equivalent consent and transaction-binding protections elsewhere. FastMCP does not provide or verify those external protections.Ordinary upstream OAuth consent is generally not equivalent. It typically authorizes FastMCP’s shared upstream application without identifying the downstream MCP client or binding approval to that client’s transaction. Use "external" only when your surrounding authorization system supplies those protections.False — disable entirely:
Authorization proceeds directly to the upstream provider without any consent UI. Logs a security warning. Only for local development or testing.str | None
default:"None"
Content Security Policy for the consent page.
None(default): Uses the built-in CSP policy with appropriate directives for form submission- Empty string
"": Disables CSP entirely (no meta tag rendered) - Custom string: Uses the provided value as the CSP policy
Using Built-in Providers
FastMCP includes pre-configured providers for common services:GitHubProvider, GoogleProvider, and others. These handle token verification automatically.
Token Verification
The OAuth proxy requires a compatibleTokenVerifier to validate tokens from your provider. Different providers use different token formats:
- JWT tokens (Google, Azure): Use
JWTVerifierwith the provider’s JWKS endpoint - Opaque tokens with RFC 7662 introspection (Auth0, Okta, WorkOS): Use
IntrospectionTokenVerifier - Opaque tokens (provider-specific) (GitHub, Discord): Use provider-specific verifiers like
GitHubTokenVerifier
Scope Configuration
OAuth scopes control what permissions your application requests from users. They’re configured through yourTokenVerifier (required for the OAuth proxy to validate tokens from your provider). Set required_scopes to automatically request the permissions your application needs:
Custom Parameters
Some OAuth providers require additional parameters beyond the standard OAuth2 flow. Useextra_authorize_params and extra_token_params to pass provider-specific requirements. For example, Auth0 requires an audience parameter to issue JWT tokens instead of opaque tokens:
resource parameters from MCP clients to upstream providers that support them. This is enabled by default via the forward_resource parameter. Disable it for providers that reject unknown parameters.
OAuth Flow
The flow diagram above illustrates the complete OAuth proxy pattern. Let’s understand each phase:Registration Phase
When an MCP client calls/register with its dynamic callback URL, the proxy responds with your pre-configured upstream credentials. The client stores these credentials believing it has registered a new app. Meanwhile, the proxy records the client’s callback URL for later use.
Authorization Phase
The client initiates OAuth by redirecting to the proxy’s/authorize endpoint. The proxy:
- Stores the client’s transaction with its PKCE challenge
- Generates its own PKCE parameters for upstream security
- Shows the user a consent page with the client’s details, redirect URI, and requested scopes
- If the user approves (or the client was previously approved), sets a consent binding cookie and redirects to the upstream provider using the fixed callback URL
Callback Phase
After user authorization, the provider redirects back to the proxy’s fixed callback URL. The proxy:- Verifies the consent binding cookie matches the transaction (rejecting requests from a different browser)
- Exchanges the authorization code for tokens with the provider
- Stores these tokens temporarily
- Generates a new authorization code for the client
- Redirects to the client’s original dynamic callback URL
Token Exchange Phase
Finally, the client exchanges its authorization code with the proxy. The proxy validates the client’s PKCE verifier, then issues its own FastMCP JWT tokens (rather than forwarding the upstream provider’s tokens). See Token Architecture for details on this design. This entire flow is transparent to the MCP client—it experiences a standard OAuth flow with dynamic registration, unaware that a proxy is managing the complexity behind the scenes.Token Architecture
The OAuth proxy implements a token factory pattern: instead of directly forwarding tokens from the upstream OAuth provider, it issues its own JWT tokens to MCP clients. This maintains proper OAuth 2.0 token audience boundaries and enables better security controls. How it works: When an MCP client completes authorization, the proxy:- Receives upstream tokens from the OAuth provider (GitHub, Google, etc.)
- Encrypts and stores these tokens using Fernet encryption (AES-128-CBC + HMAC-SHA256)
- Issues FastMCP JWT tokens to the client, signed with HS256
- FastMCP validates the JWT signature, expiration, issuer, and audience
- Looks up the upstream token using the JTI from the validated JWT
- Decrypts and validates the upstream token with the provider
mcp-remote (used by Claude Desktop) has known issues handling access-token expiry, so a short upstream lifetime can push users through a full OAuth flow after every idle period. Set fastmcp_access_token_expiry_seconds to decouple the FastMCP token lifetime from the upstream expires_in:
GitHubProvider, GoogleProvider, AzureProvider, and the rest).
Extending the lifetime only works when the upstream provider issues a refresh token, since that’s what lets the proxy renew the access token behind the scenes. When the upstream provides no refresh token, the FastMCP token lifetime is capped at the upstream expires_in — issuing a longer-lived token would claim a validity the proxy can’t honor.
Refresh tokens:
The proxy issues its own refresh tokens that map to upstream refresh tokens. When a client uses a FastMCP refresh token, the proxy refreshes the upstream token and issues a new FastMCP access token.
PKCE Forwarding
The OAuth proxy automatically handles PKCE (Proof Key for Code Exchange) when working with providers that support or require it. The proxy generates its own PKCE parameters to send upstream while separately validating the client’s PKCE, ensuring end-to-end security at both layers. This is enabled by default via theforward_pkce parameter and works seamlessly with providers like Google, Azure AD, and GitHub. Only disable it for legacy providers that don’t support PKCE:
Redirect URI Validation
By default, the OAuth proxy validates DCR clients against their registered redirect URIs while allowing loopback ports to vary for MCP compatibility. Unsafe browser schemes such asjavascript: are always rejected. You can restrict which clients can connect at the server level by specifying allowed patterns:
Application Type (Web vs. Native)
During Dynamic Client Registration, a client may declare anapplication_type (per RFC 7591 and SEP-837) that governs which redirect URIs it is allowed to use. The OAuth proxy honors this field both at registration and when authorizing a redirect.
application_type defaults to "native" because MCP clients typically run locally and register loopback callbacks. Clients that omit the field keep the permissive behavior described above. A client that explicitly registers as "web" is held to the stricter browser-app rules.
Loopback covers the whole reserved range in both the address and name forms: every address in 127.0.0.0/8, ::1, and — per RFC 6761 — the name localhost along with any subdomain of it, such as app.localhost. The absolute (trailing-dot) spellings localhost. and 127.0.0.1. are treated identically. A name that merely contains localhost as a label of a registrable domain, like localhost.example.com, is an ordinary public host and is not treated as loopback.
Web clients must register a non-loopback
https callback — that is the restriction SEP-837 asks for, and a web client that registers no redirect URI at all is refused, since it could never complete an authorization. Native clients keep the full range of schemes their platforms use; the only new limit is that cleartext http must target a loopback host, per RFC 8252 §7.3.
Both application types always reject unsafe browser schemes (javascript:, data:, file:, vbscript:). FastMCP does not otherwise filter a native client’s scheme: there is no reliable way to tell an app-dispatch scheme from a network transport, since the IANA registry lists vscode: alongside coap: and smb:, so any such filter would reject callbacks that real MCP clients depend on.
A redirect URI that violates the declared type is refused during registration with a RegistrationError (invalid_redirect_uri). For example, a "web" client that registers http://localhost:12345/callback is rejected, since web clients must use a non-loopback https callback. Configure remote, browser-based clients as application_type="web" and give them an https callback URL.
CIMD Support
The OAuth proxy supports Client ID Metadata Documents (CIMD), an alternative to Dynamic Client Registration where clients host a static JSON document at an HTTPS URL. Instead of registering dynamically, clients simply provide their CIMD URL as theirclient_id, and the server fetches and validates the metadata.
CIMD clients appear in the consent screen with a verified domain badge, giving users confidence about which application is requesting access. This provides stronger identity verification than DCR, where any client can claim any name.
How CIMD Works
When a client presents an HTTPS URL as itsclient_id (for example, https://myapp.example.com/oauth/client.json), the OAuth proxy recognizes it as a CIMD client and:
- Fetches the JSON document from that URL
- Validates that the document’s
client_idfield matches the URL - Extracts client metadata (name, redirect URIs, scopes, etc.)
- Stores the client persistently alongside DCR clients
- Shows the verified domain in the consent screen
CIMD Configuration
CIMD support is enabled by default forOAuthProxy.
CIMD Parameters
bool
default:"True"
Whether to accept CIMD URLs as client identifiers. When enabled, clients can use HTTPS URLs pointing to metadata documents as their
client_id instead of registering via DCR.Private Key JWT Authentication
CIMD clients can authenticate usingprivate_key_jwt instead of the default none authentication method. This provides cryptographic proof of client identity by signing JWT assertions with a private key, while the server verifies using the client’s public key from their CIMD document.
To use private_key_jwt, the CIMD document must include either a jwks_uri (URL to fetch the public key set) or inline jwks (the key set directly in the document):
Security Considerations
CIMD provides several security advantages over DCR:- Verified identity: The domain in the
client_idURL is verified by HTTPS, so users know which organization is requesting access - No registration required: Clients don’t need to store or manage dynamically-issued credentials
- Redirect URI enforcement: CIMD documents must declare
redirect_uris, which are enforced by the proxy (wildcard patterns supported) - SSRF protection: The OAuth proxy blocks fetches to localhost, private IPs, and reserved addresses
- Replay prevention: For
private_key_jwtclients, JTI claims are tracked to prevent assertion replay - Cache-aware fetching: CIMD documents are cached according to HTTP cache headers and revalidated when required
enable_cimd=False explicitly:
Identity Assertion (SEP-990)
Identity assertion enables an enterprise “on-behalf-of” flow. A corporate identity provider (Okta, Microsoft Entra, etc.) issues an ID-JAG — a signed JWT that asserts an employee’s identity to a specific MCP authorization server. The client presents that ID-JAG at the token endpoint using the RFC 7523jwt-bearer grant, and the proxy validates it and mints a short-lived access token for the asserted user. No refresh token is issued: the identity provider controls session lifetime, and the client re-exchanges a fresh ID-JAG when its access token expires. This lets a workforce reach your MCP server with corporate-managed identity and centralized revocation, without each user running an interactive browser login.
To enable it, pass an IdentityAssertion configuration listing the issuers you trust:
urn:ietf:params:oauth:grant-type:jwt-bearer grant type and the urn:ietf:params:oauth:grant-profile:id-jag grant profile in its authorization server metadata, so compatible clients can discover the capability. When it is not configured, the grant is rejected as unsupported.
How Validation Works
For each ID-JAG presented at the token endpoint, the proxy checks that:- the JOSE header
typisoauth-id-jag+jwt; - the
issclaim is one of the configuredtrusted_issuers; - the signature verifies against the issuer’s published keys;
- the
audclaim identifies this authorization server — configure your identity provider to mint assertions whoseaudis theissuervalue published at/.well-known/oauth-authorization-server, which is yourissuer_urlwhen you set one and yourbase_urlotherwise; - the signed
client_idclaim matches the client presenting the assertion — an assertion the IdP minted for one client cannot be redeemed by another; - the signed
resourceclaim names this server — an assertion minted for a different MCP server behind the same IdP is rejected; exp(andiat/nbf, when present) place the assertion within a short lifetime and its validity window; and- the
jtihas not been seen before, preventing replay.
{issuer}/.well-known/openid-configuration). For issuers that do not publish a discovery document, provide the JWKS URI explicitly per issuer:
RS256 unless the issuer signs with another algorithm, in which case set algorithm explicitly (any asymmetric JWS algorithm — RS*, PS*, or ES* — since assertions are verified against a published JWKS, not a shared secret). When trusted issuers use different algorithms, override per issuer with algorithms, keyed the same way as jwks_uris:
get_access_token() exactly as they would for any other token, because the proxy issues the access token through its own token factory.
Security
Key and Storage Management
The OAuth proxy requires cryptographic keys for JWT signing and storage encryption, plus persistent storage to maintain valid tokens across server restarts. Default behavior (appropriate for development only): On every platform, FastMCP deterministically derivesjwt_signing_key from upstream_client_secret using HKDF, and storage defaults to an encrypted disk store in your platform’s data directory (derived from platformdirs). Tokens survive server restarts as long as upstream_client_secret doesn’t change. This is only suitable for development and local testing.
For production:
Configure the following parameters together: provide a unique jwt_signing_key (for signing FastMCP JWTs), and a shared client_storage backend (for storing tokens). Both are required for production deployments. Use a network-accessible storage backend like Redis or DynamoDB rather than local disk storage. Wrap your storage in FernetEncryptionWrapper to encrypt sensitive OAuth tokens at rest (see the client_storage parameter documentation above for examples). The keys accept any secret string and derive proper cryptographic keys using HKDF. See OAuth Token Security and Storage Backends for complete production setup.
Confused Deputy Attacks
A confused deputy attack allows a malicious client to steal your authorization by tricking you into granting it access under your identity. The OAuth proxy works by bridging DCR clients to traditional auth providers, which means that multiple MCP clients connect through a single upstream OAuth application. An attacker can exploit this shared application by registering a malicious client with their own redirect URI, then sending you an authorization link. When you click it, your browser goes through the OAuth flow—but since you may have already authorized this OAuth app before, the provider might auto-approve the request. The authorization code then gets sent to the attacker’s redirect URI instead of a legitimate client, giving them access under your credentials.Mitigation
FastMCP’s OAuth proxy defends against confused deputy attacks with two layers of protection: Consent screen. Before any authorization happens, you see a consent page showing the client’s details, redirect URI, and requested scopes. This gives you the opportunity to review and deny suspicious requests. By default (require_authorization_consent=True), the page is shown on every flow, which is the strongest protection. Setting require_authorization_consent="remember" approves previously-approved (client_id, redirect_uri) pairs silently on return visits, trading some protection for UX (see below). The consent mechanism is implemented with CSRF tokens and cryptographically signed cookies to prevent tampering.

AS-in-the-middle variant
A related attack works even with browser-session binding in place: a malicious MCP server advertises its own authorization server, which redirects the victim’s browser into the legitimate proxy’s/authorize endpoint. Because the victim’s browser carries both the prior-approval cookie and the newly-issued session-binding cookie throughout, both layers pass. The defense is the consent prompt itself: if consent is shown (require_authorization_consent=True), the victim sees the benign MCP server’s name on the consent page — which doesn’t match the malicious server they thought they were connecting to — and can deny.
require_authorization_consent="remember" adds a Sec-Fetch-Site check to keep this path safe for legitimate return flows (the attack navigation lands as cross-site and falls back to the prompt), but this is a browser-level heuristic. For the strongest defense, leave require_authorization_consent=True.
Learn more:
- MCP Security Best Practices - Official specification guidance
- Confused Deputy Attacks Explained - Detailed walkthrough by Den Delimarsky
Token Passthrough
Token passthrough occurs when an intermediary exposes upstream tokens to downstream clients, allowing those clients to impersonate the intermediary or access services they shouldn’t reach.Client-facing mitigation
The OAuth proxy’s token factory architecture prevents this by design. MCP clients only ever receive FastMCP-issued JWTs — the upstream provider token is never sent to the client. A FastMCP JWT is scoped to your server and cannot be used to access the upstream provider directly, even if intercepted.Calling downstream services
When your MCP server needs to call other APIs on behalf of the authenticated user, avoid forwarding the upstream token directly — this reintroduces the token passthrough problem in the other direction. Instead, use a token exchange flow like OAuth 2.0 Token Exchange (RFC 8693) or your provider’s equivalent (such as Azure’s On-Behalf-Of flow) to obtain a new token scoped to the downstream service. The upstream token is available in your tool functions viaget_access_token() or the CurrentAccessToken dependency, which you can use as the assertion for a token exchange. The exchanged token will be scoped to the specific downstream service and identify your MCP server as the authorized intermediary, maintaining proper audience boundaries throughout the chain.

