# FastMCP Google MCP OAuth Authentication Guide
## Table of Contents
1. [Architecture Overview](#architecture-overview)
2. [Authentication Flow](#authentication-flow)
3. [OAuth Proxy Details](#oauth-proxy-details)
4. [Configuration Guide](#configuration-guide)
5. [API Reference](#api-reference)
6. [Security Best Practices](#security-best-practices)
7. [Troubleshooting](#troubleshooting)
8. [Migration Guide](#migration-guide)
---
## Architecture Overview
### Why OAuth Proxy?
The OAuth Proxy is a critical component that bridges the gap between two incompatible OAuth approaches:
1. **MCP's Expectation**: The Model Context Protocol (MCP) specification requires Dynamic Client Registration (DCR) where clients can register themselves dynamically and receive unique OAuth credentials.
2. **Google's Reality**: Google OAuth doesn't support true DCR. Instead, it uses fixed OAuth credentials that must be pre-registered through the Google Cloud Console.
### The Solution: OAuth Proxy
The OAuth Proxy acts as an intermediary layer that:
- **Generates temporary credentials** (prefixed with `mcp_`) for each MCP client
- **Maps temporary credentials to real Google OAuth credentials** internally
- **Never exposes real Google credentials** to external clients
- **Handles token exchange** transparently
- **Provides DCR-compliant endpoints** while using fixed Google credentials behind the scenes
### Security Benefits
1. **Credential Isolation**: Real Google OAuth credentials are never exposed to MCP clients
2. **Audit Trail**: Each MCP client gets unique temporary credentials for tracking
3. **Revocation Control**: Individual client access can be revoked without affecting others
4. **Automatic Cleanup**: Expired proxy clients are automatically removed
### Architecture Diagram
```mermaid
graph TB
subgraph MCP Client
MC[MCP Inspector/Client]
end
subgraph FastMCP2 Server
DE[Discovery Endpoints]
OP[OAuth Proxy]
DCR[Dynamic Client Registration]
TE[Token Exchange]
CS[Credential Storage]
end
subgraph Google OAuth
GO[Google OAuth Server]
GA[Google APIs]
end
MC -->|1. Discover| DE
MC -->|2. Register| DCR
DCR -->|3. Generate temp creds| OP
OP -->|4. Store mapping| CS
MC -->|5. Token request| TE
TE -->|6. Map creds| OP
OP -->|7. Real creds| GO
GO -->|8. Access token| TE
TE -->|9. Return token| MC
MC -->|10. API calls| GA
```
---
## Authentication Flow
### Step-by-Step Process
#### 1. Discovery Phase
The MCP client discovers OAuth endpoints by querying well-known URIs.
```http
GET /.well-known/oauth-protected-resource
```
**Response:**
```json
{
"oauth_authorization_server": "http://localhost:8002/.well-known/oauth-authorization-server",
"scopes_supported": ["drive.file", "gmail.send", "calendar.events"],
"bearer_methods_supported": ["header", "body", "query"]
}
```
#### 2. Authorization Server Discovery
The client fetches the authorization server configuration.
```http
GET /.well-known/oauth-authorization-server
```
**Response:**
```json
{
"issuer": "http://localhost:8002",
"authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
"token_endpoint": "http://localhost:8002/oauth/token",
"registration_endpoint": "http://localhost:8002/oauth/register",
"scopes_supported": ["drive.file", "gmail.send", "calendar.events", "..."],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"]
}
```
#### 3. Dynamic Client Registration
The client registers itself and receives temporary credentials. **Public clients** can specify `token_endpoint_auth_method: "none"` for PKCE-only flows.
```http
POST /oauth/register
Content-Type: application/json
{
"client_name": "MCP Inspector",
"redirect_uris": ["http://localhost:3000/auth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "none" // For public clients using PKCE
}
```
**Response:**
```json
{
"client_id": "mcp_7f3a9b2c",
"client_secret": "temp_secret_x5k9m2p4",
"client_id_issued_at": 1703123456,
"client_secret_expires_at": 0,
"registration_access_token": "reg_token_abc123",
"registration_client_uri": "http://localhost:8002/oauth/register/mcp_7f3a9b2c",
"token_endpoint_auth_method": "none" // Echoed for public clients
}
```
**Note for Public Clients:**
- When `token_endpoint_auth_method: "none"` is specified, the proxy skips client_secret validation
- This enables true public client PKCE flows without requiring client_secret
- Your Google OAuth client type should be configured as "Public application" or "Mobile" for this to work
#### 4. Authorization Flow
The client redirects the user to Google's authorization endpoint.
```
https://accounts.google.com/o/oauth2/v2/auth?
client_id=856407677608-c004jbl22ejkqmpv8511i20sallrrk2e.apps.googleusercontent.com
&redirect_uri=http://localhost:3000/auth/callback
&response_type=code
&scope=drive.file+gmail.send+calendar.events
&state=random_state_string
```
#### 5. Token Exchange
After authorization, the client exchanges the authorization code for tokens.
```http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=4/0AY0e-g7...
&client_id=mcp_7f3a9b2c
&client_secret=temp_secret_x5k9m2p4
&redirect_uri=http://localhost:3000/auth/callback
```
**Response:**
```json
{
"access_token": "ya29.a0AS3H6Nx5...",
"token_type": "Bearer",
"expires_in": 3599,
"refresh_token": "1//04eT7hoXDsywc...",
"scope": "drive.file gmail.send calendar.events"
}
```
### Sequence Diagram
```mermaid
sequenceDiagram
participant C as MCP Client
participant P as OAuth Proxy
participant D as DCR Handler
participant T as Token Exchange
participant G as Google OAuth
participant U as User
C->>P: GET /.well-known/oauth-protected-resource
P-->>C: Discovery info with token endpoint
C->>D: POST /oauth/register
D->>P: Generate temp credentials
P-->>D: mcp_xxx, temp_secret_yyy
D-->>C: Return temp credentials
C->>U: Redirect to Google Auth
U->>G: Authorize access
G-->>C: Authorization code
C->>T: POST /oauth/token with temp creds
T->>P: Map temp to real creds
P-->>T: Real Google credentials
T->>G: Exchange code with real creds
G-->>T: Access token + refresh token
T-->>C: Return tokens
C->>G: API calls with access token
G-->>C: API responses
```
---
## OAuth Proxy Details
### Temporary Credential Generation with Public Client Support
The OAuth Proxy generates unique temporary credentials for each MCP client and supports **public clients** (PKCE without client_secret):
```python
# auth/oauth_proxy.py - Enhanced with Public Client Support
def register_proxy_client(self, real_client_id: str, real_client_secret: str,
client_metadata: Dict[str, Any]) -> Dict[str, Any]:
"""Register a new proxy client with temporary credentials."""
# Generate unique temporary credentials
temp_client_id = f"mcp_{secrets.token_urlsafe(16)}"
temp_client_secret = secrets.token_urlsafe(32)
# Create proxy client mapping
proxy_client = ProxyClient(
temp_client_id=temp_client_id,
temp_client_secret=temp_client_secret,
real_client_id=real_client_id,
real_client_secret=real_client_secret,
client_metadata=client_metadata,
created_at=datetime.now(timezone.utc)
)
# Store in registry (never expose real credentials)
self._proxy_clients[temp_client_id] = proxy_client
return {
"client_id": temp_client_id,
"client_secret": temp_client_secret,
"registration_access_token": proxy_client.registration_access_token,
# ... other DCR fields
}
def get_real_credentials(self, temp_client_id: str, temp_client_secret: str):
"""Get real credentials with public client support."""
proxy_client = self._proxy_clients.get(temp_client_id)
# Check if this is a public client (no authentication required)
auth_method = proxy_client.client_metadata.get('token_endpoint_auth_method')
is_public_client = auth_method == 'none'
# Skip client_secret validation for public clients
if is_public_client:
logger.info("✅ Public client - skipping client_secret validation")
elif proxy_client.temp_client_secret != temp_client_secret:
logger.warning("❌ Invalid temp secret for client")
return None
return (proxy_client.real_client_id, proxy_client.real_client_secret)
```
### Credential Mapping Mechanism
The proxy maintains an internal mapping between temporary and real credentials:
```python
# During token exchange
def exchange_token(self, client_id: str, client_secret: str,
authorization_code: str) -> Dict[str, Any]:
# Check if this is a proxy client
if client_id.startswith("mcp_"):
# Get real credentials from proxy
real_credentials = oauth_proxy.get_real_credentials(
client_id, client_secret
)
if not real_credentials:
raise ValueError("Invalid proxy client credentials")
real_client_id, real_client_secret = real_credentials
# Use real credentials for Google OAuth
return google_oauth_exchange(
real_client_id,
real_client_secret,
authorization_code
)
```
### Security Considerations
1. **Credential Lifetime**: Proxy clients have configurable expiry (default: 24 hours)
2. **Secret Validation**: All requests validate both client_id and client_secret
3. **Registration Tokens**: Used for client management operations
4. **Automatic Cleanup**: Expired clients are removed periodically
```python
def cleanup_expired_clients(self):
"""Remove expired proxy clients."""
current_time = datetime.now(timezone.utc)
expired = []
for client_id, client in self._proxy_clients.items():
age = current_time - client.created_at
if age > timedelta(hours=24): # Configurable expiry
expired.append(client_id)
for client_id in expired:
del self._proxy_clients[client_id]
logger.info(f"Cleaned up expired proxy client: {client_id}")
```
---
## Configuration Guide
### Environment Variables
Create a `.env` file in your project root with the following variables:
```bash
# ============================================
# GOOGLE OAUTH CONFIGURATION
# ============================================
# Option 1: Using credentials JSON file (recommended)
GOOGLE_OAUTH_CREDENTIALS_FILE=credentials.json
# Option 2: Individual credentials (if not using JSON file)
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
GOOGLE_REDIRECT_URI=http://localhost:3000/auth/callback
# OAuth Scopes (space-separated)
GOOGLE_OAUTH_SCOPES="https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/gmail.send https://www.googleapis.com/auth/calendar.events"
# ============================================
# CREDENTIAL STORAGE CONFIGURATION
# ============================================
# Storage mode for credentials
# Options: FILE_PLAINTEXT, FILE_ENCRYPTED, MEMORY_ONLY, MEMORY_WITH_BACKUP
CREDENTIAL_STORAGE_MODE=FILE_ENCRYPTED
# Directory for storing credentials
CREDENTIALS_DIR=./credentials
# Encryption key file (for FILE_ENCRYPTED mode)
AUTH_ENCRYPTION_KEY_FILE=.auth_encryption_key
# Session configuration
SESSION_TIMEOUT_MINUTES=60
MAX_SESSION_LIFETIME_HOURS=24
# ============================================
# MCP SERVER CONFIGURATION
# ============================================
# Server host and port
MCP_HOST=localhost
MCP_PORT=8002
# Base URL for OAuth endpoints
BASE_URL=http://localhost:8002
# MCP Authentication
MCP_AUTH_ENABLED=true
MCP_AUTH_REALM=FastMCP_Google_MCP
# ============================================
# OAUTH PROXY CONFIGURATION
# ============================================
# Proxy client expiry (hours)
PROXY_CLIENT_EXPIRY_HOURS=24
# Cleanup interval (minutes)
PROXY_CLEANUP_INTERVAL_MINUTES=60
# Maximum concurrent proxy clients
MAX_PROXY_CLIENTS=100
```
### Setting Up Google OAuth Credentials
#### Step 1: Create a Google Cloud Project
1. Go to [Google Cloud Console](https://console.cloud.google.com)
2. Create a new project or select an existing one
3. Enable the APIs you need (Drive, Gmail, Calendar, etc.)
#### Step 2: Configure OAuth Consent Screen
1. Navigate to "APIs & Services" > "OAuth consent screen"
2. Choose "External" for public access
3. Fill in the application information
4. Add the required scopes
5. Add test users if in testing mode
#### Step 3: Create OAuth Credentials
1. Go to "APIs & Services" > "Credentials"
2. Click "Create Credentials" > "OAuth client ID"
3. **Choose application type:**
- **"Web application"** - For confidential clients (requires client_secret)
- **"Public application"** or **"Mobile"** - For public clients (PKCE without client_secret)
4. Add authorized redirect URIs:
- `http://localhost:3000/auth/callback`
- `http://127.0.0.1:3000/auth/callback`
- `http://localhost:6274/oauth/callback/debug`
5. Download the credentials JSON file
#### Step 4: Configure FastMCP Google MCP
Save the credentials file as `credentials.json` in your project root or set individual environment variables.
---
## Custom OAuth Client Support
FastMCP2 now supports **custom OAuth clients** with robust fallback mechanisms, enabling you to use your own OAuth credentials alongside the system defaults.
### Using Custom OAuth Credentials
```python
# Initiate OAuth flow with custom credentials
auth_url = await initiate_oauth_flow(
user_email="user@example.com",
custom_client_id="your-custom-client-id.apps.googleusercontent.com",
custom_client_secret="your-custom-secret", # Optional for PKCE
use_pkce=True,
auth_method='pkce_file' # or 'pkce_memory'
)
```
### Credential Fallback Chain
The system implements a **three-tier fallback mechanism** for maximum resilience:
#### 1. State Map (Primary)
```python
# Credentials stored during OAuth initiation
_oauth_state_map[state] = {
'user_email': user_email,
'custom_client_id': custom_client_id,
'custom_client_secret': custom_client_secret,
'auth_method': auth_method
}
```
#### 2. UnifiedSession Metadata (Fallback 1)
```python
# Enhanced persistence via UnifiedSession
unified_session = UnifiedSession()
unified_session.store_custom_oauth_credentials(
state=state,
custom_client_id=custom_client_id,
custom_client_secret=custom_client_secret,
auth_method=auth_method
)
```
#### 3. Context Storage (Fallback 2)
```python
# Context-based storage for cross-request persistence
from auth.context import store_custom_oauth_credentials
store_custom_oauth_credentials(state, custom_client_id, custom_client_secret)
```
### Public Client PKCE (No client_secret)
For public clients, you can omit the `client_secret`:
```python
auth_url = await initiate_oauth_flow(
user_email="user@example.com",
custom_client_id="your-public-client-id.apps.googleusercontent.com",
# No custom_client_secret provided
use_pkce=True,
auth_method='pkce_memory' # Ephemeral storage for public clients
)
```
**Important:** Your Google OAuth client must be configured as **"Public application"** or **"Mobile"** type to use PKCE without client_secret.
### Credential Resolution Flow
```mermaid
graph TD
A[OAuth Callback] --> B{State Map<br/>has credentials?}
B -->|Yes| F[Use State Map]
B -->|No| C{UnifiedSession<br/>has credentials?}
C -->|Yes| F
C -->|No| D{Context Storage<br/>has credentials?}
D -->|Yes| F
D -->|No| E[Use Default Credentials]
F --> G[Token Exchange]
```
### Benefits
1. **Server Restart Resilience**: Multiple fallback layers ensure OAuth flows survive server restarts
2. **Custom Client Support**: Use your own OAuth credentials without modifying system config
3. **Public Client PKCE**: True public client support without requiring client_secret
4. **Automatic Fallback**: Seamlessly falls back to default credentials when custom ones aren't complete
### Credential Storage Modes
| Mode | Security | Persistence | Use Case |
|------|----------|-------------|----------|
| `FILE_PLAINTEXT` | Low | Yes | Development only |
| `FILE_ENCRYPTED` | High | Yes | Production recommended |
| `MEMORY_ONLY` | Highest | No | High-security, ephemeral |
| `MEMORY_WITH_BACKUP` | High | Yes | Balance of security and UX |
#### FILE_ENCRYPTED Configuration (Recommended)
```bash
CREDENTIAL_STORAGE_MODE=FILE_ENCRYPTED
AUTH_ENCRYPTION_KEY_FILE=.auth_encryption_key
```
The server will automatically:
1. Generate an encryption key if it doesn't exist
2. Use AES-256-GCM encryption for credentials
3. Store encrypted credentials in `credentials/*.enc` files
4. Set restrictive file permissions (0600)
---
## API Reference
### Discovery Endpoints
#### OAuth Protected Resource Discovery
```http
GET /.well-known/oauth-protected-resource
```
**Response Schema:**
```typescript
interface ProtectedResourceMetadata {
oauth_authorization_server: string; // Authorization server discovery URL
scopes_supported: string[]; // Available OAuth scopes
bearer_methods_supported: string[]; // Token passing methods
resource_documentation?: string; // Optional documentation URL
}
```
**Example Response:**
```json
{
"oauth_authorization_server": "http://localhost:8002/.well-known/oauth-authorization-server",
"scopes_supported": [
"https://www.googleapis.com/auth/drive.file",
"https://www.googleapis.com/auth/gmail.send",
"https://www.googleapis.com/auth/calendar.events"
],
"bearer_methods_supported": ["header", "body", "query"],
"resource_documentation": "https://docs.fastmcp2.com"
}
```
#### Authorization Server Discovery
```http
GET /.well-known/oauth-authorization-server
```
**Response Schema:**
```typescript
interface AuthorizationServerMetadata {
issuer: string; // Server identifier
authorization_endpoint: string; // Google OAuth authorization URL
token_endpoint: string; // Local token exchange endpoint
registration_endpoint: string; // Dynamic client registration
scopes_supported: string[]; // All supported scopes
response_types_supported: string[]; // OAuth response types
grant_types_supported: string[]; // OAuth grant types
token_endpoint_auth_methods_supported: string[];
service_documentation?: string;
}
```
### Registration Endpoint
#### Register Client
```http
POST /oauth/register
Content-Type: application/json
```
**Request Schema:**
```typescript
interface ClientRegistrationRequest {
client_name: string; // Display name for the client
redirect_uris: string[]; // Allowed redirect URIs
grant_types?: string[]; // OAuth grant types (default: ["authorization_code"])
response_types?: string[]; // OAuth response types (default: ["code"])
scope?: string; // Requested scopes (space-separated)
contacts?: string[]; // Contact emails
logo_uri?: string; // Client logo URL
client_uri?: string; // Client homepage URL
policy_uri?: string; // Privacy policy URL
tos_uri?: string; // Terms of service URL
}
```
**Response Schema:**
```typescript
interface ClientRegistrationResponse {
client_id: string; // Temporary client ID (mcp_xxx)
client_secret: string; // Temporary client secret
client_id_issued_at: number; // Unix timestamp
client_secret_expires_at: number; // 0 for no expiry
registration_access_token: string; // Token for client management
registration_client_uri: string; // Client management endpoint
// Echo back request fields
client_name: string;
redirect_uris: string[];
grant_types: string[];
response_types: string[];
scope?: string;
}
```
**Example Request:**
```json
{
"client_name": "MCP Inspector",
"redirect_uris": [
"http://localhost:3000/auth/callback",
"http://127.0.0.1:3000/auth/callback"
],
"grant_types": ["authorization_code", "refresh_token"],
"scope": "drive.file gmail.send"
}
```
#### Get Client Information
```http
GET /oauth/register/{client_id}
Authorization: Bearer {registration_access_token}
```
#### Update Client
```http
PUT /oauth/register/{client_id}
Authorization: Bearer {registration_access_token}
Content-Type: application/json
```
#### Delete Client
```http
DELETE /oauth/register/{client_id}
Authorization: Bearer {registration_access_token}
```
### Token Exchange Endpoint
#### Exchange Authorization Code
```http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
```
**Request Parameters:**
```
grant_type=authorization_code
&code={authorization_code}
&client_id={temp_client_id}
&client_secret={temp_client_secret}
&redirect_uri={redirect_uri}
```
**Response Schema:**
```typescript
interface TokenResponse {
access_token: string; // Google OAuth access token
token_type: string; // "Bearer"
expires_in: number; // Seconds until expiry
refresh_token?: string; // Refresh token (if granted)
scope: string; // Granted scopes (space-separated)
id_token?: string; // OpenID Connect ID token
}
```
#### Refresh Token
```http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
```
**Request Parameters:**
```
grant_type=refresh_token
&refresh_token={refresh_token}
&client_id={temp_client_id}
&client_secret={temp_client_secret}
```
---
## Security Best Practices
### 1. Credential Storage
#### Production Settings
```bash
# Always use encrypted storage in production
CREDENTIAL_STORAGE_MODE=FILE_ENCRYPTED
# Enable HTTPS
ENABLE_HTTPS=true
SSL_CERT_FILE=/etc/ssl/certs/server.crt
SSL_KEY_FILE=/etc/ssl/private/server.key
# Strict session timeouts
SESSION_TIMEOUT_MINUTES=30
MAX_SESSION_LIFETIME_HOURS=8
```
#### File Permissions
Ensure proper file permissions for credential storage:
```bash
# Set restrictive permissions on credentials directory
chmod 700 ./credentials
# Encryption key should be readable only by owner
chmod 600 .auth_encryption_key
# Credentials files are automatically set to 600 by the server
```
### 2. Environment Security
#### Never Commit Sensitive Files
Add to `.gitignore`:
```
.env
.env.*
credentials.json
.auth_encryption_key
credentials/
*.enc
```
#### Use Environment-Specific Configurations
```bash
# Development
.env.development
# Staging
.env.staging
# Production
.env.production
```
### 3. Token Management
#### Token Refresh Strategy
```python
def ensure_valid_token(credentials):
"""Ensure token is valid, refresh if needed."""
if credentials.expired:
# Automatic refresh using refresh token
credentials.refresh(Request())
# Save updated credentials
middleware.save_credentials(user_email, credentials)
return credentials.token
```
#### Token Expiry Handling
```python
# Configure token lifetime
ACCESS_TOKEN_EXPIRY = 3600 # 1 hour
REFRESH_TOKEN_EXPIRY = 2592000 # 30 days
# Monitor token age
def check_token_age(issued_at, current_time):
age = current_time - issued_at
if age > ACCESS_TOKEN_EXPIRY:
return "expired"
elif age > ACCESS_TOKEN_EXPIRY * 0.9:
return "near_expiry"
return "valid"
```
### 4. Proxy Client Security
#### Client Validation
```python
def validate_proxy_client(client_id: str, client_secret: str) -> bool:
"""Validate proxy client credentials."""
if not client_id.startswith("mcp_"):
return False
client = self._proxy_clients.get(client_id)
if not client:
return False
# Constant-time comparison for secrets
return secrets.compare_digest(
client.temp_client_secret,
client_secret
)
```
#### Rate Limiting
```python
# Implement rate limiting for registration
from functools import lru_cache
from time import time
@lru_cache(maxsize=1000)
def check_rate_limit(ip_address: str, window: int = 60) -> bool:
"""Check if IP has exceeded rate limit."""
current = int(time() / window)
key = f"{ip_address}:{current}"
if registration_counts.get(key, 0) >= MAX_REGISTRATIONS_PER_MINUTE:
return False
registration_counts[key] = registration_counts.get(key, 0) + 1
return True
```
### 5. Audit Logging
#### Security Events to Log
```python
# Log all security-relevant events
logger.info(f"✅ Client registered: {temp_client_id} from IP {request.client.host}")
logger.warning(f"⚠️ Invalid credentials attempt: {client_id}")
logger.error(f"❌ Token exchange failed: {client_id} - {error}")
logger.info(f"🔄 Token refreshed for: {client_id}")
logger.info(f"🗑️ Expired client cleaned up: {client_id}")
```
#### Structured Logging
```python
import structlog
logger = structlog.get_logger()
logger.info(
"oauth_event",
event_type="client_registration",
client_id=temp_client_id,
ip_address=request.client.host,
timestamp=datetime.utcnow().isoformat(),
success=True
)
```
---
## Troubleshooting
### Common Issues and Solutions
#### 1. "Invalid client credentials" Error
**Symptom:**
```json
{
"error": "invalid_client",
"error_description": "Invalid client credentials"
}
```
**Causes & Solutions:**
- **Expired proxy client**: Proxy clients expire after 24 hours. Re-register the client.
- **Wrong client_secret**: Ensure you're using the temporary secret from registration.
- **Client not found**: The client_id may have been cleaned up. Re-register.
**Debug Steps:**
```python
# Check if client exists
client = oauth_proxy.get_proxy_client(client_id)
if not client:
print("Client not found - needs re-registration")
# Check client age
age = datetime.now(timezone.utc) - client.created_at
if age > timedelta(hours=24):
print("Client expired - needs re-registration")
```
#### 2. "Redirect URI mismatch" Error
**Symptom:**
```
Error 400: redirect_uri_mismatch
The redirect URI in the request does not match the ones authorized
```
**Solution:**
1. Ensure redirect_uri in token exchange matches exactly what was registered
2. Check Google Console for authorized redirect URIs
3. Common issues:
- Missing trailing slash
- HTTP vs HTTPS mismatch
- Port number mismatch
**Correct Configuration:**
```python
# In registration
{
"redirect_uris": ["http://localhost:3000/auth/callback"]
}
# In token exchange (must match exactly)
redirect_uri = "http://localhost:3000/auth/callback"
```
#### 3. "Insufficient scope" Error
**Symptom:**
```json
{
"error": "insufficient_scope",
"error_description": "Request requires higher privileges"
}
```
**Solution:**
Check that all required scopes are:
1. Enabled in Google Cloud Console
2. Listed in OAuth consent screen
3. Included in authorization request
4. Not restricted by organization policies
**Verify Scopes:**
```python
# Check configured scopes
print(settings.google_oauth_scopes)
# Check granted scopes in token
token_info = credentials.get_token_info()
print(token_info.get("scope", "").split())
```
#### 4. Token Refresh Failures
**Symptom:**
```json
{
"error": "invalid_grant",
"error_description": "Token has been expired or revoked"
}
```
**Causes & Solutions:**
- **Revoked access**: User revoked access in Google Account settings
- **Expired refresh token**: Refresh tokens expire after 6 months of inactivity
- **Invalid refresh token**: Token corruption or storage issue
**Recovery Steps:**
```python
def handle_refresh_failure(user_email: str):
"""Handle refresh token failure."""
# Clear invalid credentials
middleware.delete_credentials(user_email)
# Require re-authentication
return {
"error": "reauth_required",
"message": "Please authenticate again",
"auth_url": get_authorization_url()
}
```
#### 5. Credential Storage Issues
**Symptom:**
```
Failed to save/load credentials
Permission denied: credentials/user_credentials.enc
```
**Solutions:**
For FILE_ENCRYPTED mode:
```bash
# Fix permissions
chmod 700 ./credentials
chmod 600 .auth_encryption_key
# Verify encryption key exists
ls -la .auth_encryption_key
# Check disk space
df -h ./credentials
```
For MEMORY_ONLY mode:
```python
# Credentials lost on restart - this is expected
# Implement session persistence if needed
```
#### 6. SSL/TLS Issues
**Symptom:**
```
SSL: CERTIFICATE_VERIFY_FAILED
```
**Solutions:**
```bash
# For development (NOT for production)
export NODE_TLS_REJECT_UNAUTHORIZED=0
# For production - install proper certificates
pip install certifi
export SSL_CERT_FILE=$(python -m certifi)
```
### Debug Mode
Enable debug logging for detailed troubleshooting:
```bash
# In .env
DEBUG_MODE=true
LOG_LEVEL=DEBUG
VERBOSE_ERRORS=true
```
```python
# In code
import logging
logging.basicConfig(level=logging.DEBUG)
# OAuth2 library debug
import httplib2
httplib2.debuglevel = 4
```
### Log Messages to Look For
#### Successful Flow
```
✅ OAuth Proxy initialized
✅ Client registered: mcp_7f3a9b2c
✅ Mapped temp to real credentials
✅ Token exchange successful
✅ Token refreshed successfully
✅ Saved encrypted credentials for user@example.com
```
#### Error Indicators
```
❌ Invalid proxy client credentials: mcp_xxx
❌ Token exchange failed: invalid_grant
⚠️ Expired proxy client: mcp_xxx
⚠️ Failed to decrypt credentials for user@example.com
❌ Rate limit exceeded for IP: 192.168.1.1
```
### Health Check Endpoint
Implement a health check to verify OAuth system status:
```python
@app.get("/health/oauth")
async def oauth_health():
"""Check OAuth system health."""
return {
"status": "healthy",
"proxy_clients": len(oauth_proxy._proxy_clients),
"storage_mode": settings.credential_storage_mode,
"encryption_enabled": settings.credential_storage_mode != "FILE_PLAINTEXT",
"google_oauth_configured": bool(settings.google_client_id),
"endpoints_available": {
"discovery": True,
"registration": True,
"token": True
}
}
```
---
## Migration Guide
### From Old Authentication to OAuth Proxy
#### Step 1: Backup Existing Credentials
```bash
# Backup existing credentials
cp -r ./credentials ./credentials.backup
```
#### Step 2: Update Environment Configuration
```bash
# Old configuration
GOOGLE_CLIENT_ID=xxx
GOOGLE_CLIENT_SECRET=yyy
# New configuration (add these)
CREDENTIAL_STORAGE_MODE=FILE_ENCRYPTED
MCP_AUTH_ENABLED=true
PROXY_CLIENT_EXPIRY_HOURS=24
```
#### Step 3: Migrate Credential Storage
```python
# Migration script
from auth.middleware import AuthMiddleware, CredentialStorageMode
import json
from pathlib import Path
def migrate_credentials():
"""Migrate from FILE_PLAINTEXT to FILE_ENCRYPTED."""
# Initialize middleware with old and new modes
old_middleware = AuthMiddleware(
storage_mode=CredentialStorageMode.FILE_PLAINTEXT
)
new_middleware = AuthMiddleware(
storage_mode=CredentialStorageMode.FILE_ENCRYPTED
)
# Find all credential files
creds_dir = Path("./credentials")
for cred_file in creds_dir.glob("*_credentials.json"):
# Extract email from filename
email = cred_file.stem.replace("_at_", "@").replace("_", ".")
email = email.replace(".credentials", "")
# Load with old method
credentials = old_middleware.load_credentials(email)
if credentials:
# Save with new method
new_middleware.save_credentials(email, credentials)
print(f"✅ Migrated: {email}")
else:
print(f"⚠️ Skipped (invalid): {email}")
print("Migration complete!")
if __name__ == "__main__":
migrate_credentials()
```
#### Step 4: Update Client Code
**Old Direct Credentials Approach:**
```python
# Old: Direct use of Google credentials
response = requests.post(
"http://localhost:8002/oauth/token",
data={
"client_id": GOOGLE_CLIENT_ID,
"client_secret": GOOGLE_CLIENT_SECRET,
"code": auth_code
}
)
```
**New OAuth Proxy Approach:**
```python
# New: Register and use temporary credentials
# 1. Register client
reg_response = requests.post(
"http://localhost:8002/oauth/register",
json={
"client_name": "My MCP Client",
"redirect_uris": ["http://localhost:3000/callback"]
}
)
temp_credentials = reg_response.json()
# 2. Use temporary credentials
token_response = requests.post(
"http://localhost:8002/oauth/token",
data={
"client_id": temp_credentials["client_id"], # mcp_xxx
"client_secret": temp_credentials["client_secret"],
"code": auth_code
}
)
```
### Backward Compatibility
The OAuth Proxy maintains backward compatibility:
1. **Direct credential usage still works** for existing integrations
2. **Automatic detection** of proxy vs direct credentials (mcp_ prefix)
3. **Gradual migration** possible - both methods can coexist
```python
# The proxy automatically handles both
if client_id.startswith("mcp_"):
# New proxy client
real_creds = oauth_proxy.get_real_credentials(client_id, client_secret)
else:
# Legacy direct credentials
real_creds = (client_id, client_secret)
```
### Rollback Plan
If you need to rollback:
```bash
# 1. Restore backup
mv ./credentials.backup ./credentials
# 2. Revert environment
CREDENTIAL_STORAGE_MODE=FILE_PLAINTEXT
MCP_AUTH_ENABLED=false
# 3. Restart server
python server.py
```
### Testing Migration
```python
# Test script
import requests
def test_oauth_migration():
"""Test that both old and new auth methods work."""
base_url = "http://localhost:8002"
# Test 1: Discovery endpoints
discovery = requests.get(f"{base_url}/.well-known/oauth-protected-resource")
assert discovery.status_code == 200
print("✅ Discovery endpoints working")
# Test 2: Client registration
reg_response = requests.post(
f"{base_url}/oauth/register",
json={
"client_name": "Test Client",
"redirect_uris": ["http://localhost:3000/callback"]
}
)
assert reg_response.status_code == 200
print("✅ Client registration working")
# Test 3: Verify temp credentials format
client_data = reg_response.json()
assert client_data["client_id"].startswith("mcp_")
print("✅ Temporary credentials generated")
# Test 4: Check backward compatibility
# (Requires actual auth code from Google)
# ... implement based on your needs
print("🎉 All migration tests passed!")
if __name__ == "__main__":
test_oauth_migration()
```
---
## Additional Resources
### Documentation Links
- [Google OAuth 2.0 Documentation](https://developers.google.com/identity/protocols/oauth2)
- [MCP Specification](https://spec.modelcontextprotocol.io)
- [FastMCP Documentation](https://docs.fastmcp.com)
### Example Implementations
- [MCP Inspector Integration](examples/mcp_inspector_integration.py)
- [Custom Client Example](examples/custom_oauth_client.py)
- [Migration Script](scripts/migrate_credentials.py)
### Support
- Contact your FastMCP development team for issues and community support
---
*Last Updated: December 2024*
*Version: 2.0.0*