Authentication & Authorization in MCP
Introduction
Authentication and authorization are critical components of secure MCP server implementations. While MCP itself doesn't mandate specific auth mechanisms, implementing robust authentication ensures that only authorized clients can access your MCP servers and that operations are properly scoped to user permissions.
This comprehensive guide covers various authentication strategies, from simple API keys to advanced OAuth2 implementations, along with best practices for authorization in MCP contexts.
Authentication Strategies Overview
Choosing the Right Strategy
Different authentication methods suit different use cases:
Method | Use Case | Security Level | Complexity |
---|---|---|---|
API Keys | Internal tools, simple integrations | Medium | Low |
JWT Tokens | Stateless auth, microservices | High | Medium |
OAuth2 | Third-party integrations, enterprise | Very High | High |
mTLS | Zero-trust environments | Very High | High |
Session-based | Traditional web apps | Medium | Low |
MCP Transport Considerations
// Different auth for different transports interface AuthStrategy { validateStdio?: (env: ProcessEnv) => Promise<boolean>; validateHTTP?: (headers: Headers) => Promise<boolean>; validateWebSocket?: (request: Request) => Promise<boolean>; }
class TransportAwareAuth implements AuthStrategy { async validateStdio(env: ProcessEnv): Promise<boolean> { // For stdio: Use environment variables const token = env.MCP_AUTH_TOKEN; return this.validateToken(token); } async validateHTTP(headers: Headers): Promise<boolean> { // For HTTP: Use Authorization header const authHeader = headers.get('Authorization'); if (!authHeader?.startsWith('Bearer ')) { return false; } return this.validateToken(authHeader.slice(7)); } async validateWebSocket(request: Request): Promise<boolean> { // For WebSocket: Use query parameters or headers const url = new URL(request.url); const token = url.searchParams.get('token'); return this.validateToken(token); } private async validateToken(token: string | null): Promise<boolean> { if (!token) return false; // Token validation logic return true; } }
API Key Authentication
Basic Implementation
import crypto from 'crypto'; import { createHash } from 'crypto';
class APIKeyManager { private keys: Map<string, APIKey> = new Map(); interface APIKey { id: string; hashedKey: string; name: string; permissions: string[]; createdAt: Date; lastUsed: Date; expiresAt?: Date; metadata: Record<string, any>; } generateAPIKey(): { key: string; keyData: APIKey } { // Generate secure random key const key = crypto.randomBytes(32).toString('base64url'); const keyId = crypto.randomBytes(8).toString('hex'); // Hash the key for storage const hashedKey = createHash('sha256') .update(key) .digest('hex'); const keyData: APIKey = { id: keyId, hashedKey, name: 'Generated API Key', permissions: ['read'], createdAt: new Date(), lastUsed: new Date(), metadata: {} }; this.keys.set(keyId, keyData); // Return the key only once - it can't be recovered return { key:
mcp_${keyId}_${key}
, // Prefixed format keyData }; } async validateAPIKey(apiKey: string): Promise<APIKey | null> { // Parse the key const match = apiKey.match(/^mcp_([a-f0-9]{16})_(.+)$/); if (!match) return null; const [, keyId, key] = match; const keyData = this.keys.get(keyId); if (!keyData) return null; // Check expiration if (keyData.expiresAt && keyData.expiresAt < new Date()) { return null; } // Verify the key const hashedKey = createHash('sha256') .update(key) .digest('hex'); if (hashedKey !== keyData.hashedKey) { return null; } // Update last used keyData.lastUsed = new Date(); return keyData; } revokeAPIKey(keyId: string): boolean { return this.keys.delete(keyId); } }
Advanced API Key Features
class AdvancedAPIKeyManager extends APIKeyManager {
private rateLimits: Map<string, RateLimit> = new Map();
private usageStats: Map<string, UsageStats> = new Map();
interface RateLimit {
requests: number;
window: number; // in seconds
}
interface UsageStats {
totalRequests: number;
lastRequests: Array<{ timestamp: Date; endpoint: string }>;
}
async validateWithRateLimit(
apiKey: string,
endpoint: string
): Promise<{ valid: boolean; remaining?: number }> {
const keyData = await this.validateAPIKey(apiKey);
if (!keyData) {
return { valid: false };
}
// Check rate limit
const limit = this.rateLimits.get(keyData.id) || {
requests: 1000,
window: 3600 // 1 hour
};
const stats = this.usageStats.get(keyData.id) || {
totalRequests: 0,
lastRequests: []
};
// Clean old requests
const cutoff = new Date(Date.now() - limit.window * 1000);
stats.lastRequests = stats.lastRequests.filter(
req => req.timestamp > cutoff
);
// Check limit
if (stats.lastRequests.length >= limit.requests) {
return { valid: false, remaining: 0 };
}
// Record request
stats.lastRequests.push({ timestamp: new Date(), endpoint });
stats.totalRequests++;
this.usageStats.set(keyData.id, stats);
return {
valid: true,
remaining: limit.requests - stats.lastRequests.length
};
}
// Scope-based permissions
hasPermission(keyData: APIKey, scope: string): boolean {
// Check exact match
if (keyData.permissions.includes(scope)) {
return true;
}
// Check wildcard permissions
for (const perm of keyData.permissions) {
if (perm.endsWith('*')) {
const prefix = perm.slice(0, -1);
if (scope.startsWith(prefix)) {
return true;
}
}
}
return false;
}
}
JWT Token Authentication
JWT Implementation
import jwt from 'jsonwebtoken'; import { JWK, JWS } from 'node-jose';
interface TokenPayload { sub: string; // Subject (user ID) iss: string; // Issuer aud: string[]; // Audience exp: number; // Expiration iat: number; // Issued at nbf?: number; // Not before jti?: string; // JWT ID scope?: string[]; // Permissions roles?: string[]; // User roles }
class JWTAuthenticator { private publicKey: string; private privateKey: string; private issuer: string; private audience: string[]; private tokenExpiry: number = 3600; // 1 hour constructor(config: JWTConfig) { this.publicKey = config.publicKey; this.privateKey = config.privateKey; this.issuer = config.issuer; this.audience = config.audience; } async generateToken( userId: string, scope: string[] = [], roles: string[] = [] ): Promise<string> { const now = Math.floor(Date.now() / 1000); const payload: TokenPayload = { sub: userId, iss: this.issuer, aud: this.audience, exp: now + this.tokenExpiry, iat: now, nbf: now, jti: crypto.randomUUID(), scope, roles }; return jwt.sign(payload, this.privateKey, { algorithm: 'RS256', keyid: await this.getKeyId() }); } async validateToken(token: string): Promise<TokenPayload | null> { try { const decoded = jwt.verify(token, this.publicKey, { algorithms: ['RS256'], issuer: this.issuer, audience: this.audience, clockTolerance: 10 // 10 seconds }) as TokenPayload; // Additional validation if (!decoded.sub || !decoded.scope) { return null; } // Check if token is blacklisted if (await this.isTokenBlacklisted(decoded.jti)) { return null; } return decoded; } catch (error) { console.error('Token validation failed:', error); return null; } } async refreshToken(token: string): Promise<string | null> { const payload = await this.validateToken(token); if (!payload) return null; // Check if token is eligible for refresh const now = Math.floor(Date.now() / 1000); const timeUntilExpiry = payload.exp - now; // Only refresh if less than 5 minutes remaining if (timeUntilExpiry > 300) { return token; // Return existing token } // Generate new token with same claims return this.generateToken( payload.sub, payload.scope, payload.roles ); } private async getKeyId(): Promise<string> { // Generate key ID from public key const keyStore = JWK.createKeyStore(); const key = await keyStore.add(this.publicKey, 'pem'); return key.kid; } private tokenBlacklist = new Set<string>(); async blacklistToken(jti: string): Promise<void> { this.tokenBlacklist.add(jti); // In production, store in Redis with TTL } private async isTokenBlacklisted(jti?: string): Promise<boolean> { if (!jti) return false; return this.tokenBlacklist.has(jti); } }
JWT with MCP Integration
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
class JWTProtectedMCPServer { private server: Server; private auth: JWTAuthenticator; constructor() { this.auth = new JWTAuthenticator({ publicKey: process.env.JWT_PUBLIC_KEY!, privateKey: process.env.JWT_PRIVATE_KEY!, issuer: 'mcp-server', audience: ['mcp-client'] }); this.setupServer(); } private setupServer() { // Middleware to validate JWT on every request this.server.use(async (request, next) => { // Extract token from request const token = this.extractToken(request); if (!token) { throw new Error('Authentication required'); } const payload = await this.auth.validateToken(token); if (!payload) { throw new Error('Invalid or expired token'); } // Attach user context to request request.context = { userId: payload.sub, scope: payload.scope || [], roles: payload.roles || [] }; return next(); }); // Scope-based authorization for tools this.server.setRequestHandler('tools/call', async (request) => { const { name: toolName } = request.params; const requiredScope =
tools:${toolName}
; if (!request.context.scope.includes(requiredScope) && !request.context.scope.includes('tools:*')) { throw new Error(Insufficient scope: ${requiredScope} required
); } // Execute tool... }); } private extractToken(request: any): string | null { // Try Authorization header first if (request.headers?.authorization) { const parts = request.headers.authorization.split(' '); if (parts.length === 2 && parts[0] === 'Bearer') { return parts[1]; } } // Try query parameter if (request.query?.token) { return request.query.token; } // Try cookie if (request.cookies?.mcp_token) { return request.cookies.mcp_token; } return null; } }
OAuth2 Integration
OAuth2 Server Implementation
import { OAuth2Server } from 'oauth2-server';
class MCPOAuth2Provider { private oauth: OAuth2Server; private clients: Map<string, OAuthClient> = new Map(); private tokens: Map<string, Token> = new Map(); interface OAuthClient { id: string; secret: string; redirectUris: string[]; grants: string[]; scopes: string[]; } interface Token { accessToken: string; refreshToken?: string; client: string; user: string; scope: string[]; expiresAt: Date; } constructor() { this.oauth = new OAuth2Server({ model: this, grants: ['authorization_code', 'refresh_token', 'client_credentials'], accessTokenLifetime: 3600, refreshTokenLifetime: 86400 }); } // OAuth2 Model Implementation async getClient(clientId: string, clientSecret: string): Promise<OAuthClient | null> { const client = this.clients.get(clientId); if (!client || client.secret !== clientSecret) { return null; } return client; } async saveToken(token: any, client: any, user: any): Promise<Token> { const tokenData: Token = { accessToken: token.accessToken, refreshToken: token.refreshToken, client: client.id, user: user.id, scope: token.scope || [], expiresAt: token.accessTokenExpiresAt }; this.tokens.set(token.accessToken, tokenData); if (token.refreshToken) { this.tokens.set(token.refreshToken, tokenData); } return tokenData; } async getAccessToken(accessToken: string): Promise<Token | null> { const token = this.tokens.get(accessToken); if (!token || token.expiresAt < new Date()) { return null; } return token; } // Authorization flow async authorize(request: Request, response: Response): Promise<void> { const authRequest = new OAuth2Server.Request(request); const authResponse = new OAuth2Server.Response(response); try { const code = await this.oauth.authorize(authRequest, authResponse, { authenticateHandler: { handle: async (req) => { // Verify user authentication const userId = req.headers['x-user-id']; if (!userId) throw new Error('User not authenticated'); return { id: userId }; } } }); // Redirect to callback with code const redirectUri =
${request.query.redirect_uri}?code=${code.authorizationCode}&state=${request.query.state}
; response.redirect(redirectUri); } catch (error) { response.status(error.code || 500).json({ error: error.name, error_description: error.message }); } } // Token exchange async token(request: Request, response: Response): Promise<void> { const tokenRequest = new OAuth2Server.Request(request); const tokenResponse = new OAuth2Server.Response(response); try { const token = await this.oauth.token(tokenRequest, tokenResponse); response.json({ access_token: token.accessToken, token_type: 'Bearer', expires_in: token.accessTokenLifetime, refresh_token: token.refreshToken, scope: token.scope.join(' ') }); } catch (error) { response.status(error.code || 500).json({ error: error.name, error_description: error.message }); } } }
OAuth2 Client for MCP
class OAuth2MCPClient {
private clientId: string;
private clientSecret: string;
private authorizationUrl: string;
private tokenUrl: string;
private redirectUri: string;
private scope: string[];
constructor(config: OAuth2Config) {
this.clientId = config.clientId;
this.clientSecret = config.clientSecret;
this.authorizationUrl = config.authorizationUrl;
this.tokenUrl = config.tokenUrl;
this.redirectUri = config.redirectUri;
this.scope = config.scope;
}
// Generate authorization URL
getAuthorizationUrl(state: string): string {
const params = new URLSearchParams({
response_type: 'code',
client_id: this.clientId,
redirect_uri: this.redirectUri,
scope: this.scope.join(' '),
state
});
return ${this.authorizationUrl}?${params}
;
}
// Exchange code for token
async exchangeCodeForToken(code: string): Promise<TokenResponse> {
const response = await fetch(this.tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': Basic ${Buffer.from(
${this.clientId}:${this.clientSecret}
).toString('base64')}
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: this.redirectUri
})
});
if (!response.ok) {
throw new Error('Token exchange failed');
}
return response.json();
}
// Refresh token
async refreshToken(refreshToken: string): Promise<TokenResponse> {
const response = await fetch(this.tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': Basic ${Buffer.from(
${this.clientId}:${this.clientSecret}
).toString('base64')}
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken
})
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
return response.json();
}
// Create authenticated MCP transport
createAuthenticatedTransport(accessToken: string): AuthenticatedTransport {
return new AuthenticatedTransport({
baseTransport: new HttpClientTransport(),
authHeader: Bearer ${accessToken}
,
onAuthError: async () => {
// Handle token refresh
if (this.refreshToken) {
const newToken = await this.refreshToken(this.refreshToken);
this.accessToken = newToken.access_token;
return Bearer ${newToken.access_token}
;
}
throw new Error('Authentication failed');
}
});
}
}
Mutual TLS (mTLS) Authentication
mTLS Server Setup
import tls from 'tls'; import { readFileSync } from 'fs';
class MTLSMCPServer { private server: tls.Server; private allowedCertificates: Map<string, ClientCert> = new Map(); interface ClientCert { fingerprint: string; subject: string; issuer: string; permissions: string[]; validFrom: Date; validTo: Date; } constructor() { const options: tls.TlsOptions = { key: readFileSync('server-key.pem'), cert: readFileSync('server-cert.pem'), ca: readFileSync('ca-cert.pem'), // Require client certificates requestCert: true, rejectUnauthorized: true, // Verify client certificate verifyCallback: (socket, cert) => { return this.verifyCertificate(cert); } }; this.server = tls.createServer(options, (socket) => { this.handleConnection(socket); }); } private verifyCertificate(cert: tls.PeerCertificate): boolean { // Generate fingerprint const fingerprint = cert.fingerprint.replace(/:/g, '').toLowerCase(); // Check if certificate is allowed const allowed = this.allowedCertificates.get(fingerprint); if (!allowed) { console.error('Unknown certificate:', fingerprint); return false; } // Check validity period const now = new Date(); if (now < allowed.validFrom || now > allowed.validTo) { console.error('Certificate expired:', fingerprint); return false; } // Additional checks if (cert.subject.CN !== allowed.subject) { console.error('Subject mismatch:', cert.subject.CN); return false; } return true; } private handleConnection(socket: tls.TLSSocket) { const cert = socket.getPeerCertificate(); const fingerprint = cert.fingerprint.replace(/:/g, '').toLowerCase(); const clientInfo = this.allowedCertificates.get(fingerprint)!; // Create MCP transport with client context const transport = new TLSTransport(socket, { clientId: fingerprint, permissions: clientInfo.permissions }); // Initialize MCP server with auth context const mcpServer = new Server({ name: 'mtls-protected-server', version: '1.0.0' }); // All requests are pre-authenticated via mTLS mcpServer.setRequestHandler('tools/call', async (request) => { // Check permissions from certificate const tool = request.params.name; if (!clientInfo.permissions.includes(
tools:${tool}
)) { throw new Error('Permission denied'); } // Execute tool... }); mcpServer.connect(transport); } // Certificate management async issueCertificate( clientId: string, permissions: string[] ): Promise<{ cert: string; key: string }> { // In production, use a proper CA // This is a simplified example const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }); const cert = { subject:/CN=${clientId}/O=MCP Client
, issuer: '/CN=MCP CA/O=MCP Server', validFrom: new Date(), validTo: new Date(Date.now() + 365 24 60 60 1000), extensions: { permissions: permissions.join(',') } }; // Generate and sign certificate // (Implementation depends on crypto library) return { cert: 'generated-cert-pem', key: 'generated-key-pem' }; } }
Session-Based Authentication
Secure Session Management
import { SessionData } from 'express-session'; import Redis from 'ioredis';
class SecureSessionManager { private redis: Redis; private sessionTTL: number = 3600; // 1 hour interface MCPSession extends SessionData { id: string; userId: string; roles: string[]; permissions: string[]; createdAt: Date; lastActivity: Date; ipAddress: string; userAgent: string; metadata: Record<string, any>; } constructor(redisUrl: string) { this.redis = new Redis(redisUrl); } async createSession(userId: string, context: any): Promise<string> { const sessionId = crypto.randomBytes(32).toString('hex'); const session: MCPSession = { id: sessionId, userId, roles: context.roles || [], permissions: context.permissions || [], createdAt: new Date(), lastActivity: new Date(), ipAddress: context.ipAddress, userAgent: context.userAgent, metadata: {} }; // Store in Redis with TTL await this.redis.setex(
session:${sessionId}
, this.sessionTTL, JSON.stringify(session) ); // Store user's active sessions await this.redis.sadd(user:${userId}:sessions
, sessionId); return sessionId; } async getSession(sessionId: string): Promise<MCPSession | null> { const data = await this.redis.get(session:${sessionId}
); if (!data) return null; const session = JSON.parse(data) as MCPSession; // Check if session is still valid const inactivityTimeout = 30 60 1000; // 30 minutes const lastActivity = new Date(session.lastActivity); if (Date.now() - lastActivity.getTime() > inactivityTimeout) { await this.revokeSession(sessionId); return null; } // Update last activity session.lastActivity = new Date(); await this.redis.setex(session:${sessionId}
, this.sessionTTL, JSON.stringify(session) ); return session; } async revokeSession(sessionId: string): Promise<void> { const session = await this.getSession(sessionId); if (!session) return; // Remove from Redis await this.redis.del(session:${sessionId}
); // Remove from user's sessions await this.redis.srem(user:${session.userId}:sessions
, sessionId); } async revokeAllUserSessions(userId: string): Promise<void> { const sessions = await this.redis.smembers(user:${userId}:sessions
); for (const sessionId of sessions) { await this.revokeSession(sessionId); } } // Session-based MCP middleware createSessionMiddleware() { return async (request: any, next: Function) => { // Extract session ID from cookie or header const sessionId = request.cookies?.mcp_session || request.headers['x-session-id']; if (!sessionId) { throw new Error('No session provided'); } const session = await this.getSession(sessionId); if (!session) { throw new Error('Invalid or expired session'); } // Verify IP address hasn't changed (optional) if (session.ipAddress !== request.ip) { console.warn('IP address mismatch for session:', sessionId); // Optionally revoke session } // Attach session to request request.session = session; return next(); }; } }
Multi-Factor Authentication (MFA)
TOTP Implementation
import speakeasy from 'speakeasy'; import QRCode from 'qrcode';
class MFAManager { private userSecrets: Map<string, MFASecret> = new Map(); interface MFASecret { secret: string; backupCodes: string[]; createdAt: Date; lastUsed?: Date; } async enableMFA(userId: string): Promise<{ secret: string; qrCode: string; backupCodes: string[]; }> { // Generate secret const secret = speakeasy.generateSecret({ name:
MCP Server (${userId})
, issuer: 'MCP Auth', length: 32 }); // Generate backup codes const backupCodes = Array.from({ length: 8 }, () => crypto.randomBytes(4).toString('hex') ); // Store secret this.userSecrets.set(userId, { secret: secret.base32, backupCodes, createdAt: new Date() }); // Generate QR code const qrCode = await QRCode.toDataURL(secret.otpauth_url!); return { secret: secret.base32, qrCode, backupCodes }; } async verifyMFA( userId: string, token: string ): Promise<boolean> { const mfaSecret = this.userSecrets.get(userId); if (!mfaSecret) return false; // Try TOTP verification const verified = speakeasy.totp.verify({ secret: mfaSecret.secret, encoding: 'base32', token, window: 2 // Allow 2 time steps tolerance }); if (verified) { mfaSecret.lastUsed = new Date(); return true; } // Try backup codes const codeIndex = mfaSecret.backupCodes.indexOf(token); if (codeIndex !== -1) { // Remove used backup code mfaSecret.backupCodes.splice(codeIndex, 1); mfaSecret.lastUsed = new Date(); return true; } return false; } }
// MFA-protected authentication flow class MFAAuthFlow { private mfaManager: MFAManager; private pendingAuths: Map<string, PendingAuth> = new Map(); interface PendingAuth { userId: string; timestamp: Date; attempts: number; } async authenticate( username: string, password: string ): Promise<{ requiresMFA: boolean; authToken?: string }> { // Verify username/password const user = await this.verifyCredentials(username, password); if (!user) { throw new Error('Invalid credentials'); } // Check if MFA is enabled if (!user.mfaEnabled) { // Generate auth token directly const token = await this.generateAuthToken(user.id); return { requiresMFA: false, authToken: token }; } // Create pending auth const pendingId = crypto.randomUUID(); this.pendingAuths.set(pendingId, { userId: user.id, timestamp: new Date(), attempts: 0 }); // Clean up after 5 minutes setTimeout(() => { this.pendingAuths.delete(pendingId); }, 5 60 1000); return { requiresMFA: true, pendingAuthId: pendingId }; } async completeMFA( pendingAuthId: string, mfaToken: string ): Promise<string> { const pending = this.pendingAuths.get(pendingAuthId); if (!pending) { throw new Error('Invalid or expired auth session'); } // Check attempts pending.attempts++; if (pending.attempts > 3) { this.pendingAuths.delete(pendingAuthId); throw new Error('Too many failed attempts'); } // Verify MFA const verified = await this.mfaManager.verifyMFA( pending.userId, mfaToken ); if (!verified) { throw new Error('Invalid MFA token'); } // Clean up and generate token this.pendingAuths.delete(pendingAuthId); return this.generateAuthToken(pending.userId); } }
Authorization Patterns
Fine-Grained Permissions
class PermissionManager {
private permissions: Map<string, Permission> = new Map();
interface Permission {
id: string;
resource: string;
action: string;
conditions?: PermissionCondition[];
}
interface PermissionCondition {
type: 'ownership' | 'time' | 'location' | 'attribute';
operator: 'eq' | 'ne' | 'gt' | 'lt' | 'in' | 'between';
value: any;
}
// Define permissions
definePermission(permission: Permission): void {
const key = ${permission.resource}:${permission.action}
;
this.permissions.set(key, permission);
}
// Check permission with context
async checkPermission(
user: User,
resource: string,
action: string,
context: any
): Promise<boolean> {
// Check direct permissions
const directPerm = ${resource}:${action}
;
if (user.permissions.includes(directPerm)) {
return this.evaluateConditions(directPerm, context);
}
// Check wildcard permissions
if (user.permissions.includes(${resource}:*
)) {
return true;
}
if (user.permissions.includes(':')) {
return true;
}
// Check role-based permissions
for (const role of user.roles) {
const rolePerm = await this.getRolePermissions(role);
if (rolePerm.includes(directPerm)) {
return this.evaluateConditions(directPerm, context);
}
}
return false;
}
private evaluateConditions(
permissionKey: string,
context: any
): boolean {
const permission = this.permissions.get(permissionKey);
if (!permission?.conditions) return true;
for (const condition of permission.conditions) {
if (!this.evaluateCondition(condition, context)) {
return false;
}
}
return true;
}
private evaluateCondition(
condition: PermissionCondition,
context: any
): boolean {
switch (condition.type) {
case 'ownership':
return context.ownerId === context.userId;
case 'time':
const now = new Date();
const value = new Date(condition.value);
switch (condition.operator) {
case 'gt': return now > value;
case 'lt': return now < value;
case 'between':
return now >= value[0] && now <= value[1];
}
break;
case 'location':
// Implement location-based checks
break;
case 'attribute':
const attrValue = context[condition.field];
switch (condition.operator) {
case 'eq': return attrValue === condition.value;
case 'ne': return attrValue !== condition.value;
case 'in': return condition.value.includes(attrValue);
}
break;
}
return false;
}
}
Dynamic Authorization Policies
class PolicyEngine {
private policies: Policy[] = [];
interface Policy {
id: string;
priority: number;
effect: 'allow' | 'deny';
subjects: PolicySubject[];
resources: PolicyResource[];
actions: string[];
conditions?: PolicyCondition[];
}
interface PolicySubject {
type: 'user' | 'role' | 'group';
value: string | string[];
}
interface PolicyResource {
type: string;
pattern: string; // Supports wildcards
}
interface PolicyCondition {
key: string;
operator: string;
value: any;
}
evaluateAccess(
subject: any,
resource: string,
action: string,
environment: any
): 'allow' | 'deny' | 'notapplicable' {
// Sort policies by priority
const sortedPolicies = [...this.policies].sort(
(a, b) => b.priority - a.priority
);
for (const policy of sortedPolicies) {
if (this.policyApplies(policy, subject, resource, action, environment)) {
return policy.effect;
}
}
return 'notapplicable';
}
private policyApplies(
policy: Policy,
subject: any,
resource: string,
action: string,
environment: any
): boolean {
// Check subjects
if (!this.subjectMatches(policy.subjects, subject)) {
return false;
}
// Check resources
if (!this.resourceMatches(policy.resources, resource)) {
return false;
}
// Check actions
if (!policy.actions.includes(action) &&
!policy.actions.includes('*')) {
return false;
}
// Check conditions
if (policy.conditions) {
const context = { subject, resource, action, environment };
if (!this.conditionsMatch(policy.conditions, context)) {
return false;
}
}
return true;
}
private subjectMatches(
policySubjects: PolicySubject[],
subject: any
): boolean {
for (const ps of policySubjects) {
switch (ps.type) {
case 'user':
if (Array.isArray(ps.value)) {
if (ps.value.includes(subject.id)) return true;
} else {
if (ps.value === subject.id) return true;
}
break;
case 'role':
if (Array.isArray(ps.value)) {
if (ps.value.some(r => subject.roles?.includes(r))) return true;
} else {
if (subject.roles?.includes(ps.value)) return true;
}
break;
case 'group':
if (Array.isArray(ps.value)) {
if (ps.value.some(g => subject.groups?.includes(g))) return true;
} else {
if (subject.groups?.includes(ps.value)) return true;
}
break;
}
}
return false;
}
private resourceMatches(
policyResources: PolicyResource[],
resource: string
): boolean {
for (const pr of policyResources) {
const pattern = pr.pattern.replace(/\/g, '.');
const regex = new RegExp(^${pattern}$
);
if (regex.test(resource)) {
return true;
}
}
return false;
}
}
Security Best Practices
Token Storage
class SecureTokenStorage {
// Never store tokens in:
// - localStorage (XSS vulnerable)
// - sessionStorage (XSS vulnerable)
// - Cookies without proper flags
// Secure cookie storage
setSecureToken(res: Response, token: string): void {
res.cookie('mcp_token', token, {
httpOnly: true, // Prevent XSS access
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 3600000, // 1 hour
path: '/',
domain: '.example.com'
});
}
// For SPAs: Use memory + refresh token
class TokenManager {
private accessToken: string | null = null;
private refreshToken: string | null = null;
setTokens(access: string, refresh: string): void {
this.accessToken = access;
// Store refresh token in httpOnly cookie
document.cookie = mcp_refresh=${refresh};
+
'HttpOnly; Secure; SameSite=Strict; Path=/api/refresh';
}
getAccessToken(): string | null {
return this.accessToken;
}
async refreshAccessToken(): Promise<string> {
const response = await fetch('/api/refresh', {
method: 'POST',
credentials: 'include' // Send cookies
});
const { access_token } = await response.json();
this.accessToken = access_token;
return access_token;
}
}
}
Secure Communication
// Always use HTTPS in production if (process.env.NODE_ENV === 'production') { app.use((req, res, next) => { if (req.header('x-forwarded-proto') !== 'https') { res.redirect(
https://${req.header('host')}${req.url}
); } else { next(); } }); }
// Add security headers app.use((req, res, next) => { res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-XSS-Protection', '1; mode=block'); res.setHeader('Content-Security-Policy', "default-src 'self'"); next(); });
Conclusion
Implementing robust authentication and authorization in MCP servers requires careful consideration of security requirements, user experience, and system architecture. Key takeaways:
- Choose the right auth method for your use case
- Layer security with defense in depth
- Validate everything from the client
- Audit all access for security monitoring
- Keep tokens secure with proper storage
- Update regularly to patch vulnerabilities
Remember that security is an ongoing process. Regular security audits, penetration testing, and staying updated with the latest security practices are essential for maintaining secure MCP deployments.