Skip to main content
Glama
OAUTH2_AUTHENTICATION.md51.2 kB
# OAuth 2.1 Authentication Guide This guide explains the OAuth 2.1 authorization implementation for the MCP Cisco Support server, including architecture, flows, and usage examples. ## Table of Contents - [Overview](#overview) - [Architecture](#architecture) - [Authentication Modes](#authentication-modes) - [OAuth 2.1 Flow](#oauth-21-flow) - [PKCE Explained](#pkce-explained) - [Implementation Details](#implementation-details) - [Configuration](#configuration) - [Security Features](#security-features) - [Usage Examples](#usage-examples) - [When to Use Each Mode](#when-to-use-each-mode) - [Production Considerations](#production-considerations) ## Overview The MCP Cisco Support server implements **two authentication modes** for HTTP transport: 1. **Bearer Token (Default)** - Simple, fast authentication using static tokens 2. **OAuth 2.1** - Full OAuth 2.1 authorization server with PKCE The implementation follows the [MCP Authorization Specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) and is fully backward compatible with existing deployments. ## Architecture ### High-Level Architecture ``` ┌─────────────────────────────────────────────────────────┐ │ MCP Server (HTTP) │ ├─────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────────┐ │ │ │ Bearer │ │ OAuth 2.1 │ │ │ │ Token │ OR │ Authorization │ │ │ │ Middleware │ │ Server │ │ │ └──────────────┘ └──────────────────┘ │ │ │ │ │ │ │ │ │ │ └──────────┬───────────────────┘ │ │ │ │ │ ┌─────▼──────┐ │ │ │ MCP │ │ │ │ Endpoints │ │ │ └────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` ### Component Structure ``` src/ ├── oauth2.ts # OAuth 2.1 authorization server implementation ├── sse-server.ts # HTTP server with auth middleware selection ├── index.ts # Main entry point └── mcp-server.ts # MCP server implementation ``` ## Authentication Modes ### Mode 1: Bearer Token (Default) **Description:** Simple bearer token authentication using static tokens. **Workflow:** ``` Client Request → Bearer Token Check → MCP Handler ↓ (if valid) Attach user info ``` **Configuration:** ```bash # Use default bearer authentication AUTH_TYPE=bearer # or omit this line # Auto-generate random token on startup npx mcp-cisco-support --http # Output: 🔑 Bearer token: abc123... # Use custom token from environment export MCP_BEARER_TOKEN=your_custom_token_here npx mcp-cisco-support --http ``` **Client Usage:** ```bash # Include token in Authorization header (recommended) curl -H "Authorization: Bearer <token>" http://localhost:3000/mcp # Or use query parameter (less secure) curl http://localhost:3000/mcp?token=<token> ``` ### Mode 2: OAuth 2.1 Authorization **Description:** Full OAuth 2.1 authorization server with PKCE, client registration, and token management. **Workflow:** ``` Client Registration → Authorization Request → User Consent ↓ Token Validation ← Token Exchange ← Authorization Code ↓ MCP Handler ``` **Configuration:** ```bash # Enable OAuth 2.1 authentication AUTH_TYPE=oauth2.1 # Optional: Configure issuer URL (defaults to http://localhost:PORT) OAUTH2_ISSUER_URL=https://your-server.com # Optional: Disable dynamic client registration (enabled by default) OAUTH2_ALLOW_DYNAMIC_REGISTRATION=false # Start server npx mcp-cisco-support --http ``` **Server Output:** ``` 🔐 Authentication Type: OAUTH2.1 📋 OAuth 2.1 Authorization Server Configuration: Issuer: http://localhost:3000 Authorization Endpoint: http://localhost:3000/authorize Token Endpoint: http://localhost:3000/token Registration Endpoint: http://localhost:3000/register Metadata: http://localhost:3000/.well-known/oauth-authorization-server ``` ## OAuth 2.1 Flow ### Complete Flow Diagram ``` ┌─────────┐ ┌──────────┐ ┌─────────┐ │ Client │ │ MCP │ │ User │ │ │ │ Server │ │ │ └────┬────┘ └────┬─────┘ └────┬────┘ │ │ │ │ 1. Register Client (POST /register) │ │ ├───────────────────────────────────────────>│ │ │ │ │ │ 2. Client ID & Secret │ │ │<───────────────────────────────────────────┤ │ │ │ │ │ 3. Generate PKCE (code_verifier/challenge) │ │ │ │ │ │ 4. Authorization Request │ │ │ GET /authorize?code_challenge=... │ │ ├───────────────────────────────────────────>│ │ │ │ │ │ 5. Show Authorization Page │ │ │<───────────────────────────────────────────┤ │ │ │ │ │ 6. User Approves │ │ ├────────────────────────────────────────────┼──────────────────────────────────────────>│ │ │ │ │ 7. Redirect with Authorization Code │ │ │ ?code=abc123&state=... │<──────────────────────────────────────────┤ │<───────────────────────────────────────────┤ │ │ │ │ │ 8. Token Request │ │ │ POST /token (code + code_verifier) │ │ ├───────────────────────────────────────────>│ │ │ │ │ │ 9. Validate PKCE & Issue Access Token │ │ │ (access_token + refresh_token) │ │ │<───────────────────────────────────────────┤ │ │ │ │ │ 10. MCP Request with Access Token │ │ │ Authorization: Bearer <token> │ │ ├───────────────────────────────────────────>│ │ │ │ │ │ 11. MCP Response │ │ │<───────────────────────────────────────────┤ │ ``` ### Step-by-Step Flow #### Step 1: Discover Server Metadata (RFC 8414) **Request:** ```bash curl http://localhost:3000/.well-known/oauth-authorization-server ``` **Response:** ```json { "issuer": "http://localhost:3000", "authorization_endpoint": "http://localhost:3000/authorize", "token_endpoint": "http://localhost:3000/token", "registration_endpoint": "http://localhost:3000/register", "response_types_supported": ["code"], "grant_types_supported": [ "authorization_code", "refresh_token", "client_credentials" ], "code_challenge_methods_supported": ["S256", "plain"], "token_endpoint_auth_methods_supported": [ "client_secret_basic", "client_secret_post", "none" ], "scopes_supported": ["mcp"], "mcp-protocol-version": "2025-06-18" } ``` #### Step 2: Register Client (RFC 7591) **Request:** ```bash curl -X POST http://localhost:3000/register \ -H "Content-Type: application/json" \ -d '{ "redirect_uris": ["http://localhost:3001/callback"], "client_name": "My MCP Client", "grant_types": ["authorization_code"] }' ``` **Response:** ```json { "client_id": "mcp_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "client_secret": "q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2", "redirect_uris": ["http://localhost:3001/callback"], "grant_types": ["authorization_code"], "response_types": ["code"], "client_name": "My MCP Client", "client_id_issued_at": 1698765432, "client_secret_expires_at": 0 } ``` **What the server does:** - Generates unique `client_id` and `client_secret` - Validates redirect URIs are secure (HTTPS or localhost) - Stores client information in memory #### Step 3: Generate PKCE Parameters **Client-Side Code:** ```bash # Generate code_verifier (random string, 43-128 characters) CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=' | tr '+/' '-_') # Example: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" # Generate code_challenge (SHA256 hash of verifier, base64url encoded) CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | \ openssl dgst -binary -sha256 | \ openssl base64 | tr -d '=' | tr '+/' '-_') # Example: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" ``` **JavaScript Example:** ```javascript // Generate code_verifier function generateCodeVerifier() { const array = new Uint8Array(32); crypto.getRandomValues(array); return base64UrlEncode(array); } // Generate code_challenge from verifier async function generateCodeChallenge(verifier) { const encoder = new TextEncoder(); const data = encoder.encode(verifier); const hash = await crypto.subtle.digest('SHA-256', data); return base64UrlEncode(new Uint8Array(hash)); } function base64UrlEncode(array) { return btoa(String.fromCharCode(...array)) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); } ``` #### Step 4: Request Authorization Code **Browser URL:** ``` http://localhost:3000/authorize?response_type=code&client_id=mcp_a1b2c3d4...&redirect_uri=http://localhost:3001/callback&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&state=random_state_value ``` **Query Parameters:** - `response_type=code` - Request authorization code - `client_id` - Your registered client ID - `redirect_uri` - Where to send the user after authorization - `code_challenge` - PKCE challenge derived from code_verifier - `code_challenge_method=S256` - SHA256 hashing method - `state` - Random value to prevent CSRF attacks **Server Response:** HTML authorization page ```html <!DOCTYPE html> <html> <head> <title>MCP Authorization</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; background: #f5f5f5; } .container { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } h1 { color: #333; } .client-info { background: #f9f9f9; padding: 15px; border-radius: 4px; margin: 20px 0; } .button { background: #007bff; color: white; border: none; padding: 12px 24px; border-radius: 4px; cursor: pointer; font-size: 16px; margin-right: 10px; } .button:hover { background: #0056b3; } .button.deny { background: #6c757d; } .button.deny:hover { background: #545b62; } </style> </head> <body> <div class="container"> <h1>🔐 MCP Authorization Request</h1> <div class="client-info"> <p><strong>Client:</strong> My MCP Client</p> <p><strong>Requested Scope:</strong> mcp (default)</p> </div> <p>This application is requesting access to your MCP server.</p> <form method="POST" action="/authorize/approve"> <input type="hidden" name="client_id" value="..."> <input type="hidden" name="redirect_uri" value="..."> <input type="hidden" name="code_challenge" value="..."> <input type="hidden" name="code_challenge_method" value="S256"> <input type="hidden" name="scope" value=""> <input type="hidden" name="state" value="..."> <button type="submit" class="button">Authorize</button> <button type="button" class="button deny" onclick="...">Deny</button> </form> </div> </body> </html> ``` #### Step 5: User Authorization Approval **When user clicks "Authorize":** **Server Actions:** 1. Generates authorization code: `auth_xyz789...` 2. Stores authorization code with PKCE challenge: ```javascript { code: "auth_xyz789...", client_id: "mcp_a1b2c3d4...", redirect_uri: "http://localhost:3001/callback", code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", code_challenge_method: "S256", scope: "mcp", user_id: "default_user", expires_at: Date.now() + 600000, // 10 minutes used: false } ``` 3. Redirects user back to client: ``` Location: http://localhost:3001/callback?code=auth_xyz789...&state=random_state_value ``` #### Step 6: Exchange Code for Access Token **Request:** ```bash curl -X POST http://localhost:3000/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=auth_xyz789..." \ -d "redirect_uri=http://localhost:3001/callback" \ -d "client_id=mcp_a1b2c3d4..." \ -d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" ``` **Server Validation:** 1. Retrieves authorization code 2. Validates code exists and not expired 3. Validates code not already used 4. Validates client_id matches 5. Validates redirect_uri matches 6. **Validates PKCE:** `SHA256(code_verifier) == stored code_challenge` 7. Marks code as used (prevents reuse) **Response:** ```json { "access_token": "token_abc123def456ghi789jkl012mno345pqr678", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "refresh_stu901vwx234yz567abc890def123ghi456", "scope": "mcp" } ``` #### Step 7: Use Access Token for MCP Requests **Request:** ```bash curl -X POST http://localhost:3000/mcp \ -H "Authorization: Bearer token_abc123def456ghi789jkl012mno345pqr678" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "tools/list", "id": 1 }' ``` **Server Validation:** 1. Extracts token from Authorization header 2. Looks up token in storage 3. Checks token exists 4. Checks token not expired 5. Attaches user info to request 6. Processes MCP request #### Step 8: Refresh Token (When Access Token Expires) **Request:** ```bash curl -X POST http://localhost:3000/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "refresh_token=refresh_stu901vwx234yz567abc890def123ghi456" ``` **Server Actions:** 1. Validates refresh token exists 2. Generates new access token and refresh token 3. Deletes old tokens (token rotation) 4. Stores new tokens **Response:** ```json { "access_token": "token_jkl345mno678pqr901stu234vwx567yz890", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "refresh_abc123def456ghi789jkl012mno345pqr678", "scope": "mcp" } ``` ## PKCE Explained ### What is PKCE? **PKCE** (Proof Key for Code Exchange, RFC 7636) prevents authorization code interception attacks. ### The Problem Without PKCE ``` 1. User authorizes application 2. Server redirects: http://client.com/callback?code=AUTH_CODE 3. ❌ Attacker intercepts redirect and steals AUTH_CODE 4. ❌ Attacker exchanges AUTH_CODE for access token 5. ❌ Attacker gains access to user's data ``` ### How PKCE Solves This ``` 1. Client generates random code_verifier (kept secret) 2. Client derives code_challenge = SHA256(code_verifier) 3. Client sends code_challenge in authorization request 4. Server stores code_challenge with authorization code 5. Server redirects with authorization code 6. Attacker may intercept code, but doesn't have code_verifier 7. Client sends code + code_verifier to token endpoint 8. Server validates: SHA256(code_verifier) == stored code_challenge 9. ✅ Only original client can exchange code for token ``` ### PKCE Implementation **Code Verifier Generation:** ```javascript // Must be 43-128 characters, [A-Z][a-z][0-9]-._~ function generateCodeVerifier() { const array = new Uint8Array(32); crypto.getRandomValues(array); return base64UrlEncode(array); // Results in 43 characters } ``` **Code Challenge Generation (S256 Method):** ```javascript async function generateCodeChallenge(verifier) { const encoder = new TextEncoder(); const data = encoder.encode(verifier); const hash = await crypto.subtle.digest('SHA-256', data); return base64UrlEncode(new Uint8Array(hash)); } ``` **Server-Side Validation:** ```typescript function validatePKCE( codeVerifier: string, codeChallenge: string, codeChallengeMethod: string ): boolean { if (codeChallengeMethod === 'S256') { const hash = createHash('sha256') .update(codeVerifier) .digest('base64url'); return hash === codeChallenge; } if (codeChallengeMethod === 'plain') { return codeVerifier === codeChallenge; } return false; } ``` ## Implementation Details ### File Structure #### `src/oauth2.ts` **Core Components:** 1. **Type Definitions** ```typescript interface OAuth2Client { client_id: string; client_secret?: string; redirect_uris: string[]; grant_types: string[]; response_types: string[]; client_name?: string; client_uri?: string; created_at: number; } interface AuthorizationCode { code: string; client_id: string; redirect_uri: string; code_challenge: string; code_challenge_method: string; scope?: string; user_id: string; expires_at: number; used: boolean; } interface AccessToken { access_token: string; token_type: 'Bearer'; expires_in: number; refresh_token?: string; scope?: string; created_at: number; user_id: string; client_id: string; } ``` 2. **Storage (In-Memory)** ```typescript const clients = new Map<string, OAuth2Client>(); const authorizationCodes = new Map<string, AuthorizationCode>(); const accessTokens = new Map<string, AccessToken>(); const refreshTokens = new Map<string, string>(); ``` 3. **Configuration** ```typescript const TOKEN_EXPIRY = 3600; // 1 hour const CODE_EXPIRY = 600; // 10 minutes const REFRESH_TOKEN_EXPIRY = 86400; // 24 hours ``` 4. **Key Functions** | Function | Purpose | RFC | |----------|---------|-----| | `generateAuthorizationServerMetadata()` | Server metadata endpoint | RFC 8414 | | `registerClient()` | Dynamic client registration | RFC 7591 | | `handleAuthorizeRequest()` | Authorization endpoint | RFC 6749 | | `handleAuthorizeApproval()` | Process user approval | - | | `handleTokenRequest()` | Token endpoint | RFC 6749 | | `createOAuth2Middleware()` | Token validation middleware | - | | `validatePKCE()` | PKCE validation | RFC 7636 | | `cleanupExpiredTokens()` | Automatic token cleanup | - | #### `src/sse-server.ts` **Integration Points:** ```typescript // Detect authentication type const authType = (process.env.AUTH_TYPE || 'bearer').toLowerCase(); // OAuth 2.1 configuration const oauth2Config: OAuth2Config = { issuerUrl: process.env.OAUTH2_ISSUER_URL || `http://localhost:${port}`, allowDynamicRegistration: process.env.OAUTH2_ALLOW_DYNAMIC_REGISTRATION !== 'false', requirePKCE: true }; // Register OAuth 2.1 endpoints if (enableAuth && authType === 'oauth2.1') { app.get('/.well-known/oauth-authorization-server', ...); app.post('/register', ...); app.get('/authorize', ...); app.post('/authorize/approve', ...); app.post('/token', ...); } // Apply appropriate middleware if (authType === 'oauth2.1') { app.use(createOAuth2Middleware()); } else { app.use(createAuthMiddleware(authToken, enableAuth)); } ``` ### OAuth 2.1 Endpoints | Endpoint | Method | Purpose | Public | |----------|--------|---------|--------| | `/.well-known/oauth-authorization-server` | GET | Server metadata (RFC 8414) | ✅ Yes | | `/register` | POST | Client registration (RFC 7591) | ✅ Yes | | `/authorize` | GET | Authorization request | ✅ Yes | | `/authorize/approve` | POST | User approval | ✅ Yes | | `/token` | POST | Token exchange/refresh | ✅ Yes | | `/mcp` | POST/GET/DELETE | MCP endpoints | ❌ No (requires auth) | | `/sse` | GET | Legacy SSE | ❌ No (requires auth) | | `/messages` | POST | Legacy messages | ❌ No (requires auth) | | `/health` | GET | Health check | ✅ Yes | | `/` | GET | Server info | ✅ Yes | ## Configuration ### Environment Variables ```bash # ============================================ # Authentication Type (NEW) # ============================================ AUTH_TYPE=bearer # Default: Simple bearer token AUTH_TYPE=oauth2.1 # Full OAuth 2.1 authorization server # ============================================ # Bearer Token Configuration (Existing) # ============================================ MCP_BEARER_TOKEN=custom_token_here # Use specific bearer token # If not set, random token is generated # ============================================ # OAuth 2.1 Configuration (NEW) # ============================================ # Issuer URL (defaults to http://localhost:PORT) OAUTH2_ISSUER_URL=https://api.example.com # Allow dynamic client registration (defaults to true) OAUTH2_ALLOW_DYNAMIC_REGISTRATION=true # Enable registration endpoint OAUTH2_ALLOW_DYNAMIC_REGISTRATION=false # Disable registration endpoint # ============================================ # Disable Authentication (Existing) # ============================================ DANGEROUSLY_OMIT_AUTH=true # ⚠️ Disables all HTTP authentication # Only for development/testing # ============================================ # Cisco API Configuration (Existing) # ============================================ CISCO_CLIENT_ID=your_cisco_client_id CISCO_CLIENT_SECRET=your_cisco_client_secret SUPPORT_API=enhanced_analysis # ============================================ # Server Configuration (Existing) # ============================================ PORT=3000 NODE_ENV=development ``` ### Example Configurations #### Development (Bearer Token) ```bash # .env AUTH_TYPE=bearer MCP_BEARER_TOKEN=dev_token_12345 PORT=3000 CISCO_CLIENT_ID=your_id CISCO_CLIENT_SECRET=your_secret ``` #### Production (OAuth 2.1) ```bash # .env AUTH_TYPE=oauth2.1 OAUTH2_ISSUER_URL=https://api.mycompany.com OAUTH2_ALLOW_DYNAMIC_REGISTRATION=true PORT=443 NODE_ENV=production CISCO_CLIENT_ID=your_id CISCO_CLIENT_SECRET=your_secret ``` #### Testing (No Auth) ```bash # .env DANGEROUSLY_OMIT_AUTH=true PORT=3000 CISCO_CLIENT_ID=your_id CISCO_CLIENT_SECRET=your_secret ``` ## Security Features ### 1. PKCE Mandatory **Protection:** Prevents authorization code interception attacks **Implementation:** ```typescript // Authorization request MUST include code_challenge if (!code_challenge) { return res.status(400).json({ error: 'invalid_request', error_description: 'PKCE code_challenge is required' }); } // Token request MUST include code_verifier if (!code_verifier) { return res.status(400).json({ error: 'invalid_request', error_description: 'PKCE code_verifier is required' }); } // Server validates PKCE if (!validatePKCE(code_verifier, authCode.code_challenge, authCode.code_challenge_method)) { return res.status(400).json({ error: 'invalid_grant', error_description: 'PKCE validation failed' }); } ``` ### 2. Secure Redirect URI Validation **Protection:** Prevents open redirect attacks **Implementation:** ```typescript function isSecureRedirectUri(uri: string): boolean { try { const parsed = new URL(uri); // Allow localhost for development, require HTTPS otherwise return parsed.protocol === 'https:' || parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1'; } catch { return false; } } // Exact match required function validateRedirectUri(uri: string, registeredUris: string[]): boolean { return registeredUris.includes(uri); } ``` ### 3. Short-Lived Tokens **Protection:** Limits damage if tokens are compromised **Token Lifespans:** - Authorization codes: 10 minutes (single-use) - Access tokens: 1 hour - Refresh tokens: 24 hours **Implementation:** ```typescript const TOKEN_EXPIRY = 3600; // 1 hour const CODE_EXPIRY = 600; // 10 minutes const REFRESH_TOKEN_EXPIRY = 86400; // 24 hours // Check if token is expired const expiresAt = accessToken.created_at + (accessToken.expires_in * 1000); if (Date.now() > expiresAt) { accessTokens.delete(token); return res.status(401).json({ error: 'invalid_token', error_description: 'Access token expired' }); } ``` ### 4. Token Rotation **Protection:** Prevents refresh token reuse attacks **Implementation:** ```typescript // Refresh token flow const oldAccessToken = refreshTokens.get(refresh_token); const oldToken = accessTokens.get(oldAccessToken); // Generate new tokens const new_access_token = randomUUID().replace(/-/g, ''); const new_refresh_token = randomUUID().replace(/-/g, ''); // Remove old tokens (rotation) accessTokens.delete(oldAccessToken); refreshTokens.delete(refresh_token); // Store new tokens accessTokens.set(new_access_token, newToken); refreshTokens.set(new_refresh_token, new_access_token); ``` ### 5. One-Time Authorization Codes **Protection:** Prevents authorization code reuse **Implementation:** ```typescript // Check if code was already used if (authCode.used) { authorizationCodes.delete(code); return res.status(400).json({ error: 'invalid_grant', error_description: 'Authorization code already used' }); } // Mark code as used after successful token exchange authCode.used = true; ``` ### 6. Automatic Token Cleanup **Protection:** Removes expired tokens from memory **Implementation:** ```typescript export function cleanupExpiredTokens(): void { const now = Date.now(); // Clean up expired authorization codes for (const [code, authCode] of authorizationCodes.entries()) { if (now > authCode.expires_at || authCode.used) { authorizationCodes.delete(code); } } // Clean up expired access tokens for (const [token, accessToken] of accessTokens.entries()) { const expiresAt = accessToken.created_at + (accessToken.expires_in * 1000); if (now > expiresAt) { accessTokens.delete(token); if (accessToken.refresh_token) { refreshTokens.delete(accessToken.refresh_token); } } } } // Run cleanup every 5 minutes setInterval(cleanupExpiredTokens, 5 * 60 * 1000); ``` ### 7. State Parameter Support **Protection:** Prevents CSRF attacks **Implementation:** ```typescript // Authorization request includes state const { state } = req.query; // Redirect includes state const redirectUrl = new URL(redirect_uri); redirectUrl.searchParams.set('code', code); if (state) { redirectUrl.searchParams.set('state', state); } // Client validates state matches original request ``` ## Usage Examples ### Example 1: Simple CLI Tool (Bearer Token) **Server Setup:** ```bash # Start server with bearer token auth export MCP_BEARER_TOKEN=my_cli_token_123 export CISCO_CLIENT_ID=your_cisco_id export CISCO_CLIENT_SECRET=your_cisco_secret npx mcp-cisco-support --http ``` **Client Code (Python):** ```python import requests MCP_SERVER = "http://localhost:3000" BEARER_TOKEN = "my_cli_token_123" def call_mcp_tool(tool_name, arguments): headers = { "Authorization": f"Bearer {BEARER_TOKEN}", "Content-Type": "application/json" } payload = { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": tool_name, "arguments": arguments }, "id": 1 } response = requests.post(f"{MCP_SERVER}/mcp", json=payload, headers=headers) return response.json() # Example: Search for bugs result = call_mcp_tool("search_bugs_by_keyword", { "keyword": "memory leak", "severity": "3", "page_index": 1 }) print(result) ``` ### Example 2: Web Application (OAuth 2.1) **Server Setup:** ```bash # Start server with OAuth 2.1 auth export AUTH_TYPE=oauth2.1 export OAUTH2_ISSUER_URL=https://api.myapp.com export CISCO_CLIENT_ID=your_cisco_id export CISCO_CLIENT_SECRET=your_cisco_secret npx mcp-cisco-support --http ``` **Client Code (JavaScript/Express):** ```javascript const express = require('express'); const session = require('express-session'); const crypto = require('crypto'); const fetch = require('node-fetch'); const app = express(); const MCP_SERVER = 'https://api.myapp.com'; app.use(session({ secret: 'your-session-secret', resave: false, saveUninitialized: true })); // Home page - start OAuth flow app.get('/', (req, res) => { if (req.session.accessToken) { res.send(` <h1>Connected to MCP Server</h1> <p>Access Token: ${req.session.accessToken.substring(0, 20)}...</p> <a href="/search">Search Bugs</a> | <a href="/logout">Logout</a> `); } else { res.send(` <h1>MCP Client Example</h1> <a href="/login">Connect to MCP Server</a> `); } }); // Step 1: Register client (do this once, store credentials) let CLIENT_ID = process.env.MCP_CLIENT_ID; let CLIENT_SECRET = process.env.MCP_CLIENT_SECRET; if (!CLIENT_ID) { // Register client fetch(`${MCP_SERVER}/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ redirect_uris: ['http://localhost:3001/callback'], client_name: 'My Web App', grant_types: ['authorization_code', 'refresh_token'] }) }) .then(res => res.json()) .then(data => { CLIENT_ID = data.client_id; CLIENT_SECRET = data.client_secret; console.log('Client registered:', CLIENT_ID); }); } // Step 2 & 3: Generate PKCE and redirect to authorization app.get('/login', (req, res) => { // Generate PKCE parameters const codeVerifier = crypto.randomBytes(32).toString('base64url'); const codeChallenge = crypto .createHash('sha256') .update(codeVerifier) .digest('base64url'); // Generate state for CSRF protection const state = crypto.randomBytes(16).toString('hex'); // Store in session req.session.codeVerifier = codeVerifier; req.session.state = state; // Redirect to authorization endpoint const authUrl = new URL(`${MCP_SERVER}/authorize`); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('client_id', CLIENT_ID); authUrl.searchParams.set('redirect_uri', 'http://localhost:3001/callback'); authUrl.searchParams.set('code_challenge', codeChallenge); authUrl.searchParams.set('code_challenge_method', 'S256'); authUrl.searchParams.set('state', state); authUrl.searchParams.set('scope', 'mcp'); res.redirect(authUrl.toString()); }); // Step 5 & 6: Handle callback and exchange code for token app.get('/callback', async (req, res) => { const { code, state } = req.query; // Validate state if (state !== req.session.state) { return res.status(400).send('Invalid state parameter'); } // Exchange code for access token const tokenResponse = await fetch(`${MCP_SERVER}/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code: code, redirect_uri: 'http://localhost:3001/callback', client_id: CLIENT_ID, code_verifier: req.session.codeVerifier }) }); const tokens = await tokenResponse.json(); // Store tokens in session req.session.accessToken = tokens.access_token; req.session.refreshToken = tokens.refresh_token; req.session.tokenExpiry = Date.now() + (tokens.expires_in * 1000); // Clear PKCE parameters delete req.session.codeVerifier; delete req.session.state; res.redirect('/'); }); // Step 7: Use access token for MCP requests app.get('/search', async (req, res) => { if (!req.session.accessToken) { return res.redirect('/login'); } // Check if token expired, refresh if needed if (Date.now() > req.session.tokenExpiry) { const refreshResponse = await fetch(`${MCP_SERVER}/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: req.session.refreshToken }) }); const tokens = await refreshResponse.json(); req.session.accessToken = tokens.access_token; req.session.refreshToken = tokens.refresh_token; req.session.tokenExpiry = Date.now() + (tokens.expires_in * 1000); } // Make MCP request const mcpResponse = await fetch(`${MCP_SERVER}/mcp`, { method: 'POST', headers: { 'Authorization': `Bearer ${req.session.accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', method: 'tools/call', params: { name: 'search_bugs_by_keyword', arguments: { keyword: 'memory leak', severity: '3', page_index: 1 } }, id: 1 }) }); const result = await mcpResponse.json(); res.json(result); }); // Logout app.get('/logout', (req, res) => { req.session.destroy(); res.redirect('/'); }); app.listen(3001, () => { console.log('Client app running on http://localhost:3001'); }); ``` ### Example 3: Service Account (Client Credentials) **Server Setup:** ```bash export AUTH_TYPE=oauth2.1 npx mcp-cisco-support --http ``` **Register Service Account:** ```bash # Register service account client curl -X POST http://localhost:3000/register \ -H "Content-Type: application/json" \ -d '{ "client_name": "Monitoring Service", "grant_types": ["client_credentials"] }' # Response: # { # "client_id": "mcp_service123...", # "client_secret": "secret456...", # ... # } ``` **Client Code (Bash):** ```bash #!/bin/bash CLIENT_ID="mcp_service123..." CLIENT_SECRET="secret456..." MCP_SERVER="http://localhost:3000" # Get access token using client credentials TOKEN_RESPONSE=$(curl -s -X POST "$MCP_SERVER/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=$CLIENT_ID" \ -d "client_secret=$CLIENT_SECRET") ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token') # Use access token for MCP requests curl -X POST "$MCP_SERVER/mcp" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "tools/list", "id": 1 }' ``` ## When to Use Each Mode ### Decision Matrix | Scenario | Bearer Token | OAuth 2.1 | Rationale | |----------|--------------|-----------|-----------| | **Single user/application** | ✅ Recommended | ❌ Overkill | Simple auth sufficient | | **Development & testing** | ✅ Recommended | ⚠️ Optional | Quick setup, easy debugging | | **CLI tools** | ✅ Recommended | ❌ Not suitable | No user interaction | | **Internal services** | ✅ Recommended | ⚠️ Optional | Simple, fast, no user consent needed | | **Multi-user web app** | ❌ Limited | ✅ Recommended | Need user consent, token refresh | | **Third-party integrations** | ❌ Insecure | ✅ Recommended | OAuth standard for delegated access | | **Enterprise deployment** | ⚠️ Acceptable | ✅ Recommended | OAuth 2.1 compliance often required | | **Mobile app** | ❌ Not ideal | ✅ Recommended | PKCE prevents token interception | | **Public API** | ❌ Not suitable | ✅ Recommended | Need granular access control | | **Service-to-service** | ✅ Recommended | ⚠️ Optional | Client credentials flow works for both | ### Use Bearer Token When: ✅ **Single Application/User** - You control both client and server - No need for user consent - Simple deployment requirements ✅ **Development & Testing** - Quick setup needed - Iterating rapidly - Debugging authentication issues ✅ **CLI Tools** - No user browser interaction - Token stored in config file or environment ✅ **Internal Services** - Running within secure network - Service-to-service communication - No external access ✅ **Simple Deployments** - Minimal infrastructure - No need for token refresh - Static authentication sufficient ### Use OAuth 2.1 When: ✅ **Multi-User Applications** - Multiple users need access - Each user has own credentials - User consent required ✅ **Third-Party Integrations** - External applications need access - Delegated authorization needed - Standard OAuth flow expected ✅ **Web Applications** - Browser-based clients - Need token refresh capability - Session management required ✅ **Mobile Applications** - PKCE prevents code interception - Token refresh for long sessions - Standard mobile auth pattern ✅ **Enterprise Deployments** - OAuth 2.1 compliance mandated - Integration with corporate SSO - Audit trail requirements ✅ **Public APIs** - Open to external developers - Need rate limiting per client - Granular access control ### Comparison Table | Feature | Bearer Token | OAuth 2.1 | |---------|-------------|-----------| | **Setup Complexity** | ⭐ Simple | ⭐⭐⭐ Complex | | **User Interaction** | None | Required (authorization flow) | | **Token Management** | Manual rotation | Automatic refresh | | **Multi-User Support** | ⭐ Limited | ⭐⭐⭐ Full | | **Security Level** | ⭐⭐ Good | ⭐⭐⭐ Excellent | | **Client Types** | Trusted clients | Public & confidential clients | | **User Consent** | ❌ No | ✅ Yes | | **Token Expiry** | Static | Dynamic (with refresh) | | **Standards Compliance** | Basic auth | Full OAuth 2.1 | | **Audit Trail** | Basic | Comprehensive | | **Revocation** | Manual | Per-user/per-client | | **Best For** | Simple, trusted environments | Multi-user, public APIs | | **Default** | ✅ Yes | ❌ No | ## Production Considerations ### Current Implementation Limitations The current implementation uses **in-memory storage** which has these limitations: ❌ **Data Loss on Restart** - All clients, tokens, and codes lost when server restarts - Users must re-authorize after deployment ❌ **No Horizontal Scaling** - Cannot run multiple server instances - Each instance has separate storage ❌ **Memory Leaks** - Without external persistence, old tokens accumulate - Automatic cleanup helps but not perfect ### Production Recommendations #### 1. Use Persistent Storage **Replace in-memory Maps with Redis or Database:** ```typescript // Instead of: const clients = new Map<string, OAuth2Client>(); const accessTokens = new Map<string, AccessToken>(); // Use Redis: import Redis from 'ioredis'; const redis = new Redis(); // Store client await redis.set(`client:${client_id}`, JSON.stringify(client)); await redis.expire(`client:${client_id}`, 86400 * 365); // 1 year // Store access token with auto-expiry await redis.set(`token:${access_token}`, JSON.stringify(tokenData)); await redis.expire(`token:${access_token}`, TOKEN_EXPIRY); // Retrieve token const tokenJson = await redis.get(`token:${access_token}`); const token = tokenJson ? JSON.parse(tokenJson) : null; ``` #### 2. Implement Rate Limiting **Protect endpoints from abuse:** ```typescript import rateLimit from 'express-rate-limit'; // Rate limit token endpoint const tokenLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 10, // 10 requests per window message: 'Too many token requests, please try again later', standardHeaders: true, legacyHeaders: false, }); app.post('/token', tokenLimiter, (req, res) => { handleTokenRequest(req, res); }); ``` #### 3. Add Proper User Management **Implement real user authentication:** ```typescript // Instead of hardcoded user user_id: 'default_user' // Use actual user authentication import passport from 'passport'; app.get('/authorize', passport.authenticate('local', { session: true }), (req, res) => { // req.user contains authenticated user handleAuthorizeRequest(req, res, req.user); } ); ``` #### 4. Implement Audit Logging **Track all OAuth operations:** ```typescript import winston from 'winston'; const logger = winston.createLogger({ level: 'info', format: winston.format.json(), transports: [ new winston.transports.File({ filename: 'oauth-audit.log' }) ] }); // Log all OAuth events logger.info('Client registered', { client_id, client_name, redirect_uris, timestamp: new Date().toISOString(), ip: req.ip }); logger.info('Authorization granted', { client_id, user_id, scope, timestamp: new Date().toISOString() }); logger.info('Access token issued', { client_id, user_id, token_id: access_token.substring(0, 10), expires_in: TOKEN_EXPIRY, timestamp: new Date().toISOString() }); ``` #### 5. Use HTTPS in Production **Enforce TLS for all communications:** ```typescript import https from 'https'; import fs from 'fs'; const options = { key: fs.readFileSync('server-key.pem'), cert: fs.readFileSync('server-cert.pem') }; https.createServer(options, app).listen(443); // Redirect HTTP to HTTPS app.use((req, res, next) => { if (req.protocol !== 'https') { return res.redirect(`https://${req.hostname}${req.url}`); } next(); }); ``` #### 6. Implement Token Revocation **Allow users to revoke tokens:** ```typescript // Revocation endpoint (RFC 7009) app.post('/revoke', async (req, res) => { const { token, token_type_hint } = req.body; // Remove from storage if (token_type_hint === 'access_token') { await redis.del(`token:${token}`); } else if (token_type_hint === 'refresh_token') { const access_token = await redis.get(`refresh:${token}`); await redis.del(`token:${access_token}`); await redis.del(`refresh:${token}`); } logger.info('Token revoked', { token_type_hint }); res.status(200).json({ success: true }); }); ``` #### 7. Add Scope Support **Implement granular permissions:** ```typescript // Define scopes const SCOPES = { 'mcp:read': 'Read MCP tools and resources', 'mcp:write': 'Execute MCP tools', 'mcp:admin': 'Administer MCP server' }; // Check scopes in middleware function requireScope(...requiredScopes: string[]) { return (req, res, next) => { const userScopes = req.oauth2.scope?.split(' ') || []; const hasRequired = requiredScopes.every(s => userScopes.includes(s)); if (!hasRequired) { return res.status(403).json({ error: 'insufficient_scope', error_description: 'Required scopes not granted' }); } next(); }; } // Protect endpoints app.post('/mcp', requireScope('mcp:write'), ...); ``` #### 8. Monitor and Alert **Set up monitoring for OAuth operations:** ```typescript import prometheus from 'prom-client'; // Metrics const tokenIssued = new prometheus.Counter({ name: 'oauth_tokens_issued_total', help: 'Total number of access tokens issued' }); const tokenValidation = new prometheus.Counter({ name: 'oauth_token_validations_total', help: 'Total number of token validations', labelNames: ['result'] // 'success' or 'failure' }); // Instrument code tokenIssued.inc(); tokenValidation.labels('success').inc(); // Expose metrics app.get('/metrics', async (req, res) => { res.set('Content-Type', prometheus.register.contentType); res.end(await prometheus.register.metrics()); }); ``` ### Production Deployment Checklist - [ ] **Replace in-memory storage with Redis/Database** - [ ] **Implement proper user authentication** - [ ] **Enable HTTPS/TLS** - [ ] **Add rate limiting on all OAuth endpoints** - [ ] **Implement comprehensive audit logging** - [ ] **Set up monitoring and alerting** - [ ] **Add token revocation endpoint** - [ ] **Implement scope-based access control** - [ ] **Configure CORS properly** - [ ] **Set secure cookie flags** - [ ] **Implement CSRF protection** - [ ] **Add security headers (already done via Helmet)** - [ ] **Regular security audits** - [ ] **Backup and recovery procedures** - [ ] **Load balancing strategy** - [ ] **Disaster recovery plan** ## Troubleshooting ### Common Issues #### Issue 1: "PKCE validation failed" **Cause:** code_verifier doesn't match code_challenge **Solution:** ```bash # Ensure code_verifier is stored correctly # Verify code_challenge generation: CODE_VERIFIER="dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | \ openssl dgst -binary -sha256 | \ openssl base64 | tr -d '=' | tr '+/' '-_') echo "Verifier: $CODE_VERIFIER" echo "Challenge: $CODE_CHALLENGE" ``` #### Issue 2: "Invalid redirect_uri" **Cause:** Redirect URI doesn't match registered URIs **Solution:** - Ensure exact match (including trailing slash) - Use same protocol (http/https) - Use same port number ```bash # Registered: http://localhost:3001/callback # Request: http://localhost:3001/callback ✅ # Request: http://localhost:3001/callback/ ❌ (trailing slash) # Request: https://localhost:3001/callback ❌ (different protocol) ``` #### Issue 3: "Authorization code expired" **Cause:** More than 10 minutes elapsed **Solution:** - Complete token exchange within 10 minutes - Restart authorization flow if expired #### Issue 4: "Access token expired" **Cause:** Token older than 1 hour **Solution:** - Use refresh token to get new access token ```bash curl -X POST http://localhost:3000/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "refresh_token=YOUR_REFRESH_TOKEN" ``` #### Issue 5: "Invalid client" **Cause:** Client not registered or wrong credentials **Solution:** - Register client first - Verify client_id and client_secret - Check client exists in storage ### Debug Mode **Enable detailed logging:** ```bash # Set environment variable DEBUG=oauth:* npx mcp-cisco-support --http # Or modify code temporarily logger.level = 'debug'; ``` **Check server logs:** ```bash # Look for OAuth events grep "OAuth" /var/log/mcp-server.log # Check token validation grep "token validation" /var/log/mcp-server.log ``` ## References ### RFCs and Standards - **[RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749)** - The OAuth 2.0 Authorization Framework - **[RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)** - Proof Key for Code Exchange (PKCE) - **[RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414)** - OAuth 2.0 Authorization Server Metadata - **[RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591)** - OAuth 2.0 Dynamic Client Registration - **[RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009)** - OAuth 2.0 Token Revocation - **[OAuth 2.1 Draft](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-09)** - OAuth 2.1 Authorization Framework ### MCP Specifications - **[MCP Authorization Specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization)** - MCP OAuth 2.1 Authorization - **[MCP Protocol](https://modelcontextprotocol.io/)** - Model Context Protocol Documentation ### Additional Resources - **[OAuth 2.0 Security Best Practices](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics)** - Security recommendations - **[OAuth 2.0 for Native Apps](https://datatracker.ietf.org/doc/html/rfc8252)** - Mobile and desktop apps - **[PKCE Explained](https://www.oauth.com/oauth2-servers/pkce/)** - Visual guide to PKCE --- **Document Version:** 1.0 **Last Updated:** 2025-01-30 **MCP Server Version:** 1.17.0+ **Author:** Claude (Anthropic)

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sieteunoseis/mcp-cisco-support'

If you have feedback or need assistance with the MCP directory API, please join our Discord server