mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
409 lines
17 KiB
Text
409 lines
17 KiB
Text
---
|
|
title: oauth_proxy
|
|
sidebarTitle: oauth_proxy
|
|
---
|
|
|
|
# `fastmcp.server.auth.oauth_proxy`
|
|
|
|
|
|
OAuth Proxy Provider for FastMCP.
|
|
|
|
This provider acts as a transparent proxy to an upstream OAuth Authorization Server,
|
|
handling Dynamic Client Registration locally while forwarding all other OAuth flows.
|
|
This enables authentication with upstream providers that don't support DCR or have
|
|
restricted client registration policies.
|
|
|
|
Key features:
|
|
- Proxies authorization and token endpoints to upstream server
|
|
- Implements local Dynamic Client Registration with fixed upstream credentials
|
|
- Validates tokens using upstream JWKS
|
|
- Maintains minimal local state for bookkeeping
|
|
- Enhanced logging with request correlation
|
|
|
|
This implementation is based on the OAuth 2.1 specification and is designed for
|
|
production use with enterprise identity providers.
|
|
|
|
|
|
## Functions
|
|
|
|
### `create_consent_html` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L236" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
```python
|
|
create_consent_html(client_id: str, redirect_uri: str, scopes: list[str], txn_id: str, csrf_token: str, client_name: str | None = None, title: str = 'Application Access Request', server_name: str | None = None, server_icon_url: str | None = None, server_website_url: str | None = None, client_website_url: str | None = None) -> str
|
|
```
|
|
|
|
|
|
Create a styled HTML consent page for OAuth authorization requests.
|
|
|
|
|
|
## Classes
|
|
|
|
### `OAuthTransaction` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L108" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
|
|
OAuth transaction state for consent flow.
|
|
|
|
Stored server-side to track active authorization flows with client context.
|
|
Includes CSRF tokens for consent protection per MCP security best practices.
|
|
|
|
|
|
### `ClientCode` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L129" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
|
|
Client authorization code with PKCE and upstream tokens.
|
|
|
|
Stored server-side after upstream IdP callback. Contains the upstream
|
|
tokens bound to the client's PKCE challenge for secure token exchange.
|
|
|
|
|
|
### `UpstreamTokenSet` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L147" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
|
|
Stored upstream OAuth tokens from identity provider.
|
|
|
|
These tokens are obtained from the upstream provider (Google, GitHub, etc.)
|
|
and stored in plaintext within this model. Encryption is handled transparently
|
|
at the storage layer via FernetEncryptionWrapper. Tokens are never exposed to MCP clients.
|
|
|
|
|
|
### `JTIMapping` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L169" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
|
|
Maps FastMCP token JTI to upstream token ID.
|
|
|
|
This allows stateless JWT validation while still being able to look up
|
|
the corresponding upstream token when tools need to access upstream APIs.
|
|
|
|
|
|
### `ProxyDCRClient` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L181" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
|
|
Client for DCR proxy with configurable redirect URI validation.
|
|
|
|
This special client class is critical for the OAuth proxy to work correctly
|
|
with Dynamic Client Registration (DCR). Here's why it exists:
|
|
|
|
Problem:
|
|
--------
|
|
When MCP clients use OAuth, they dynamically register with random localhost
|
|
ports (e.g., http://localhost:55454/callback). The OAuth proxy needs to:
|
|
1. Accept these dynamic redirect URIs from clients based on configured patterns
|
|
2. Use its own fixed redirect URI with the upstream provider (Google, GitHub, etc.)
|
|
3. Forward the authorization code back to the client's dynamic URI
|
|
|
|
Solution:
|
|
---------
|
|
This class validates redirect URIs against configurable patterns,
|
|
while the proxy internally uses its own fixed redirect URI with the upstream
|
|
provider. This allows the flow to work even when clients reconnect with
|
|
different ports or when tokens are cached.
|
|
|
|
Without proper validation, clients could get "Redirect URI not registered" errors
|
|
when trying to authenticate with cached tokens, or security vulnerabilities could
|
|
arise from accepting arbitrary redirect URIs.
|
|
|
|
|
|
**Methods:**
|
|
|
|
#### `validate_redirect_uri` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L210" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
```python
|
|
validate_redirect_uri(self, redirect_uri: AnyUrl | None) -> AnyUrl
|
|
```
|
|
|
|
Validate redirect URI against allowed patterns.
|
|
|
|
Since we're acting as a proxy and clients register dynamically,
|
|
we validate their redirect URIs against configurable patterns.
|
|
This is essential for cached token scenarios where the client may
|
|
reconnect with a different port.
|
|
|
|
|
|
### `TokenHandler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L383" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
|
|
TokenHandler that returns OAuth 2.1 compliant error responses.
|
|
|
|
The MCP SDK always returns HTTP 400 for all client authentication issues.
|
|
However, OAuth 2.1 Section 5.3 and the MCP specification require that
|
|
invalid or expired tokens MUST receive a HTTP 401 response.
|
|
|
|
This handler extends the base MCP SDK TokenHandler to transform client
|
|
authentication failures into OAuth 2.1 compliant responses:
|
|
- Changes 'unauthorized_client' to 'invalid_client' error code
|
|
- Returns HTTP 401 status code instead of 400 for client auth failures
|
|
|
|
Per OAuth 2.1 Section 5.3: "The authorization server MAY return an HTTP 401
|
|
(Unauthorized) status code to indicate which HTTP authentication schemes
|
|
are supported."
|
|
|
|
Per MCP spec: "Invalid or expired tokens MUST receive a HTTP 401 response."
|
|
|
|
|
|
**Methods:**
|
|
|
|
#### `response` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L402" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
```python
|
|
response(self, obj: TokenSuccessResponse | TokenErrorResponse)
|
|
```
|
|
|
|
Override response method to provide OAuth 2.1 compliant error handling.
|
|
|
|
|
|
### `OAuthProxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L432" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
|
|
OAuth provider that presents a DCR-compliant interface while proxying to non-DCR IDPs.
|
|
|
|
Purpose
|
|
-------
|
|
MCP clients expect OAuth providers to support Dynamic Client Registration (DCR),
|
|
where clients can register themselves dynamically and receive unique credentials.
|
|
Most enterprise IDPs (Google, GitHub, Azure AD, etc.) don't support DCR and require
|
|
pre-registered OAuth applications with fixed credentials.
|
|
|
|
This proxy bridges that gap by:
|
|
- Presenting a full DCR-compliant OAuth interface to MCP clients
|
|
- Translating DCR registration requests to use pre-configured upstream credentials
|
|
- Proxying all OAuth flows to the upstream IDP with appropriate translations
|
|
- Managing the state and security requirements of both protocols
|
|
|
|
Architecture Overview
|
|
--------------------
|
|
The proxy maintains a single OAuth app registration with the upstream provider
|
|
while allowing unlimited MCP clients to register and authenticate dynamically.
|
|
It implements the complete OAuth 2.1 + DCR specification for clients while
|
|
translating to whatever OAuth variant the upstream provider requires.
|
|
|
|
Key Translation Challenges Solved
|
|
---------------------------------
|
|
1. Dynamic Client Registration:
|
|
- MCP clients expect to register dynamically and get unique credentials
|
|
- Upstream IDPs require pre-registered apps with fixed credentials
|
|
- Solution: Accept DCR requests, return shared upstream credentials
|
|
|
|
2. Dynamic Redirect URIs:
|
|
- MCP clients use random localhost ports that change between sessions
|
|
- Upstream IDPs require fixed, pre-registered redirect URIs
|
|
- Solution: Use proxy's fixed callback URL with upstream, forward to client's dynamic URI
|
|
|
|
3. Authorization Code Mapping:
|
|
- Upstream returns codes for the proxy's redirect URI
|
|
- Clients expect codes for their own redirect URIs
|
|
- Solution: Exchange upstream code server-side, issue new code to client
|
|
|
|
4. State Parameter Collision:
|
|
- Both client and proxy need to maintain state through the flow
|
|
- Only one state parameter available in OAuth
|
|
- Solution: Use transaction ID as state with upstream, preserve client's state
|
|
|
|
5. Token Management:
|
|
- Clients may expect different token formats/claims than upstream provides
|
|
- Need to track tokens for revocation and refresh
|
|
- Solution: Store token relationships, forward upstream tokens transparently
|
|
|
|
OAuth Flow Implementation
|
|
------------------------
|
|
1. Client Registration (DCR):
|
|
- Accept any client registration request
|
|
- Store ProxyDCRClient that accepts dynamic redirect URIs
|
|
|
|
2. Authorization:
|
|
- Store transaction mapping client details to proxy flow
|
|
- Redirect to upstream with proxy's fixed redirect URI
|
|
- Use transaction ID as state parameter with upstream
|
|
|
|
3. Upstream Callback:
|
|
- Exchange upstream authorization code for tokens (server-side)
|
|
- Generate new authorization code bound to client's PKCE challenge
|
|
- Redirect to client's original dynamic redirect URI
|
|
|
|
4. Token Exchange:
|
|
- Validate client's code and PKCE verifier
|
|
- Return previously obtained upstream tokens
|
|
- Clean up one-time use authorization code
|
|
|
|
5. Token Refresh:
|
|
- Forward refresh requests to upstream using authlib
|
|
- Handle token rotation if upstream issues new refresh token
|
|
- Update local token mappings
|
|
|
|
State Management
|
|
---------------
|
|
The proxy maintains minimal but crucial state:
|
|
- _oauth_transactions: Active authorization flows with client context
|
|
- _client_codes: Authorization codes with PKCE challenges and upstream tokens
|
|
- _access_tokens, _refresh_tokens: Token storage for revocation
|
|
- Token relationship mappings for cleanup and rotation
|
|
|
|
Security Considerations
|
|
----------------------
|
|
- PKCE enforced end-to-end (client to proxy, proxy to upstream)
|
|
- Authorization codes are single-use with short expiry
|
|
- Transaction IDs are cryptographically random
|
|
- All state is cleaned up after use to prevent replay
|
|
- Token validation delegates to upstream provider
|
|
|
|
Provider Compatibility
|
|
---------------------
|
|
Works with any OAuth 2.0 provider that supports:
|
|
- Authorization code flow
|
|
- Fixed redirect URI (configured in provider's app settings)
|
|
- Standard token endpoint
|
|
|
|
Handles provider-specific requirements:
|
|
- Google: Ensures minimum scope requirements
|
|
- GitHub: Compatible with OAuth Apps and GitHub Apps
|
|
- Azure AD: Handles tenant-specific endpoints
|
|
- Generic: Works with any spec-compliant provider
|
|
|
|
|
|
**Methods:**
|
|
|
|
#### `get_client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L813" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
```python
|
|
get_client(self, client_id: str) -> OAuthClientInformationFull | None
|
|
```
|
|
|
|
Get client information by ID. This is generally the random ID
|
|
provided to the DCR client during registration, not the upstream client ID.
|
|
|
|
For unregistered clients, returns None (which will raise an error in the SDK).
|
|
|
|
|
|
#### `register_client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L829" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
```python
|
|
register_client(self, client_info: OAuthClientInformationFull) -> None
|
|
```
|
|
|
|
Register a client locally
|
|
|
|
When a client registers, we create a ProxyDCRClient that is more
|
|
forgiving about validating redirect URIs, since the DCR client's
|
|
redirect URI will likely be localhost or unknown to the proxied IDP. The
|
|
proxied IDP only knows about this server's fixed redirect URI.
|
|
|
|
|
|
#### `authorize` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L876" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
```python
|
|
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
|
|
```
|
|
|
|
Start OAuth transaction and route through consent interstitial.
|
|
|
|
Flow:
|
|
1. Store transaction with client details and PKCE (if forwarding)
|
|
2. Return local /consent URL; browser visits consent first
|
|
3. Consent handler redirects to upstream IdP if approved/already approved
|
|
|
|
If consent is disabled (require_authorization_consent=False), skip the consent screen
|
|
and redirect directly to the upstream IdP.
|
|
|
|
|
|
#### `load_authorization_code` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L951" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
```python
|
|
load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None
|
|
```
|
|
|
|
Load authorization code for validation.
|
|
|
|
Look up our client code and return authorization code object
|
|
with PKCE challenge for validation.
|
|
|
|
|
|
#### `exchange_authorization_code` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L994" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
```python
|
|
exchange_authorization_code(self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken
|
|
```
|
|
|
|
Exchange authorization code for FastMCP-issued tokens.
|
|
|
|
Implements the token factory pattern:
|
|
1. Retrieves upstream tokens from stored authorization code
|
|
2. Extracts user identity from upstream token
|
|
3. Encrypts and stores upstream tokens
|
|
4. Issues FastMCP-signed JWT tokens
|
|
5. Returns FastMCP tokens (NOT upstream tokens)
|
|
|
|
PKCE validation is handled by the MCP framework before this method is called.
|
|
|
|
|
|
#### `load_refresh_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L1159" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
```python
|
|
load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None
|
|
```
|
|
|
|
Load refresh token from local storage.
|
|
|
|
|
|
#### `exchange_refresh_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L1167" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
```python
|
|
exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken
|
|
```
|
|
|
|
Exchange FastMCP refresh token for new FastMCP access token.
|
|
|
|
Implements two-tier refresh:
|
|
1. Verify FastMCP refresh token
|
|
2. Look up upstream token via JTI mapping
|
|
3. Refresh upstream token with upstream provider
|
|
4. Update stored upstream token
|
|
5. Issue new FastMCP access token
|
|
6. Keep same FastMCP refresh token (unless upstream rotates)
|
|
|
|
|
|
#### `load_access_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L1368" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
```python
|
|
load_access_token(self, token: str) -> AccessToken | None
|
|
```
|
|
|
|
Validate FastMCP JWT by swapping for upstream token.
|
|
|
|
This implements the token swap pattern:
|
|
1. Verify FastMCP JWT signature (proves it's our token)
|
|
2. Look up upstream token via JTI mapping
|
|
3. Decrypt upstream token
|
|
4. Validate upstream token with provider (GitHub API, JWT validation, etc.)
|
|
5. Return upstream validation result
|
|
|
|
The FastMCP JWT is a reference token - all authorization data comes
|
|
from validating the upstream token via the TokenVerifier.
|
|
|
|
|
|
#### `revoke_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L1424" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
```python
|
|
revoke_token(self, token: AccessToken | RefreshToken) -> None
|
|
```
|
|
|
|
Revoke token locally and with upstream server if supported.
|
|
|
|
Removes tokens from local storage and attempts to revoke them with
|
|
the upstream server if a revocation endpoint is configured.
|
|
|
|
|
|
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L1468" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
|
|
|
```python
|
|
get_routes(self, mcp_path: str | None = None) -> list[Route]
|
|
```
|
|
|
|
Get OAuth routes with custom handlers for better error UX.
|
|
|
|
This method creates standard OAuth routes and replaces:
|
|
- /authorize endpoint: Enhanced error responses for unregistered clients
|
|
- /token endpoint: OAuth 2.1 compliant error codes
|
|
|
|
**Args:**
|
|
- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")
|
|
This is used to advertise the resource URL in metadata.
|
|
|