Project 19: MCP Server Authentication & Security

Project 19: MCP Server Authentication & Security


Project Overview

Attribute Value
File P19-mcp-authentication.md
Main Programming Language TypeScript
Alternative Programming Languages Python, Go, Rust
Coolness Level Level 3: Genuinely Clever
Business Potential 3. The “Service & Support” Model
Difficulty Level 3: Advanced
Knowledge Area MCP / Security / Authentication
Software or Tool MCP SDK, JWT, OAuth
Main Book “Security in Computing” by Pfleeger
Time Estimate 2 weeks
Prerequisites Projects 15-18 completed, security fundamentals

What You Will Build

A secure MCP server with: authentication (API keys, OAuth, mTLS), authorization (role-based tool access), audit logging, rate limiting, and secure secret handling. Implements defense in depth.

Production MCP servers need security. This project teaches you how to build secure services that handle authentication, authorization, and protect sensitive operations.


Real World Outcome

# Server startup with security enabled
$ mcp-server --auth-mode=oauth --audit-log=/var/log/mcp-audit.log

MCP Server starting...
  [OK] OAuth token validation enabled
  [OK] Role-based access control active
  [OK] Audit logging to /var/log/mcp-audit.log
  [OK] Rate limiting: 100 req/min per user
Server ready on stdio

# Claude tries to access restricted tool:
You: Delete all user data

Claude: [Invokes mcp__secure__delete_all_users]

ACCESS DENIED
--------------
Tool: delete_all_users
Required role: admin
Your role: developer
Action: Blocked and logged

This operation requires admin privileges.
Please contact your administrator.

# In audit log:
[2025-12-22T10:15:32Z] DENIED user=dev@example.com tool=delete_all_users role=developer required=admin

The Core Question You Are Answering

“How do I build secure MCP servers that authenticate users, authorize operations, and maintain audit trails?”

MCP servers often access sensitive systems. This project teaches you to build secure servers that protect against unauthorized access and maintain compliance.


Security Pipeline Architecture

+------------------------------------------------------------------+
|                    SECURITY PIPELINE                               |
+------------------------------------------------------------------+
|                                                                    |
|  Incoming MCP Request                                              |
|           |                                                        |
|           v                                                        |
|  +------------------------------------------------------------+   |
|  | 1. AUTHENTICATE                                             |   |
|  |    Verify identity                                          |   |
|  |                                                             |   |
|  |    Methods:                                                 |   |
|  |    - API Key: Check header/env variable                    |   |
|  |    - OAuth:   Validate JWT signature & expiry              |   |
|  |    - mTLS:    Verify client certificate                    |   |
|  +------------------------------------------------------------+   |
|           | Identity verified                                      |
|           v                                                        |
|  +------------------------------------------------------------+   |
|  | 2. RATE LIMIT                                               |   |
|  |    Check quotas                                             |   |
|  |                                                             |   |
|  |    Track: requests per user per time window                |   |
|  |    Limit: 100 requests/minute                              |   |
|  |    Response: 429 Too Many Requests if exceeded             |   |
|  +------------------------------------------------------------+   |
|           | Within limits                                          |
|           v                                                        |
|  +------------------------------------------------------------+   |
|  | 3. AUTHORIZE                                                |   |
|  |    Check permissions                                        |   |
|  |                                                             |   |
|  |    User role:    developer                                  |   |
|  |    Tool:         delete_users                              |   |
|  |    Required:     admin                                      |   |
|  |    Decision:     DENIED                                     |   |
|  +------------------------------------------------------------+   |
|           | Decision made                                          |
|           v                                                        |
|  +------------------------------------------------------------+   |
|  | 4. AUDIT LOG                                                |   |
|  |    Record decision                                          |   |
|  |                                                             |   |
|  |    Log: who, what, when, from where, decision              |   |
|  |    Format: JSON for structured querying                    |   |
|  +------------------------------------------------------------+   |
|           |                                                        |
|           v                                                        |
|  Response: Access Denied / Tool Result                             |
|                                                                    |
+------------------------------------------------------------------+

Authentication Methods Comparison

+------------------------------------------------------------------+
|                   AUTHENTICATION METHODS                           |
+------------------------------------------------------------------+

1. API KEY
   +----------------+
   | Simple token   |
   | in header/env  |
   +----------------+

   Pros: Simple to implement, no external dependencies
   Cons: Shared secret, manual rotation, no user identity

   Example:
   Authorization: Bearer sk_live_abc123...

   Use when: Internal tools, development, simple deployments


2. OAUTH 2.0 / JWT
   +----------------+
   | Token issued   |
   | by auth server |
   +----------------+

   Pros: Industry standard, user identity, expiry, scopes
   Cons: Requires auth infrastructure, token management

   Example:
   Authorization: Bearer eyJhbGc...

   JWT Structure:
   {
     "header": {"alg": "RS256", "typ": "JWT"},
     "payload": {
       "sub": "user@example.com",
       "roles": ["developer"],
       "exp": 1734864000
     },
     "signature": "..."
   }

   Use when: Multi-user, production, compliance requirements


3. mTLS (Mutual TLS)
   +----------------+
   | Client cert    |
   | verified by    |
   | server         |
   +----------------+

   Pros: Strong identity, no tokens to steal, automatic
   Cons: Certificate management complexity, infrastructure

   Example:
   Client presents certificate during TLS handshake
   Server validates against CA

   Use when: Service-to-service, high security, zero trust

Role-Based Access Control (RBAC)

+------------------------------------------------------------------+
|                    RBAC MODEL                                      |
+------------------------------------------------------------------+

ROLES                          PERMISSIONS
+----------+                   +-----------------------------------+
| admin    |------------------>| ALL tools                         |
+----------+                   +-----------------------------------+

+----------+                   +-----------------------------------+
| developer|------------------>| query, list_tables, describe,     |
+----------+                   | create_pr, list_prs               |
                               +-----------------------------------+

+----------+                   +-----------------------------------+
| viewer   |------------------>| list_tables, describe_table       |
+----------+                   +-----------------------------------+


TOOL REQUIREMENTS MATRIX
+--------------------+---------------------------+
| Tool               | Required Roles            |
+--------------------+---------------------------+
| delete_users       | admin                     |
| drop_table         | admin                     |
| execute_sql        | admin, developer          |
| query              | admin, developer, viewer  |
| list_tables        | admin, developer, viewer  |
+--------------------+---------------------------+

AUTHORIZATION CHECK
  user_roles = ["developer"]
  tool = "delete_users"
  required = ["admin"]

  has_permission = any(role in required for role in user_roles)
  # False -> DENIED

Concepts You Must Understand First

Stop and research these before coding:

1. Authentication Methods

Method Strength Complexity Use Case
API Keys Weak Low Internal, dev
OAuth 2.0 Strong Medium User-facing
mTLS Strongest High Service-to-service

Reference: “Security in Computing” Ch. 4

2. Authorization Models

Model Description Flexibility
RBAC Role-based access control Medium
ABAC Attribute-based access control High
ReBAC Relationship-based access control Very High

Key principle: Least Privilege - Grant minimum access required.

Reference: “Security in Computing” Ch. 5

3. Audit Logging

What to log (the 5 W’s):

  • Who: User identity
  • What: Action attempted
  • When: Timestamp (ISO 8601)
  • Where: Source IP, client info
  • Why: Decision (ALLOWED/DENIED) and reason

Reference: OWASP Logging Cheat Sheet


Questions to Guide Your Design

Before implementing, think through these:

1. Authentication Strategy

Question Options
How do users authenticate? API key, OAuth token, client certificate
Where are credentials validated? Locally, external auth server
How do you handle token refresh? Automatic renewal, re-auth required
What happens on auth failure? Clear error, no information leakage

2. Authorization Rules

Question Considerations
What roles exist? admin, developer, viewer, service
Which tools require which roles? Sensitivity-based mapping
How do you define rules? Configuration file, database
Are rules static or dynamic? Runtime changes, caching

3. Security Hardening

Layer Implementation
Rate limiting Token bucket, sliding window
Input validation Schema validation, sanitization
Secret rotation Versioned secrets, graceful rollover
Secure transport TLS 1.3, certificate pinning

Thinking Exercise

Design the Security Layer

Consider a request flow and identify security checkpoints:

Request: Claude calls "delete_users" tool
  |
  +-- 1. Is the request properly formatted? (Input validation)
  |
  +-- 2. Who is making this request? (Authentication)
  |       - Extract token from request
  |       - Validate signature
  |       - Check expiration
  |       - Extract user identity
  |
  +-- 3. Are they within rate limits? (Rate limiting)
  |       - Check user's request count
  |       - Enforce quota
  |
  +-- 4. Can they perform this action? (Authorization)
  |       - Get user's roles
  |       - Check tool requirements
  |       - Make allow/deny decision
  |
  +-- 5. Record the decision (Audit logging)
  |       - Log all details
  |       - Ensure tamper resistance
  |
  +-- 6. Execute or reject (Response)

Questions to consider:

  • What happens if authentication fails?
  • How do you handle graceful degradation?
  • Should audit logs include request content (PII concerns)?
  • How do you prevent log injection attacks?

The Interview Questions They Will Ask

  1. “How would you secure an AI tool server?”
    • Discuss defense in depth: authentication, authorization, rate limiting, audit logging.
  2. “What is the difference between authentication and authorization?”
    • Authentication: verifying identity (“Who are you?”)
    • Authorization: checking permissions (“What can you do?”)
  3. “How do you implement rate limiting in a distributed system?”
    • Token bucket, sliding window, distributed counters (Redis).
  4. “What should be included in security audit logs?”
    • Who, what, when, where, decision, but be careful of PII.
  5. “How do you handle secrets in service configurations?”
    • Environment variables, secret managers, never in code.

Hints in Layers

Hint 1: Start with API Keys

The simplest authentication: check for a known key in environment or header:

function authenticate(request: MCPRequest): string | null {
  const token = process.env.MCP_API_KEY;
  const provided = request.metadata?.apiKey;

  if (!token || !provided || token !== provided) {
    return null;  // Authentication failed
  }
  return "api-key-user";  // Return user identifier
}

Hint 2: Add Role Mapping

Create a configuration file mapping users to roles, and tools to required roles:

# security.yaml
roles:
  admin:
    users: ["admin@example.com"]
  developer:
    users: ["dev@example.com", "api-key-user"]
  viewer:
    users: ["viewer@example.com"]

tool_permissions:
  delete_users: ["admin"]
  execute_sql: ["admin", "developer"]
  query: ["admin", "developer", "viewer"]
  list_tables: ["admin", "developer", "viewer"]

Hint 3: Implement Rate Limiting

Use a simple in-memory counter with time windows. For production, use Redis:

const rateLimits: Map<string, { count: number; resetAt: Date }> = new Map();

function checkRateLimit(userId: string): boolean {
  const limit = rateLimits.get(userId);
  const now = new Date();

  if (!limit || now > limit.resetAt) {
    rateLimits.set(userId, {
      count: 1,
      resetAt: new Date(now.getTime() + 60000)  // 1 minute window
    });
    return true;
  }

  if (limit.count >= 100) {
    return false;  // Rate limit exceeded
  }

  limit.count++;
  return true;
}

Hint 4: Log Everything

Log auth attempts, tool calls, and denials. Include enough context to investigate:

function auditLog(entry: AuditEntry): void {
  const log = {
    timestamp: new Date().toISOString(),
    user: entry.user,
    tool: entry.tool,
    decision: entry.decision,
    reason: entry.reason,
    clientInfo: entry.clientInfo,
    requestId: crypto.randomUUID()
  };

  fs.appendFileSync(
    process.env.AUDIT_LOG_PATH || "/var/log/mcp-audit.log",
    JSON.stringify(log) + "\n"
  );
}

Books That Will Help

Topic Book Chapter Why It Helps
Security Fundamentals “Security in Computing” by Pfleeger Ch. 4-5 Authentication & Authorization
OAuth 2.0 “OAuth 2.0 Simplified” by Parecki All Token-based auth
Audit Logging OWASP Logging Cheat Sheet All Compliance & forensics
Cryptography “Serious Cryptography” by Aumasson Ch. 1-4 Understanding primitives

Implementation Skeleton

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import * as jwt from "jsonwebtoken";
import * as fs from "fs";

// Security configuration
interface SecurityConfig {
  authMode: "apikey" | "jwt" | "none";
  jwtSecret?: string;
  rateLimitPerMinute: number;
  auditLogPath: string;
}

interface SecurityContext {
  user: string;
  roles: string[];
  rateLimit: { remaining: number; resetAt: Date };
}

// Role-based permissions
const ROLE_REQUIREMENTS: Record<string, string[]> = {
  delete_users: ["admin"],
  drop_table: ["admin"],
  execute_sql: ["admin", "developer"],
  query: ["admin", "developer", "viewer"],
  list_tables: ["admin", "developer", "viewer"],
  describe_table: ["admin", "developer", "viewer"]
};

// User role assignments (in production, use external system)
const USER_ROLES: Record<string, string[]> = {
  "admin@example.com": ["admin", "developer", "viewer"],
  "dev@example.com": ["developer", "viewer"],
  "viewer@example.com": ["viewer"],
  "api-key-user": ["developer", "viewer"]
};

// Rate limit tracking
const rateLimits: Map<string, { count: number; resetAt: Date }> = new Map();

// Security configuration
const config: SecurityConfig = {
  authMode: (process.env.AUTH_MODE as any) || "apikey",
  jwtSecret: process.env.JWT_SECRET,
  rateLimitPerMinute: parseInt(process.env.RATE_LIMIT || "100"),
  auditLogPath: process.env.AUDIT_LOG || "/var/log/mcp-audit.log"
};


// 1. AUTHENTICATION
async function authenticate(
  metadata: Record<string, any>
): Promise<SecurityContext | null> {
  let user: string | null = null;

  switch (config.authMode) {
    case "apikey":
      const apiKey = metadata?.apiKey || process.env.CLIENT_API_KEY;
      const expectedKey = process.env.MCP_API_KEY;
      if (apiKey && apiKey === expectedKey) {
        user = "api-key-user";
      }
      break;

    case "jwt":
      const token = metadata?.authorization?.replace("Bearer ", "");
      if (token && config.jwtSecret) {
        try {
          const decoded = jwt.verify(token, config.jwtSecret) as any;
          user = decoded.sub;
        } catch (e) {
          // Invalid token
        }
      }
      break;

    case "none":
      user = "anonymous";
      break;
  }

  if (!user) return null;

  const roles = USER_ROLES[user] || [];
  const rateLimit = checkRateLimit(user);

  return { user, roles, rateLimit };
}


// 2. RATE LIMITING
function checkRateLimit(userId: string): { remaining: number; resetAt: Date } {
  const now = new Date();
  let limit = rateLimits.get(userId);

  if (!limit || now > limit.resetAt) {
    limit = {
      count: 0,
      resetAt: new Date(now.getTime() + 60000)
    };
    rateLimits.set(userId, limit);
  }

  limit.count++;

  return {
    remaining: Math.max(0, config.rateLimitPerMinute - limit.count),
    resetAt: limit.resetAt
  };
}


// 3. AUTHORIZATION
function authorize(ctx: SecurityContext, tool: string): boolean {
  const required = ROLE_REQUIREMENTS[tool];

  // If no specific requirements, allow all authenticated users
  if (!required || required.length === 0) {
    return true;
  }

  // Check if user has any of the required roles
  return required.some(role => ctx.roles.includes(role));
}


// 4. AUDIT LOGGING
function auditLog(
  ctx: SecurityContext | null,
  tool: string,
  decision: "ALLOWED" | "DENIED" | "AUTH_FAILED" | "RATE_LIMITED",
  details?: string
): void {
  const entry = {
    timestamp: new Date().toISOString(),
    user: ctx?.user || "unknown",
    roles: ctx?.roles || [],
    tool,
    decision,
    details,
    rateLimit: ctx?.rateLimit
  };

  try {
    fs.appendFileSync(config.auditLogPath, JSON.stringify(entry) + "\n");
  } catch (e) {
    console.error("Failed to write audit log:", e);
  }
}


// Create secure server
const server = new Server(
  { name: "secure-mcp-server", version: "1.0.0" },
  { capabilities: { tools: {} } }
);


// Security middleware wrapper
async function secureToolCall(
  name: string,
  args: any,
  metadata: Record<string, any>,
  handler: (args: any) => Promise<any>
): Promise<any> {
  // 1. Authenticate
  const ctx = await authenticate(metadata);

  if (!ctx) {
    auditLog(null, name, "AUTH_FAILED");
    return {
      content: [{ type: "text", text: "Authentication required" }],
      isError: true
    };
  }

  // 2. Rate limit
  if (ctx.rateLimit.remaining <= 0) {
    auditLog(ctx, name, "RATE_LIMITED");
    return {
      content: [{
        type: "text",
        text: `Rate limit exceeded. Resets at ${ctx.rateLimit.resetAt.toISOString()}`
      }],
      isError: true
    };
  }

  // 3. Authorize
  if (!authorize(ctx, name)) {
    const required = ROLE_REQUIREMENTS[name] || [];
    auditLog(ctx, name, "DENIED", `Required: ${required.join(", ")}`);

    return {
      content: [{
        type: "text",
        text: [
          "ACCESS DENIED",
          "-------------",
          `Tool: ${name}`,
          `Required role: ${required.join(" or ")}`,
          `Your roles: ${ctx.roles.join(", ") || "none"}`,
          "",
          "This operation requires elevated privileges."
        ].join("\n")
      }],
      isError: true
    };
  }

  // 4. Execute and audit
  try {
    const result = await handler(args);
    auditLog(ctx, name, "ALLOWED");
    return result;
  } catch (e) {
    auditLog(ctx, name, "ALLOWED", `Error: ${e.message}`);
    throw e;
  }
}


// Tool handlers
server.setRequestHandler("tools/list", async () => {
  return {
    tools: [
      {
        name: "query",
        description: "Execute a read-only SQL query (requires: developer)",
        inputSchema: {
          type: "object",
          properties: { sql: { type: "string" } },
          required: ["sql"]
        }
      },
      {
        name: "delete_users",
        description: "Delete user records (requires: admin)",
        inputSchema: {
          type: "object",
          properties: { userIds: { type: "array", items: { type: "string" } } },
          required: ["userIds"]
        }
      }
    ]
  };
});


server.setRequestHandler("tools/call", async (request) => {
  const { name, arguments: args } = request.params;
  const metadata = request.params._meta || {};

  return secureToolCall(name, args, metadata, async (args) => {
    // Actual tool implementation
    if (name === "query") {
      // Execute query...
      return {
        content: [{ type: "text", text: "Query results here..." }]
      };
    }

    if (name === "delete_users") {
      // Delete users...
      return {
        content: [{ type: "text", text: `Deleted ${args.userIds.length} users` }]
      };
    }

    throw new Error(`Unknown tool: ${name}`);
  });
});


async function main() {
  console.error("Secure MCP Server starting...");
  console.error(`  Auth mode: ${config.authMode}`);
  console.error(`  Rate limit: ${config.rateLimitPerMinute}/min`);
  console.error(`  Audit log: ${config.auditLogPath}`);

  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Server ready");
}

main().catch(console.error);

Learning Milestones

Milestone What It Proves Verification
Authentication works You understand identity verification Invalid token is rejected
Authorization blocks unauthorized access You understand RBAC Developer cannot delete users
Rate limiting enforces quotas You understand abuse prevention Rapid requests are throttled
Audit logs capture decisions You have built compliance-ready security Logs show who did what

Core Challenges Mapped to Concepts

Challenge Concept Book Reference
Authentication methods Token/certificate handling “Security in Computing” Ch. 4
Authorization rules RBAC implementation “Security in Computing” Ch. 5
Secret management Secure credential storage OWASP Cheat Sheets
Audit logging Compliance requirements OWASP Logging Cheat Sheet

Extension Ideas

Once the basic security works, consider these enhancements:

  1. Add JWT token generation with refresh tokens
  2. Implement mTLS for service-to-service authentication
  3. Add IP-based restrictions for additional security
  4. Implement secret rotation without downtime
  5. Add anomaly detection for unusual access patterns

Common Pitfalls

  1. Storing secrets in code - Always use environment variables or secret managers
  2. Logging sensitive data - Redact passwords, tokens, PII
  3. Missing rate limit on authentication - Prevents brute force
  4. Not validating JWT expiration - Expired tokens should fail
  5. Information leakage in errors - Do not reveal internal details

Success Criteria

You have completed this project when:

  • Authentication rejects invalid credentials
  • Authorization blocks unauthorized tool access
  • Rate limiting enforces request quotas
  • Audit logs capture all access attempts
  • Secrets are not hardcoded in source
  • Error messages do not leak sensitive information
  • Security can be configured via environment variables
  • The security pipeline is applied consistently to all tools