Claude Skills Guide

MCP OAuth 2.1 Authentication Implementation Guide

OAuth 2.1 represents the latest evolution in authorization protocols, consolidating best practices from OAuth 2.0 and its extensions. When implementing authentication for Model Context Protocol (MCP) servers, understanding OAuth 2.1 principles helps you build secure, standards-compliant systems. This guide walks through practical implementation patterns for MCP authentication.

What OAuth 2.1 Brings to MCP

The MCP protocol enables AI assistants like Claude to interact with external tools and services through a standardized interface. When your MCP server handles sensitive data or provides access to protected resources, proper authentication becomes essential. OAuth 2.1 simplifies the security model by requiring PKCE (Proof Key for Code Exchange) for all grant types, mandating secure token storage, and deprecating insecure practices.

If you are building MCP servers that need authentication, the mcp-builder skill provides foundational patterns for server architecture. Combine that with proper OAuth 2.1 implementation for production deployments.

Implementing the Authorization Code Flow with PKCE

The recommended flow for MCP authentication uses Authorization Code grant with PKCE. This pattern protects against authorization code interception attacks and works well for server-to-server and client applications.

Server-Side Implementation

# Simplified OAuth 2.1 Authorization Code + PKCE flow
import secrets
import hashlib
import base64

class MCPAuthorizationServer:
    def __init__(self, client_id, client_secret, redirect_uri):
        self.client_id = client_id
        self.client_secret = client_secret
        self.redirect_uri = redirect_uri
        self.authorized_codes = {}
        self.access_tokens = {}
    
    def generate_code_verifier(self):
        return secrets.token_urlsafe(64)
    
    def generate_code_challenge(self, verifier):
        digest = hashlib.sha256(verifier.encode()).digest()
        return base64.urlsafe_b64encode(digest).decode().rstrip('=')
    
    def authorization_endpoint(self, client_id, redirect_uri, scope, state):
        code_verifier = self.generate_code_verifier()
        code_challenge = self.generate_code_challenge(code_verifier)
        
        auth_code = secrets.token_urlsafe(32)
        self.authorized_codes[auth_code] = {
            'client_id': client_id,
            'redirect_uri': redirect_uri,
            'scope': scope,
            'code_verifier': code_verifier,
            'state': state
        }
        
        return auth_code, code_challenge
    
    def token_endpoint(self, auth_code, code_verifier, client_id):
        auth_data = self.authorized_codes.get(auth_code)
        
        if not auth_data:
            raise ValueError("Invalid authorization code")
        
        if auth_data['client_id'] != client_id:
            raise ValueError("Client ID mismatch")
        
        if auth_data['code_verifier'] != code_verifier:
            raise ValueError("Invalid code verifier - possible PKCE attack")
        
        access_token = secrets.token_urlsafe(32)
        self.access_tokens[access_token] = {
            'client_id': client_id,
            'scope': auth_data['scope']
        }
        
        return {
            'access_token': access_token,
            'token_type': 'Bearer',
            'expires_in': 3600
        }

Client-Side MCP Integration

// MCP client requesting authenticated resources
class MCPAuthenticatedClient {
    constructor(serverUrl, clientId, clientSecret) {
        this.serverUrl = serverUrl;
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.accessToken = null;
    }
    
    async initiateAuth() {
        const codeVerifier = this.generateCodeVerifier();
        const codeChallenge = this.generateCodeChallenge(codeVerifier);
        
        const authUrl = new URL(`${this.serverUrl}/authorize`);
        authUrl.searchParams.set('client_id', this.clientId);
        authUrl.searchParams.set('redirect_uri', 'mcp://auth/callback');
        authUrl.searchParams.set('response_type', 'code');
        authUrl.searchParams.set('code_challenge', codeChallenge);
        authUrl.searchParams.set('code_challenge_method', 'S256');
        authUrl.searchParams.set('scope', 'mcp:read mcp:write');
        
        // Store verifier for token exchange
        sessionStorage.setItem('pkce_verifier', codeVerifier);
        
        return authUrl.toString();
    }
    
    async exchangeCodeForToken(authCode) {
        const codeVerifier = sessionStorage.getItem('pkce_verifier');
        
        const response = await fetch(`${this.serverUrl}/token`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: new URLSearchParams({
                grant_type: 'authorization_code',
                code: authCode,
                redirect_uri: 'mcp://auth/callback',
                client_id: this.clientId,
                code_verifier: codeVerifier
            })
        });
        
        const tokens = await response.json();
        this.accessToken = tokens.access_token;
        return this.accessToken;
    }
    
    generateCodeVerifier() {
        const array = new Uint8Array(32);
        crypto.getRandomValues(array);
        return btoa(String.fromCharCode.apply(null, array))
            .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
    }
    
    generateCodeChallenge(verifier) {
        const encoder = new TextEncoder();
        const data = encoder.encode(verifier);
        return btoa(String.fromCharCode.apply(null, new Uint8Array(
            crypto.subtle.digest('SHA-256', data)
        ))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
    }
}

Token Refresh and Security Best Practices

OAuth 2.1 requires short-lived access tokens with refresh token rotation. Implement proper token lifecycle management:

class TokenManager:
    def __init__(self):
        self.refresh_tokens = {}
    
    def rotate_refresh_token(self, old_token):
        if old_token not in self.refresh_tokens:
            raise ValueError("Invalid refresh token")
        
        new_refresh_token = secrets.token_urlsafe(48)
        token_data = self.refresh_tokens[old_token]
        
        del self.refresh_tokens[old_token]
        self.refresh_tokens[new_refresh_token] = {
            'client_id': token_data['client_id'],
            'issued_at': datetime.now(),
            'rotation_count': token_data.get('rotation_count', 0) + 1
        }
        
        return new_refresh_token
    
    def is_token_compromised(self, token_data):
        rotation_count = token_data.get('rotation_count', 0)
        issued_at = token_data.get('issued_at')
        
        if rotation_count > 1:
            return True
        
        if issued_at:
            age = (datetime.now() - issued_at).total_seconds()
            if age > 86400 * 30:
                return True
        
        return False

Integrating with MCP Server Architecture

When building MCP servers that require OAuth authentication, structure your server to validate tokens on each request:

# MCP server with OAuth validation
class MCPServer:
    def __init__(self, token_validator):
        self.token_validator = token_validator
    
    async def handle_request(self, request, tool_name, tool_args):
        auth_header = request.headers.get('Authorization')
        
        if not auth_header or not auth_header.startswith('Bearer '):
            return {'error': 'Missing or invalid authorization header'}
        
        token = auth_header[7:]
        
        try:
            token_data = await self.token_validator.validate(token)
        except TokenValidationError as e:
            return {'error': str(e), 'code': 'UNAUTHORIZED'}
        
        if not self.check_scope(token_data.get('scopes', []), tool_name):
            return {'error': 'Insufficient permissions'}
        
        return await self.execute_tool(tool_name, tool_args, token_data)

Testing Your Implementation

Use the tdd skill to build comprehensive test coverage for your OAuth implementation. The tdd skill helps you write tests before implementing security-critical code:

/tdd
Write unit tests for an OAuth 2.1 authorization server including:
- Authorization code generation and storage
- PKCE verification
- Token exchange with code verifier validation
- Refresh token rotation
- Token expiration handling

After writing tests, implement the server following test-driven development principles. For generating API documentation of your MCP endpoints, consider the pdf skill to export documentation in portable formats.

Common Pitfalls to Avoid

Several mistakes frequently appear in OAuth implementations:

  1. Skipping PKCE - OAuth 2.1 requires PKCE for all flows. Even the implicit flow technically requires it now.

  2. Weak token generation - Use cryptographically secure random generators like secrets.token_urlsafe() in Python or crypto.getRandomValues() in JavaScript.

  3. Not rotating refresh tokens - Each refresh should invalidate the previous token to limit damage from token leakage.

  4. Missing token validation - Always validate tokens on protected resources, not just at the authentication boundary.

  5. Insecure redirect URIs - Only allow pre-registered redirect URIs. Reject any attempts to redirect to unexpected domains.

Conclusion

Implementing OAuth 2.1 for MCP servers requires attention to security details but follows straightforward patterns. The key requirements—PKCE for all flows, secure token generation, refresh token rotation, and short-lived access tokens—provide robust protection against common attack vectors. Build your implementation incrementally, test thoroughly using the tdd skill, and validate against the OAuth 2.1 specification before production deployment.

For deeper integration with Claude’s ecosystem, explore how MCP servers can use the supermemory skill for context management, or use the webapp-testing skill to verify your authentication flows work correctly in browser environments.

Built by theluckystrike — More at zovo.one