Generate cryptographically random 43-128 character string using unreserved chars [A-Za-z0-9-._~]. Pattern in Node.js: const crypto = require('crypto'); const codeVerifier = crypto.randomBytes(32).toString('base64url'). This creates 43-character base64url string (32 bytes = 43 base64url chars). Requirements: Minimum 43 characters, maximum 128 characters, use crypto.randomBytes NOT Math.random() (not cryptographically secure). Unreserved chars only: A-Z, a-z, 0-9, hyphen, period, underscore, tilde. Store verifier securely for later token exchange. Never send verifier to authorization server - only send code_challenge derived from verifier.
MCP OAuth Implementation FAQ & Answers
8 expert MCP OAuth Implementation answers researched from official documentation. Every answer cites authoritative sources you can verify.
unknown
8 questionsUse S256 method (SHA-256 hash then base64url encode). Pattern: const crypto = require('crypto'); const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url'). Steps: (1) Hash verifier with SHA-256, (2) Base64url encode hash (not standard base64). S256 is REQUIRED, plain method is insecure. Plain method: code_challenge = code_verifier (DO NOT USE - only for clients that can't do SHA-256). Send to auth server: code_challenge + code_challenge_method: 'S256' in authorization request. Server stores challenge, verifies verifier matches during token exchange. This prevents authorization code interception attacks.
Required parameters: response_type=code, client_id, redirect_uri, code_challenge, code_challenge_method=S256, scope. Optional but recommended: state (CSRF protection). Full URL: https://auth.example.com/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https://yourapp.com/callback&code_challenge=CHALLENGE&code_challenge_method=S256&scope=read write&state=RANDOM_STATE. State parameter: Generate random string, store in session, verify matches in callback. Scope: Space-separated permissions (read, write, admin). Redirect URI must exactly match registered URI (no wildcards). User grants consent, server redirects to redirect_uri?code=AUTH_CODE&state=RANDOM_STATE.
POST to token endpoint with grant_type=authorization_code, code, redirect_uri, client_id, code_verifier. Pattern: fetch('https://auth.example.com/token', {method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: new URLSearchParams({grant_type: 'authorization_code', code: authCode, redirect_uri: redirectUri, client_id: clientId, code_verifier: codeVerifier})}). Response: {access_token, token_type: 'Bearer', expires_in: 3600, refresh_token, scope}. Server verifies: (1) Code is valid and not expired, (2) SHA-256(code_verifier) matches stored code_challenge, (3) redirect_uri matches. Store access_token securely, never log it. Use for API requests: Authorization: Bearer ACCESS_TOKEN.
Store in encrypted session storage or secure key-value store, NEVER in logs or client-visible locations. Options: (1) Server-side session: express-session with Redis backend, encrypt session data. (2) Encrypted database: Store with AES-256 encryption, decrypt only when needed. (3) Memory cache: For short-lived servers, store in Map with session ID key. (4) Secure cookies: HttpOnly, Secure, SameSite=Strict flags, encrypt cookie value. Anti-patterns: Plain text files, environment variables (static), local storage (client-side), logs. Pattern: const encryptedToken = encrypt(accessToken, SECRET_KEY); sessionStore.set(sessionId, {token: encryptedToken, expiresAt}). Rotate encryption keys quarterly.
Automatically refresh before expiration using refresh_token. Pattern: Check if token expires in <5 minutes, refresh proactively. Code: if (tokenExpiresAt - Date.now() < 5 * 60 * 1000) {await refreshToken();}. Refresh request: POST to token endpoint with {grant_type: 'refresh_token', refresh_token: refreshToken, client_id: clientId}. Response: New access_token + new refresh_token (invalidates old tokens). Store new tokens, update expiresAt. Handle refresh failures: (1) Retry once with exponential backoff, (2) If fails, redirect user to re-authorize. Never queue multiple simultaneous refreshes - use mutex/lock pattern: if (!refreshing) {refreshing = fetchNewToken();}. Return existing promise if refresh in progress.
5 critical validations: (1) State parameter: Generate random state, verify matches in callback (prevents CSRF). (2) PKCE verification: Verify SHA-256(code_verifier) === stored code_challenge. (3) Redirect URI validation: Exact match only, no wildcards or substrings. (4) Authorization code expiration: Codes valid for 60 seconds max, single-use only. (5) TLS enforcement: All OAuth endpoints MUST use HTTPS (no HTTP). Pattern: if (receivedState !== storedState) throw new Error('Invalid state'). Also validate: Token signature (if JWT), audience claim matches your service, issuer claim matches OAuth provider. Log failed validations for security monitoring. Reject bearer tokens in URL query params (use Authorization header only).
MCP OAuth consent follows June 2025 spec updates. Flow: (1) Client connects to protected MCP server. (2) Server returns 401 Unauthorized with WWW-Authenticate header pointing to Authorization Server. (3) Client redirects user to auth server with Resource Indicators (RFC 8707) specifying MCP server audience. (4) User sees consent prompt: 'Agent wants access to: [scoped tools/capabilities]'. (5) User approves, auth server issues token scoped to specific MCP server. (6) Client presents token via Authorization: Bearer header. Security: PKCE mandatory, Resource Indicators prevent token misuse across servers. MCP servers act as OAuth Resource Servers. Proxy servers MUST obtain consent per dynamically registered client. Scope design: tools:search, tools:discover (granular, not generic read/write). Revocation UI: let users view/revoke grants. Re-consent after 90 days recommended.