Skip to content

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:

  1. HTTP Logging - Request/response logging with Morgan
  2. Security Headers - Helmet security headers (CSP, HSTS, etc.)
  3. Metrics Collection - Prometheus metrics for HTTP requests
  4. CORS - Cross-origin resource sharing policy
  5. Body Parsing - JSON and URL-encoded body parsing
  6. Origin Validation - DNS rebinding protection
  7. Authentication - JWT validation
  8. Rate Limiting - Per-endpoint rate limiting (MCP, DCR)
  9. Route Handlers - Actual endpoint logic

Middleware Execution Order

File: src/app.ts

typescript
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

typescript
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):

typescript
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):

typescript
app.use(helmet(config.helmetApi));

Same as above but with CSP disabled (API endpoints return JSON, not HTML).

Environment Detection

typescript
strictTransportSecurity: process.env.NODE_ENV === "production"
  ? {
      maxAge: 31536000,        // 1 year
      includeSubDomains: true,
      preload: true,
    }
  : false

HSTS 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

typescript
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:

typescript
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:

typescript
const httpRequestTotal = new promClient.Counter({
  name: 'http_request_total',
  help: 'Total HTTP requests',
  labelNames: ['method', 'route', 'status_code'],
});

Conditional Application

typescript
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

typescript
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:

  1. http://localhost:* - Any port for local development
  2. https://claude.ai - Claude web application
  3. https://*.anthropic.com - All Anthropic subdomains

Custom Origins:

bash
CORS_EXTRA_ORIGINS=https://app1.example.com,https://app2.example.com

Headers

Allowed Request Headers:

  • Content-Type - JSON content type
  • Accept - Content negotiation
  • Authorization - Bearer tokens
  • Mcp-Session-Id - MCP session identifier
  • Last-Event-ID - Server-sent events (future use)

Exposed Response Headers:

  • Content-Type - Response format
  • Mcp-Session-Id - Session identifier (for new sessions)

Body Parsers

Position: Applied after CORS, before origin validation

JSON Parser

typescript
app.use(express.json());

Parses JSON request bodies for Content-Type: application/json.

Limits: Default Express limits apply (100kb)

URL-Encoded Parser

typescript
app.use(express.urlencoded({ extended: true }));

Parses URL-encoded bodies for Content-Type: application/x-www-form-urlencoded.

Options:

  • extended: true - Uses qs library 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

typescript
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:

typescript
origin === pattern  // "https://claude.ai"

Wildcard Subdomain:

typescript
pattern: "*.anthropic.com"
matches: "https://console.anthropic.com", "https://api.anthropic.com"

Wildcard Port:

typescript
pattern: "http://localhost:*"
matches: "http://localhost:3000", "http://localhost:8080"

Configuration

Allowed Origins:

typescript
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:

bash
ORIGIN_VALIDATION_ENABLED=true  # Enable/disable origin validation
ALLOWED_ORIGINS=https://app1.example.com,https://app2.example.com

DNS Rebinding Protection

Attack Scenario:

  1. Attacker serves malicious webpage at attacker.com
  2. Page makes requests to localhost:3000 via JavaScript
  3. 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:

typescript
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

typescript
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:

typescript
// 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:

typescript
// 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

  1. Logging First: Capture all requests for debugging
  2. Security Early: Apply headers before processing
  3. Metrics Before Routes: Track all request performance
  4. CORS Before Body Parsing: Reject invalid origins early
  5. Validation Before Auth: Cheap checks before expensive ones
  6. Auth Before Business Logic: Protect all endpoints by default

Error Handling

All middleware should handle errors gracefully:

typescript
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:

typescript
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:

typescript
// 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

bash
# 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 HSTS

Implementation 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

Released under the MIT License.