Security Guide
December 20, 2024
18 min read
ToolEntry Team

Implementing OAuth in MCP Servers: A Complete Security Guide

Learn how to implement secure OAuth 2.1 authentication in MCP servers with PKCE, dynamic client registration, and production-ready security features. Complete guide with code examples.

#OAuth
#MCP Servers
#Security
#Authentication
#PKCE
#JWT
#Production
#Enterprise

ToolEntry Team

Implementing OAuth in MCP Servers: A Complete Security Guide

As MCP servers move from development to production environments, implementing robust authentication becomes critical. OAuth 2.1 provides the security framework needed to protect MCP servers while maintaining compatibility with AI clients. This guide shows you how to implement a complete OAuth solution for MCP servers.

Why OAuth is Essential for MCP Servers

MCP servers often access sensitive data and systems - from databases to file systems to APIs. Without proper authentication:

- Security Risks: Unauthorized access to protected resources

- Compliance Issues: Failure to meet enterprise security requirements

- Scalability Problems: No way to manage user permissions and sessions

- Audit Challenges: Lack of user attribution for actions

OAuth 2.1 solves these problems by providing secure, standardized authentication that works with any MCP client.

Understanding the OAuth 2.1 Flow for MCP Servers

The OAuth flow for MCP servers follows these key steps:

1. Discovery: Client discovers OAuth endpoints

2. Authorization: User grants permission to access resources

3. Token Exchange: Authorization code is exchanged for access tokens

4. Resource Access: Tokens authenticate MCP server requests

5. Token Refresh: Long-lived sessions through refresh tokens

Implementing OAuth Discovery Endpoints

OAuth clients need to discover your server's authentication capabilities. Implement these well-known endpoints:

Protected Resource Metadata

typescript
// GET /.well-known/oauth-protected-resource
export function handleProtectedResourceMetadata(req: IncomingMessage, res: ServerResponse): void {
  const host = req.headers.host;
  const protocol = req.headers['x-forwarded-proto'] || 'http';
  const baseUrl = `${protocol}://${host}`;
  
  const metadata = {
    resource: `${baseUrl}/mcp`,
    authorization_servers: [baseUrl]
  };

  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(metadata));
}

Authorization Server Metadata

typescript
// GET /.well-known/oauth-authorization-server  
export function handleAuthorizationServerMetadata(req: IncomingMessage, res: ServerResponse): void {
  const baseUrl = `${protocol}://${host}`;
  
  const metadata = {
    issuer: `${baseUrl}/auth/v1`,
    authorization_endpoint: `${baseUrl}/api/oauth/authorize`,
    token_endpoint: `${baseUrl}/api/oauth/token`,
    jwks_uri: `${baseUrl}/.well-known/jwks.json`,
    grant_types_supported: ["authorization_code", "refresh_token"],
    code_challenge_methods_supported: ["S256"],
    response_types_supported: ["code"],
    scopes_supported: ["openid", "email", "profile"],
    token_endpoint_auth_methods_supported: ["client_secret_post", "none"],
    resource_indicators_supported: true,
    require_pushed_authorization_requests: false
  };

  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(metadata));
}

Dynamic Client Registration

Support dynamic client registration to handle various MCP clients:

typescript
// POST /.well-known/oauth-dynamic-client-registration
export function handleDynamicClientRegistration(req: IncomingMessage, res: ServerResponse): void {
  const redirectUris = generateSecureRedirectUris();
  
  const clientRegistration = {
    client_id: "mcp-client",
    client_secret: "", // Empty for public client compatibility
    token_endpoint_auth_method: "none",
    grant_types: ["authorization_code", "refresh_token"],
    response_types: ["code"],
    redirect_uris: redirectUris,
    scope: "openid email profile",
    client_name: "MCP Client",
    application_type: "native"
  };

  res.writeHead(201, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(clientRegistration));
}

Comprehensive Redirect URI Support

Support all major MCP clients with comprehensive redirect URIs:

typescript
function generateSecureRedirectUris(): string[] {
  const redirectUris: string[] = [];

  // 1. Localhost patterns for development
  const localhostPorts = [3000, 3001, 5000, 5173, 8080, 8000, 4000, 6274];
  const localhostPaths = ['/oauth/callback', '/callback', '/auth/callback'];
  
  for (const port of localhostPorts) {
    for (const path of localhostPaths) {
      redirectUris.push(`http://localhost:${port}${path}`);
      redirectUris.push(`http://127.0.0.1:${port}${path}`);
    }
  }

  // 2. IDE Custom URI Schemes
  const ideSchemes = [
    // VS Code family
    'vscode://oauth/callback',
    'vscode://auth/callback',
    'vscode-insiders://oauth/callback',
    
    // Cursor AI Editor  
    'cursor://oauth/callback',
    'cursor://auth/callback',
    'cursor://mcp/oauth/callback',
    
    // JetBrains family
    'jetbrains://oauth/callback',
    'intellij://oauth/callback',
    'pycharm://oauth/callback',
    'webstorm://oauth/callback',
    
    // Other editors
    'zed://oauth/callback',
    'atom://oauth/callback',
    'sublime://oauth/callback',
    
    // MCP-specific schemes
    'mcp://oauth/callback',
    'mcp://auth/callback'
  ];
  
  redirectUris.push(...ideSchemes);
  return redirectUris;
}

Authorization Endpoint Implementation

Handle the authorization request with proper validation:

typescript
// GET /api/oauth/authorize
export async function handleAuthorize(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  
  const response_type = searchParams.get('response_type');
  const redirect_uri = searchParams.get('redirect_uri');
  const scope = searchParams.get('scope');
  const state = searchParams.get('state');
  const code_challenge = searchParams.get('code_challenge');
  const code_challenge_method = searchParams.get('code_challenge_method');

  // Validate required parameters
  if (!response_type || response_type !== 'code') {
    return createOAuthErrorResponse(
      'unsupported_response_type',
      'Only authorization code flow is supported'
    );
  }

  if (!redirect_uri) {
    return createOAuthErrorResponse(
      'invalid_request',
      'redirect_uri is required'
    );
  }

  // Validate PKCE parameters (required for OAuth 2.1)
  if (!code_challenge || !code_challenge_method || code_challenge_method !== 'S256') {
    return createOAuthErrorResponse(
      'invalid_request', 
      'PKCE with S256 is required for security'
    );
  }

  // Validate redirect URI
  if (!isValidMCPRedirectUri(redirect_uri)) {
    return createOAuthErrorResponse(
      'invalid_request', 
      'Invalid redirect URI'
    );
  }

  // Check authentication and consent...
  // Implementation details depend on your auth system
}

Token Endpoint with Refresh Support

Implement token exchange with proper refresh token rotation:

typescript
// POST /api/oauth/token
export async function handleToken(request: NextRequest) {
  const body = await request.json();
  const { grant_type, code, redirect_uri, code_verifier, refresh_token } = body;

  // Validate grant type
  if (grant_type !== 'authorization_code' && grant_type !== 'refresh_token') {
    return createOAuthErrorResponse(
      'unsupported_grant_type',
      'Only authorization_code and refresh_token grant types are supported'
    );
  }

  if (grant_type === 'refresh_token') {
    return handleRefreshTokenGrant(refresh_token);
  }

  // Handle authorization code grant
  if (!code || !redirect_uri || !code_verifier) {
    return createOAuthErrorResponse(
      'invalid_request',
      'Missing required parameters'
    );
  }

  // Verify PKCE challenge
  const isValidPKCE = await verifyPKCEChallenge(code, code_verifier);
  if (!isValidPKCE) {
    return createOAuthErrorResponse(
      'invalid_grant',
      'PKCE verification failed'
    );
  }

  // Exchange code for tokens
  const tokens = await exchangeAuthorizationCode(code, redirect_uri);
  
  return NextResponse.json({
    access_token: tokens.access_token,
    token_type: 'Bearer',
    expires_in: 3600,
    refresh_token: tokens.refresh_token,
    scope: 'openid email profile'
  });
}

Secure Token Generation

Generate secure JWTs with proper claims:

typescript
async function generateAccessToken(userId: string, email: string): Promise<string> {
  const secret = new TextEncoder().encode(process.env.JWT_SECRET);
  const jti = generateJTI();
  
  const jwt = await new SignJWT({
    sub: userId,
    email: email,
    aud: 'mcp-server',
    jti: jti,
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 3600 // 1 hour
  })
  .setProtectedHeader({ alg: 'HS256' })
  .sign(secret);

  return jwt;
}

function generateRefreshToken(): string {
  // Generate cryptographically secure refresh token
  return crypto.randomBytes(32).toString('base64url');
}

User Consent Flow

Implement a clean consent interface:

typescript
// React component for consent page
export default function OAuthConfirm() {
  const [oauthParams, setOauthParams] = useState(null);
  
  const handleAuthorize = async () => {
    // Grant consent
    const response = await fetch('/api/oauth/consent', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        client_identifier: oauthParams.client_identifier,
        scope: oauthParams.scope
      })
    });

    if (response.ok) {
      // Redirect back to authorization endpoint
      const authorizeUrl = new URL('/api/oauth/authorize', window.location.origin);
      Object.entries(oauthParams).forEach(([key, value]) => {
        if (value && key !== 'timestamp') {
          authorizeUrl.searchParams.set(key, value);
        }
      });
      
      window.location.href = authorizeUrl.toString();
    }
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle>Authorization Request</CardTitle>
        <CardDescription>
          An MCP client wants to access your account
        </CardDescription>
      </CardHeader>
      <CardContent>
        <div className="space-y-4">
          <Alert>
            <Shield className="h-4 w-4" />
            <AlertDescription>
              This will allow the client to access your profile information
              and perform actions on your behalf.
            </AlertDescription>
          </Alert>
          
          <div className="flex gap-3">
            <Button onClick={handleAuthorize}>
              Authorize
            </Button>
            <Button variant="outline" onClick={() => window.close()}>
              Cancel
            </Button>
          </div>
        </div>
      </CardContent>
    </Card>
  );
}

Security Best Practices

1. Always Require PKCE

PKCE (Proof Key for Code Exchange) is mandatory for OAuth 2.1:

typescript
function validatePKCE(code_challenge: string, code_challenge_method: string) {
  if (!code_challenge || !code_challenge_method) {
    return { isValid: false, reason: 'PKCE parameters are required' };
  }
  
  if (code_challenge_method !== 'S256') {
    return { isValid: false, reason: 'Only S256 challenge method is supported' };
  }
  
  return { isValid: true };
}

2. Implement Refresh Token Rotation

Rotate refresh tokens on each use for enhanced security:

typescript
async function handleRefreshTokenGrant(refresh_token: string) {
  // Validate current refresh token
  const session = await validateRefreshToken(refresh_token);
  if (!session) {
    return createOAuthErrorResponse('invalid_grant', 'Invalid refresh token');
  }

  // Generate new tokens
  const newAccessToken = await generateAccessToken(session.user_id, session.email);
  const newRefreshToken = generateRefreshToken();
  
  // Rotate refresh token in database
  await rotateRefreshToken(session.id, newRefreshToken);

  return {
    access_token: newAccessToken,
    token_type: 'Bearer',
    expires_in: 3600,
    refresh_token: newRefreshToken
  };
}

3. Validate Redirect URIs Strictly

Implement strict redirect URI validation:

typescript
function isValidMCPRedirectUri(redirect_uri: string): boolean {
  // Allow localhost for development
  if (redirect_uri.match(/^https?://(localhost|127.0.0.1):d+//)) {
    return true;
  }
  
  // Allow HTTPS for production
  if (redirect_uri.startsWith('https://')) {
    return true;
  }
  
  // Allow IDE custom schemes
  const allowedSchemes = ['vscode:', 'cursor:', 'jetbrains:', 'mcp:'];
  if (allowedSchemes.some(scheme => redirect_uri.startsWith(scheme))) {
    return true;
  }
  
  return false;
}

4. Implement Proper CORS

Configure CORS for OAuth endpoints:

typescript
function setCORSHeaders(res: ServerResponse) {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
}

Integration with MCP Servers

Your MCP server can now authenticate requests using the issued tokens:

typescript
async function validateMCPRequest(request: any): Promise<AuthContext> {
  const authHeader = request.headers?.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return { isAuthenticated: false, error: 'Missing authorization header' };
  }

  const token = authHeader.substring(7);
  
  try {
    const payload = await verifyJWT(token);
    return {
      isAuthenticated: true,
      userId: payload.sub,
      email: payload.email,
      tokenJti: payload.jti
    };
  } catch (error) {
    return { isAuthenticated: false, error: 'Invalid token' };
  }
}

Testing Your OAuth Implementation

Test the complete flow:

1. Discovery: Verify well-known endpoints return correct metadata

2. Authorization: Test the authorization flow with various clients

3. Token Exchange: Confirm PKCE validation and token generation

4. Refresh: Test refresh token rotation

5. Resource Access: Verify token validation in MCP server

Production Considerations

Database Schema

Store OAuth sessions securely:

sql
CREATE TABLE mcp_server_sessions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id) ON DELETE CASCADE,
  client_identifier TEXT NOT NULL,
  refresh_token_hash TEXT NOT NULL,
  expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX idx_mcp_sessions_refresh_token ON mcp_server_sessions(refresh_token_hash);
CREATE INDEX idx_mcp_sessions_expires_at ON mcp_server_sessions(expires_at);

Environment Configuration

bash
# OAuth Configuration
JWT_SECRET=your-jwt-secret-key
OAUTH_BASE_URL=https://your-domain.com
MCP_SERVER_AUDIENCE=mcp-server

# Token Lifetimes
ACCESS_TOKEN_EXPIRES=3600  # 1 hour
REFRESH_TOKEN_EXPIRES=7776000  # 90 days

Monitoring and Logging

Implement comprehensive logging:

typescript
function logOAuthRequest(endpoint: string, params: any, userAgent?: string) {
  console.log('OAuth Request:', {
    endpoint,
    timestamp: new Date().toISOString(),
    userAgent,
    params: sanitizeParams(params)
  });
}

function sanitizeParams(params: any) {
  const { code_challenge, code_verifier, access_token, refresh_token, ...safe } = params;
  return safe;
}

Conclusion

Implementing OAuth 2.1 in MCP servers provides enterprise-grade security while maintaining compatibility with AI clients. Key benefits include:

- Enhanced Security: PKCE, token rotation, and strict validation

- Scalability: Support for multiple clients and users

- Compliance: Meets enterprise authentication requirements

- Flexibility: Works with any OAuth 2.1 compatible client

By following this implementation guide, you'll have a production-ready OAuth system that secures your MCP server while providing a seamless experience for users and AI clients.

The OAuth implementation ensures your MCP server can safely operate in production environments, handling sensitive data and operations with the security controls that enterprises require.

Get Started

Install ToolEntry MCP Server

This MCP server gives your AI the ability to manage its own tools

Ready in under 60 seconds

Terminal
npx @toolentry.io/cli@latest autoinstall claude-desktop --template toolentry

After installation:

• Restart Claude Desktop

• Test: "Install the Git MCP server"

• Explore workflows in your dashboard

Requires Node.js for npx installation • Download Node.js here

ToolEntry uses ToolEntry-CLI which is open source and free to use.

You might also like

Explore more insights and guides to enhance your AI workflow automation