Middleware Architecture
Seed uses Express middleware to implement cross-cutting concerns including security, authentication, metrics collection, and rate limiting. Middleware is applied in a specific order to ensure proper request processing.
Overview
The middleware stack processes every HTTP request through multiple layers:
- HTTP Logging - Request/response logging with Morgan
- Security Headers - Helmet security headers (CSP, HSTS, etc.)
- Metrics Collection - Prometheus metrics for HTTP requests
- CORS - Cross-origin resource sharing policy
- Body Parsing - JSON and URL-encoded body parsing
- Origin Validation - DNS rebinding protection
- Authentication - JWT validation
- Rate Limiting - Per-endpoint rate limiting (MCP, DCR)
- Route Handlers - Actual endpoint logic
Middleware Execution Order
File: src/app.ts
export const app = express();
// 1. Request logging
app.use(morgan(process.env.NODE_ENV === "production" ? "combined" : "dev"));
// 2. Security headers - Apply BEFORE other middleware
app.use(helmet(config.helmet));
// 3. Metrics collection (tracks all requests)
if (config.metrics.enabled) {
app.use(metricsMiddleware);
}
// 4. CORS
app.use(cors(config.cors));
// 5. Body parsers
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 6. Origin validation (DNS rebinding protection)
app.use(validateOrigin);
// 7. Authentication (JWT validation)
app.use(authMiddleware);
// 8. Routes (includes per-route rate limiting)
app.use(routes);HTTP Logging
Library: Morgan File: src/app.tsPosition: First middleware (logs all requests)
Configuration
app.use(morgan(process.env.NODE_ENV === "production" ? "combined" : "dev"));Formats:
- production:
combined- Apache combined log format - development:
dev- Colorized, concise output
Combined Format:
:remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"Dev Format:
:method :url :status :response-time ms - :res[content-length]Output: Logs to stdout (captured by Winston in production)
Security Headers (Helmet)
Library: Helmet File: src/middleware/helmet.tsPosition: Applied before other middleware to ensure headers on all responses
Configuration
Seed uses two Helmet configurations:
1. Full Configuration (Documentation routes):
app.use(helmet(config.helmet));Includes:
- Content-Security-Policy: XSS protection
- Strict-Transport-Security (HSTS): HTTPS enforcement (production only)
- X-Frame-Options: Clickjacking prevention
- X-Content-Type-Options: MIME sniffing prevention
- Referrer-Policy: Referrer information control
- Cross-Origin-*-Policy: Cross-origin behavior control
2. Relaxed Configuration (API routes):
app.use(helmet(config.helmetApi));Same as above but with CSP disabled (API endpoints return JSON, not HTML).
Environment Detection
strictTransportSecurity: process.env.NODE_ENV === "production"
? {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true,
}
: falseHSTS is only enabled in production to avoid browser caching issues in development.
See: Security for detailed security header documentation.
Metrics Collection
File: src/middleware/metrics.tsPosition: Applied after Helmet, before route handlers
Purpose
Collects Prometheus metrics for all HTTP requests to monitor:
- Request duration (histogram)
- Request count (counter)
- Status code distribution
- Route performance
Implementation
export function metricsMiddleware(req: Request, res: Response, next: NextFunction): void {
const start = Date.now();
// Capture response finish event
res.on("finish", () => {
const duration = (Date.now() - start) / 1000; // Convert to seconds
const route = req.route?.path ?? req.path;
// Record duration histogram
httpRequestDuration.observe(
{ method: req.method, route, status_code: res.statusCode },
duration
);
// Increment request counter
httpRequestTotal.inc({
method: req.method,
route,
status_code: res.statusCode,
});
});
next();
}Metrics
httpRequestDuration:
const httpRequestDuration = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request duration in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5],
});httpRequestTotal:
const httpRequestTotal = new promClient.Counter({
name: 'http_request_total',
help: 'Total HTTP requests',
labelNames: ['method', 'route', 'status_code'],
});Conditional Application
if (config.metrics.enabled) {
app.use(metricsMiddleware);
}Controlled by METRICS_ENABLED environment variable (default: true).
See: Observability for complete metrics documentation.
CORS Configuration
Library: cors File: src/config/cors.tsPosition: Applied after metrics, before body parsers
Configuration
export const corsConfig = {
origin: [
/^http:\/\/localhost(:\d+)?$/, // http://localhost:*
"https://claude.ai", // Claude web app
/^https:\/\/.*\.anthropic\.com$/, // Anthropic domains
...(process.env.CORS_EXTRA_ORIGINS?.split(",") || []),
],
credentials: true,
allowedHeaders: [
"Content-Type",
"Accept",
"Authorization",
"Mcp-Session-Id",
"Last-Event-ID",
],
exposedHeaders: [
"Content-Type",
"Mcp-Session-Id",
],
};Allowed Origins
Default Origins:
http://localhost:*- Any port for local developmenthttps://claude.ai- Claude web applicationhttps://*.anthropic.com- All Anthropic subdomains
Custom Origins:
CORS_EXTRA_ORIGINS=https://app1.example.com,https://app2.example.comHeaders
Allowed Request Headers:
Content-Type- JSON content typeAccept- Content negotiationAuthorization- Bearer tokensMcp-Session-Id- MCP session identifierLast-Event-ID- Server-sent events (future use)
Exposed Response Headers:
Content-Type- Response formatMcp-Session-Id- Session identifier (for new sessions)
Body Parsers
Position: Applied after CORS, before origin validation
JSON Parser
app.use(express.json());Parses JSON request bodies for Content-Type: application/json.
Limits: Default Express limits apply (100kb)
URL-Encoded Parser
app.use(express.urlencoded({ extended: true }));Parses URL-encoded bodies for Content-Type: application/x-www-form-urlencoded.
Options:
extended: true- Usesqslibrary for rich object parsing- Used by OAuth token endpoint
Origin Validation
File: src/middleware/origin-validation.tsPosition: Applied after body parsing, before authentication
Purpose
Protects against DNS rebinding attacks by validating the Origin header against an allowed list.
How It Works
export function validateOrigin(req: Request, res: Response, next: NextFunction): void {
// Skip if disabled
if (!config.security.originValidation.enabled) {
return next();
}
const origin = req.headers.origin;
// Allow requests without Origin header (CLI tools, native apps)
if (!origin) {
return next();
}
// Check against allowed origins
if (isAllowedOrigin(origin)) {
return next();
}
// Block and log
logSecurityEvent("origin_blocked", "medium", {
origin,
path: req.path,
ip: req.ip,
});
res.status(403).json({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Forbidden",
data: {
reason: "invalid_origin",
details: "Origin not allowed by server policy",
},
},
id: null,
});
}Pattern Matching
Exact Match:
origin === pattern // "https://claude.ai"Wildcard Subdomain:
pattern: "*.anthropic.com"
matches: "https://console.anthropic.com", "https://api.anthropic.com"Wildcard Port:
pattern: "http://localhost:*"
matches: "http://localhost:3000", "http://localhost:8080"Configuration
Allowed Origins:
export const securityConfig = {
originValidation: {
enabled: process.env.ORIGIN_VALIDATION_ENABLED !== "false",
},
allowedOrigins: [
"http://localhost",
"http://localhost:*",
"http://127.0.0.1",
"http://127.0.0.1:*",
"https://localhost",
"https://localhost:*",
"https://127.0.0.1",
"https://127.0.0.1:*",
"https://claude.ai",
"*.anthropic.com",
...(process.env.ALLOWED_ORIGINS?.split(",").map((o) => o.trim()) ?? []),
],
};Environment Variables:
ORIGIN_VALIDATION_ENABLED=true # Enable/disable origin validation
ALLOWED_ORIGINS=https://app1.example.com,https://app2.example.comDNS Rebinding Protection
Attack Scenario:
- Attacker serves malicious webpage at
attacker.com - Page makes requests to
localhost:3000via JavaScript - Without origin validation, requests would succeed
Protection:
- Origin header shows
https://attacker.com - Not in allowed list → Request blocked
- Prevents unauthorized access from malicious sites
Note: Requests without Origin header are allowed (CLI tools, Postman, etc.).
Logging
Blocked Origins:
logSecurityEvent("origin_blocked", "medium", {
origin: "https://malicious.com",
path: "/mcp",
ip: "203.0.113.45",
});See: Security for security architecture details.
Authentication
File: src/middleware/auth.tsPosition: Applied after origin validation, before route handlers
Purpose
Validates JWT bearer tokens on all requests except public paths.
Public Paths
The following paths bypass authentication:
/health/.well-known/oauth-protected-resource/.well-known/oauth-authorization-server/oauth/token/oauth/authorize/oauth/register/metrics(if metrics enabled)- Documentation paths (
/,/assets/**, etc.)
Implementation Summary
export async function authMiddleware(req: Request, res: Response, next: NextFunction) {
// Skip if auth disabled globally
if (!config.authRequired) {
return next();
}
// Skip for public paths
if (isPublicPath(req.path)) {
return next();
}
// Extract Bearer token
const token = extractBearerToken(req);
// Verify JWT with JWKS
const { payload } = await jwtVerify(token, jwksService.getKey, {
issuer: config.oidc.issuer,
audience: config.oidc.audience,
});
// Extract user context
req.user = {
sub: payload.sub,
email: payload.email,
name: payload.name,
groups: payload.groups,
token,
};
next();
}See: Authentication Flow for detailed JWT validation.
Rate Limiting
File: src/middleware/distributed-rate-limit.tsPosition: Applied per-route (not globally)
Per-Route Application
Rate limiting is applied to specific endpoints:
MCP Endpoints:
// src/routes/mcp.ts
const mcpRateLimiter = createDistributedRateLimiter({
windowMs: config.rateLimit.mcp.windowMs,
maxRequests: config.rateLimit.mcp.maxRequests,
globalMax: config.rateLimit.mcp.globalMax,
keyPrefix: 'ratelimit:mcp:',
endpointType: 'mcp',
});
router.post('/mcp', mcpRateLimiter, handleMcpRequest);
router.delete('/mcp', mcpRateLimiter, handleMcpDelete);DCR Endpoint:
// src/routes/oauth-register.ts
const dcrRateLimiter = createDistributedRateLimiter({
windowMs: config.rateLimit.dcr.windowMs,
maxRequests: config.rateLimit.dcr.maxRequests,
globalMax: config.rateLimit.dcr.globalMax,
keyPrefix: 'ratelimit:dcr:',
endpointType: 'dcr',
});
router.post('/oauth/register', dcrRateLimiter, handleRegistration);Features
- Redis-backed sliding window - Distributed across servers
- Per-IP limiting - Protects against single-source attacks
- Global limiting - Protects against distributed attacks
- Graceful degradation - Falls back if Redis unavailable
See: Rate Limiting for complete documentation.
Middleware Best Practices
Ordering Principles
- Logging First: Capture all requests for debugging
- Security Early: Apply headers before processing
- Metrics Before Routes: Track all request performance
- CORS Before Body Parsing: Reject invalid origins early
- Validation Before Auth: Cheap checks before expensive ones
- Auth Before Business Logic: Protect all endpoints by default
Error Handling
All middleware should handle errors gracefully:
export function myMiddleware(req: Request, res: Response, next: NextFunction): void {
try {
// Middleware logic
next();
} catch (error) {
logger.error('Middleware error', { error, middleware: 'myMiddleware' });
res.status(500).json({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Internal Server Error",
},
id: null,
});
}
}Async Middleware
Use async/await for async operations:
export async function asyncMiddleware(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
try {
await someAsyncOperation();
next();
} catch (error) {
next(error); // Pass to error handler
}
}Conditional Application
Some middleware should be conditional:
// Only apply if enabled
if (config.metrics.enabled) {
app.use(metricsMiddleware);
}
// Only apply to specific paths
app.use('/api/*', apiMiddleware);
// Only apply to specific methods
router.post('/endpoint', middleware, handler);Configuration Reference
Environment Variables
# Metrics
METRICS_ENABLED=true
# Origin Validation
ORIGIN_VALIDATION_ENABLED=true
ALLOWED_ORIGINS=https://app1.example.com,https://app2.example.com
# CORS
CORS_EXTRA_ORIGINS=https://app1.example.com,https://app2.example.com
# Authentication
AUTH_REQUIRED=true
OIDC_ISSUER=https://auth.example.com/application/o/my-app/
OIDC_AUDIENCE=my-client-id
# Rate Limiting
RATE_LIMIT_ENABLED=true
MCP_RATE_LIMIT_MAX=100
DCR_RATE_LIMIT_MAX=10
# Security Headers (Helmet)
NODE_ENV=production # Enables HSTSImplementation Files
- Application Setup:
src/app.ts- Middleware registration - Metrics:
src/middleware/metrics.ts- Prometheus metrics collection - Origin Validation:
src/middleware/origin-validation.ts- DNS rebinding protection - Authentication:
src/middleware/auth.ts- JWT validation - Rate Limiting:
src/middleware/distributed-rate-limit.ts- Redis-backed rate limiter - Config:
src/config/helmet.ts,src/config/cors.ts,src/config/security.ts
Related Documentation
- Authentication Flow - JWT validation details
- Rate Limiting - Complete rate limiting documentation
- Security - Security architecture and headers
- Observability - Metrics and logging
- Configuration System - Environment configuration