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
- “How would you secure an AI tool server?”
- Discuss defense in depth: authentication, authorization, rate limiting, audit logging.
- “What is the difference between authentication and authorization?”
- Authentication: verifying identity (“Who are you?”)
- Authorization: checking permissions (“What can you do?”)
- “How do you implement rate limiting in a distributed system?”
- Token bucket, sliding window, distributed counters (Redis).
- “What should be included in security audit logs?”
- Who, what, when, where, decision, but be careful of PII.
- “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:
- Add JWT token generation with refresh tokens
- Implement mTLS for service-to-service authentication
- Add IP-based restrictions for additional security
- Implement secret rotation without downtime
- Add anomaly detection for unusual access patterns
Common Pitfalls
- Storing secrets in code - Always use environment variables or secret managers
- Logging sensitive data - Redact passwords, tokens, PII
- Missing rate limit on authentication - Prevents brute force
- Not validating JWT expiration - Expired tokens should fail
- 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