# 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. [Custom OAuth Client Support](#custom-oauth-client-support)
6. [API Reference](#api-reference)
7. [Per-User API Key Management](#per-user-api-key-management)
8. [Security Best Practices](#security-best-practices)
9. [Troubleshooting](#troubleshooting)
10. [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}
```

---

## Per-User API Key Management

### Overview

After completing OAuth authentication, each user receives a unique, revocable API key. This replaces the shared `MCP_API_KEY` model with individual keys for fine-grained access control.

### Key Lifecycle

1. **Generation**: After OAuth success, a 32-byte URL-safe token is generated
2. **Display**: Key shown once on the success page (blurred by default, click-to-reveal, copy-to-clipboard)
3. **Storage**: Only the SHA-256 hash + creation timestamp stored in `.user_api_keys.json` (permissions `0o600`)
4. **Lookup**: Timing-safe comparison via `hmac.compare_digest()` prevents timing attacks
5. **Revocation**: `revoke_user_key(email)` removes the key hash immediately

### Account Linking

Per-user keys can access multiple Google accounts through bidirectional linking. Links are established via three methods, each tracked with metadata:

#### Linking Methods

| Method | How It Works | Time Restriction |
|--------|-------------|-----------------|
| **OAuth** | User connects via OAuth, then calls `start_google_auth` for another email | None — always allowed |
| **Session** | Two `start_google_auth` calls in the same MCP session | None — always allowed |
| **API Key** | Per-user API key session calls `start_google_auth` for another email | **30-minute window** from key creation |

The 30-minute API key linking window prevents stolen keys from being used to link arbitrary accounts after the initial setup period. After the window expires, OAuth authentication is required to establish new links.

#### How Linking Works

```python
# Link two accounts (bidirectional, with method tracking)
link_accounts("user@example.com", "user@company.com", method="oauth")

# Check accessible accounts
get_accessible_emails("user@example.com")
# Returns: {"user@example.com", "user@company.com"}

# Check how accounts were linked
get_link_method("user@example.com", "user@company.com")
# Returns: "oauth", "session", "api_key", or "both"

# Deferred linking (target hasn't completed OAuth yet)
request_link("user@example.com", "new@example.com", method="session")
# Activated when new@example.com completes OAuth via consume_pending_links()
```

#### Link Metadata Storage

Link metadata is stored in `.user_api_key_link_metadata.json`:

```json
{
  "user@company.com::user@example.com": {
    "method": "oauth",
    "linked_at": "2026-03-08T20:15:00+00:00"
  }
}
```

If accounts are linked via multiple methods, the metadata upgrades to `"both"`:

```json
{
  "method": "both",
  "methods": ["oauth", "session"],
  "linked_at": "2026-03-08T20:15:00+00:00",
  "updated_at": "2026-03-08T21:00:00+00:00"
}
```

#### Session Email Seeding

When a user connects via any authentication method (OAuth, JWT, API key), their email is automatically seeded into `SESSION_AUTHED_EMAILS`. This ensures that subsequent `start_google_auth` calls for other emails can discover the existing user and queue pending links.

### Credential Isolation

The middleware enforces strict credential isolation based on auth provenance:

| Auth Type | Can Access | OAuth File Fallback |
|-----------|-----------|-------------------|
| `API_KEY` (shared) | Only credentials created in this session | Blocked |
| `USER_API_KEY` (per-user) | Owner email + linked accounts | Blocked |
| `OAUTH` (browser) | All credentials for authenticated user | Allowed |

### Multi-Recipient Envelope Encryption

Credentials are encrypted using a **multi-recipient CEK (Content Encryption Key) envelope** scheme. This allows multiple linked users to independently decrypt the same credential file without sharing keys.

#### Envelope Structure

```json
{
  "v": 2,
  "enc": "per_user",
  "recipients": {
    "<sha256_hash_of_key_A>": "<CEK_wrapped_with_key_A>",
    "<sha256_hash_of_key_B>": "<CEK_wrapped_with_key_B>"
  },
  "data": "<credentials_encrypted_with_random_CEK>",
  "hmac": "<HMAC-SHA256_integrity_seal>"
}
```

#### How It Works

1. A random **Content Encryption Key (CEK)** is generated for each credential file
2. The CEK encrypts the actual Google Workspace credentials (tokens, refresh tokens, etc.)
3. The CEK is separately **wrapped** (Fernet-encrypted) for each authorized user's derived key
4. Each user's wrapping key is derived via: `HKDF-SHA256(ikm=per_user_key, salt=server_secret, info="per-user-credential-encryption-v1")`
5. An **HMAC-SHA256** integrity seal (keyed with server secret) protects the entire envelope from tampering

#### Split-Key Security Model

Neither the server secret alone NOR the per-user API key alone can decrypt credentials:

```
Decryption Key = HKDF-SHA256(
    input_key = per_user_api_key,
    salt = server_secret (.auth_encryption_key),
    info = "per-user-credential-encryption-v1",
    length = 32 bytes
)
```

This split-key model ensures:
- **Server compromise alone** does not expose credentials
- **Key theft alone** does not expose credentials
- Both pieces are required for decryption

#### Security Hardening

- **File locking**: `fcntl.flock(LOCK_EX)` prevents TOCTOU races during recipient addition
- **Full key IDs**: SHA-256 hex digest (64 chars) used for recipient lookup
- **CEK zeroing**: Best-effort `ctypes.memset` clears sensitive key material from memory
- **HMAC integrity**: Detects tampering or downgrade attacks on the envelope
- **chmod warnings**: Logged if restrictive file permissions cannot be set

#### Cross-Add (Linked Account Recipient Addition)

When user B authenticates in the same session as user A:
1. B's credentials are encrypted with both A's and B's keys (as recipients)
2. A's credential file is updated to add B as a new recipient via `add_recipient_to_encrypted_file()`
3. File locking ensures atomic read-modify-write

### Security Visualization

The OAuth success page displays a real-time security visualization showing:
- **Authorized users** with their linkage method (OAuth, session, API key)
- **Key wrapping flow** diagram
- **Sealed envelope** contents (wrapped CEKs, encrypted services, HMAC seal)
- **Security features** summary including the 30-minute API key linking policy

### Environment Variables

```bash
# Per-user API key management (also used for crypto-bound encryption)
MCP_API_KEY=your-server-api-key
```

---

## 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: March 2026*
*Version: 3.0.0*