Files
gh-jezweb-claude-skills-ski…/references/authentication-guide.md
2025-11-30 08:25:43 +08:00

11 KiB

Authentication Guide for MCP Servers

Complete guide to implementing authentication in TypeScript MCP servers on Cloudflare Workers.


Why Authentication Matters

Without authentication:

  • Anyone can access your tools
  • API abuse and DDoS attacks
  • Data leaks and security breaches
  • Unexpected Cloudflare costs

With authentication:

  • Controlled access
  • Usage tracking
  • Rate limiting per user
  • Audit trails

Best for: Most use cases, simple setup, good security.

Setup

1. Create KV namespace:

wrangler kv namespace create MCP_API_KEYS

2. Add to wrangler.jsonc:

{
  "kv_namespaces": [
    {
      "binding": "MCP_API_KEYS",
      "id": "YOUR_NAMESPACE_ID"
    }
  ]
}

3. Generate API keys:

# Generate secure key
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

# Add to KV
wrangler kv key put --binding=MCP_API_KEYS "key:abc123xyz..." "true"

Implementation

import { Hono } from 'hono';

type Env = {
  MCP_API_KEYS: KVNamespace;
};

const app = new Hono<{ Bindings: Env }>();

// Authentication middleware
app.use('/mcp', async (c, next) => {
  const authHeader = c.req.header('Authorization');

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return c.json({ error: 'Unauthorized' }, 401);
  }

  const apiKey = authHeader.replace('Bearer ', '');
  const isValid = await c.env.MCP_API_KEYS.get(`key:${apiKey}`);

  if (!isValid) {
    return c.json({ error: 'Invalid API key' }, 403);
  }

  // Optional: Track usage
  const usageKey = `usage:${apiKey}:${new Date().toISOString().split('T')[0]}`;
  const count = await c.env.MCP_API_KEYS.get(usageKey);
  await c.env.MCP_API_KEYS.put(
    usageKey,
    String(parseInt(count || '0') + 1),
    { expirationTtl: 86400 * 7 }
  );

  await next();
});

app.post('/mcp', async (c) => {
  // MCP handler (user is authenticated)
});

export default app;

Key Management

# List all keys
wrangler kv key list --binding=MCP_API_KEYS --prefix="key:"

# Add new key
wrangler kv key put --binding=MCP_API_KEYS "key:newkey123" "true"

# Revoke key
wrangler kv key delete --binding=MCP_API_KEYS "key:oldkey456"

# Check usage
wrangler kv key get --binding=MCP_API_KEYS "usage:abc123:2025-10-28"

Client Usage

curl -X POST https://mcp.example.com/mcp \
  -H "Authorization: Bearer abc123xyz..." \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'

Method 2: Cloudflare Zero Trust Access

Best for: Enterprise deployments, SSO integration, team access.

Setup

1. Configure Cloudflare Access:

  • Go to Zero Trust dashboard
  • Create Access Application
  • Set application domain (e.g., mcp.example.com)
  • Configure identity providers (Google, GitHub, SAML)
  • Set access policies (email domains, groups)

2. Install JWT verification:

npm install @tsndr/cloudflare-worker-jwt

3. Implementation:

import { verify } from '@tsndr/cloudflare-worker-jwt';

type Env = {
  CF_ACCESS_TEAM_DOMAIN: string; // e.g., "yourteam.cloudflareaccess.com"
};

app.use('/mcp', async (c, next) => {
  const jwt = c.req.header('Cf-Access-Jwt-Assertion');

  if (!jwt) {
    return c.json({ error: 'Access denied' }, 403);
  }

  try {
    // Verify JWT
    const isValid = await verify(
      jwt,
      `https://${c.env.CF_ACCESS_TEAM_DOMAIN}/cdn-cgi/access/certs`
    );

    if (!isValid) {
      return c.json({ error: 'Invalid token' }, 403);
    }

    // Decode to get user info
    const payload = JSON.parse(atob(jwt.split('.')[1]));

    // Optional: Add user to request context
    c.set('user', payload);

    await next();
  } catch (error) {
    return c.json({ error: 'Authentication failed' }, 403);
  }
});

Benefits

  • SSO with Google, GitHub, Okta, etc.
  • Team-based access control
  • Automatic user management
  • Audit logs in Zero Trust dashboard

Method 3: OAuth 2.0

Best for: Public APIs, third-party integrations, user consent flows.

Setup

1. Choose OAuth provider:

  • Auth0
  • Clerk
  • Supabase
  • Custom OAuth server

2. Install dependencies:

npm install oauth4webapi

3. Implementation:

import * as oauth from 'oauth4webapi';

type Env = {
  OAUTH_CLIENT_ID: string;
  OAUTH_CLIENT_SECRET: string;
  OAUTH_ISSUER: string; // e.g., "https://yourdomain.auth0.com"
};

app.use('/mcp', async (c, next) => {
  const authHeader = c.req.header('Authorization');

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return c.json({ error: 'Unauthorized' }, 401);
  }

  const accessToken = authHeader.replace('Bearer ', '');

  try {
    // Validate token with OAuth provider
    const issuer = new URL(c.env.OAUTH_ISSUER);
    const client = {
      client_id: c.env.OAUTH_CLIENT_ID,
      client_secret: c.env.OAUTH_CLIENT_SECRET
    };

    const response = await oauth.introspectionRequest(
      issuer,
      client,
      accessToken
    );

    const result = await oauth.processIntrospectionResponse(issuer, client, response);

    if (!result.active) {
      return c.json({ error: 'Invalid token' }, 403);
    }

    // Token is valid, user info in result
    c.set('user', result);

    await next();
  } catch (error) {
    return c.json({ error: 'Authentication failed' }, 403);
  }
});

Method 4: JWT (Custom)

Best for: Microservices, existing JWT infrastructure.

Implementation

import { verify, sign } from '@tsndr/cloudflare-worker-jwt';

type Env = {
  JWT_SECRET: string;
};

app.use('/mcp', async (c, next) => {
  const authHeader = c.req.header('Authorization');

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return c.json({ error: 'Unauthorized' }, 401);
  }

  const token = authHeader.replace('Bearer ', '');

  try {
    const isValid = await verify(token, c.env.JWT_SECRET);

    if (!isValid) {
      return c.json({ error: 'Invalid token' }, 403);
    }

    // Decode payload
    const payload = JSON.parse(atob(token.split('.')[1]));

    // Check expiration
    if (payload.exp && payload.exp < Date.now() / 1000) {
      return c.json({ error: 'Token expired' }, 403);
    }

    c.set('user', payload);

    await next();
  } catch (error) {
    return c.json({ error: 'Authentication failed' }, 403);
  }
});

// Generate JWT endpoint (for testing)
app.post('/auth/token', async (c) => {
  const { username, password } = await c.req.json();

  // Validate credentials (implement your logic)
  if (username === 'admin' && password === 'secret') {
    const token = await sign({
      sub: username,
      exp: Math.floor(Date.now() / 1000) + 3600 // 1 hour
    }, c.env.JWT_SECRET);

    return c.json({ token });
  }

  return c.json({ error: 'Invalid credentials' }, 401);
});

Method 5: mTLS (Mutual TLS)

Best for: High-security environments, machine-to-machine communication.

Note: Cloudflare Workers support mTLS for enterprise customers.

Setup

1. Enable mTLS in Cloudflare dashboard:

  • Go to SSL/TLS → Client Certificates
  • Create client certificate
  • Download certificate and private key
  • Configure API Shield mTLS

2. Implementation:

app.use('/mcp', async (c, next) => {
  const clientCert = c.req.header('Cf-Client-Cert-Der-Base64');

  if (!clientCert) {
    return c.json({ error: 'Client certificate required' }, 401);
  }

  // Cloudflare validates certificate automatically
  // You can add additional validation here

  await next();
});

Combined Authentication

Use multiple methods for flexibility:

app.use('/mcp', async (c, next) => {
  const authHeader = c.req.header('Authorization');

  if (!authHeader) {
    return c.json({ error: 'Unauthorized' }, 401);
  }

  // Try API Key
  if (authHeader.startsWith('Bearer ')) {
    const token = authHeader.replace('Bearer ', '');

    // Check if it's an API key
    const isApiKey = await c.env.MCP_API_KEYS.get(`key:${token}`);
    if (isApiKey) {
      return next();
    }

    // Check if it's a JWT
    try {
      const isValid = await verify(token, c.env.JWT_SECRET);
      if (isValid) {
        return next();
      }
    } catch {}
  }

  return c.json({ error: 'Invalid credentials' }, 403);
});

Security Best Practices

Do's

  • Use HTTPS only (enforced by Cloudflare)
  • Generate strong API keys (32+ bytes)
  • Implement rate limiting per key
  • Track usage per key
  • Rotate keys regularly
  • Store secrets in Wrangler secrets
  • Log authentication failures
  • Set token expiration
  • Validate all inputs
  • Use environment-specific keys

Don'ts

  • Never hardcode API keys
  • Don't log auth tokens
  • Avoid weak tokens (< 16 bytes)
  • Don't expose auth endpoints publicly
  • Never return detailed auth errors to clients
  • Don't skip CORS validation
  • Avoid storing tokens in localStorage (use httpOnly cookies)

Testing Authentication

Local Testing

# Start dev server
wrangler dev

# Test with curl
curl -X POST http://localhost:8787/mcp \
  -H "Authorization: Bearer YOUR_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'

Production Testing

# Test authentication failure
curl -X POST https://mcp.example.com/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
# Expected: 401 Unauthorized

# Test with valid key
curl -X POST https://mcp.example.com/mcp \
  -H "Authorization: Bearer VALID_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
# Expected: 200 OK + tool list

Migration Guide

Moving from No Auth → API Key Auth

1. Deploy with optional auth:

app.use('/mcp', async (c, next) => {
  const authHeader = c.req.header('Authorization');

  if (authHeader) {
    // Validate if provided
    const apiKey = authHeader.replace('Bearer ', '');
    const isValid = await c.env.MCP_API_KEYS.get(`key:${apiKey}`);

    if (!isValid) {
      return c.json({ error: 'Invalid API key' }, 403);
    }
  }
  // Continue even without auth (for migration period)
  await next();
});

2. Notify clients to add auth

3. Make auth required after transition:

app.use('/mcp', async (c, next) => {
  const authHeader = c.req.header('Authorization');

  if (!authHeader) {
    return c.json({ error: 'Authentication required' }, 401);
  }

  // ... validate
});

Last Updated: 2025-10-28 Verified With: Cloudflare Workers, @modelcontextprotocol/sdk@1.20.2