Skip to content

Authentication Flow

The Seed MCP Server implements a robust JWT-based authentication system using the jose library and JWKS (JSON Web Key Set) for signature verification.

Overview

All endpoints except public paths require a valid JWT bearer token in the Authorization header. The authentication middleware validates tokens using JWKS-based signature verification and extracts user context from claims.

JWT Validation Flow

Authentication Middleware

The middleware is implemented in src/middleware/auth.ts and performs the following steps:

1. Pre-Flight Checks

Global Auth Toggle:

typescript
if (!config.authRequired) {
  return next(); // Skip authentication entirely
}

Set AUTH_REQUIRED=false in environment to disable authentication (useful for development/testing).

Public Path Check:

typescript
const publicPaths = [
  "/health",
  "/.well-known/oauth-protected-resource",
  "/.well-known/oauth-authorization-server",
  "/oauth/token",
  "/oauth/authorize",
  "/oauth/register",
];

if (publicPaths.some(path => req.path === path)) {
  return next(); // Skip authentication
}

2. Token Extraction

typescript
const authHeader = req.headers.authorization;

if (!authHeader) {
  return sendAuthError(res, "missing_token", "No Authorization header provided");
}

if (!authHeader.startsWith("Bearer ")) {
  return sendAuthError(res, "invalid_format", "Authorization header must use Bearer scheme");
}

const token = authHeader.substring(7); // Remove "Bearer " prefix

3. JWT Verification

typescript
const { payload } = await jwtVerify(
  token,
  jwksService.getKey,
  {
    issuer: config.oidc.issuer || undefined,
    audience: config.oidc.audience || undefined,
  }
);

Validation performed by jwtVerify:

  • Signature verification: Using public key from JWKS
  • Issuer claim (iss): Must match OIDC_ISSUER if configured
  • Audience claim (aud): Must match OIDC_AUDIENCE if configured
  • Expiration (exp): Token must not be expired
  • Not Before (nbf): Token must be valid at current time (if present)

4. User Context Extraction

typescript
interface UserContext {
  sub: string;        // Subject (user ID) - REQUIRED
  email?: string;     // User email - OPTIONAL
  name?: string;      // Display name - OPTIONAL
  groups?: string[];  // Group memberships - OPTIONAL
  token: string;      // Original JWT token
}

const user: UserContext = {
  sub: payload.sub as string,
  token,
};

if (payload.email) user.email = payload.email as string;
if (payload.name) user.name = payload.name as string;
if (payload.groups) user.groups = payload.groups as string[];

req.user = user;

The sub (subject) claim is required. All other claims are optional.

JWKS Service

The JWKS service (src/services/jwks.ts) manages JSON Web Key Sets with automatic discovery, caching, and refresh.

Discovery Process

The service supports two methods for obtaining the JWKS URL:

1. Explicit Configuration (Priority 1):

bash
OIDC_JWKS_URL=https://auth.example.com/application/o/my-app/jwks/

2. Automatic Discovery (Priority 2):

bash
OIDC_ISSUER=https://auth.example.com/application/o/my-app/

When using automatic discovery:

  1. Fetch OpenID Configuration: GET {OIDC_ISSUER}/.well-known/openid-configuration
  2. Extract jwks_uri from response
  3. Cache the discovered URL in memory

Caching Mechanism

Cache Configuration:

typescript
jwks: {
  cacheTtlMs: 3600000,           // 1 hour
  refreshBeforeExpiryMs: 300000, // 5 minutes
}

Cache Structure:

typescript
interface JWKSCache {
  keys: JSONWebKeySet["keys"];
  fetchedAt: Date;
  expiresAt: Date;
}

Auto-Refresh Strategy

The JWKS service implements proactive cache refresh to prevent key rotation issues:

1. Background Refresh:

  • Scheduled 55 minutes after cache population (5 minutes before expiry)
  • Uses setTimeout to schedule refresh
  • Errors during background refresh are logged but don't crash the service

2. On-Demand Refresh:

  • Triggered when cache is expired during getKey() call
  • If key lookup fails, attempts another refresh (fallback mechanism)

Refresh Flow:

typescript
async function refreshKeys(): Promise<void> {
  // 1. Discover JWKS URL if needed
  const jwksUrl = await discoverJwksUrl();

  // 2. Fetch keys from JWKS endpoint
  const response = await fetch(jwksUrl);
  const jwks = await response.json();

  // 3. Update cache
  cache = {
    keys: jwks.keys,
    fetchedAt: new Date(),
    expiresAt: new Date(Date.now() + config.oidc.jwks.cacheTtlMs),
  };

  // 4. Recreate remoteJWKSet with fresh keys
  remoteJWKSet = createRemoteJWKSet(new URL(jwksUrl));

  // 5. Schedule next background refresh
  scheduleRefresh();
}

Key Lookup Process

typescript
async function getKey(header: JWTHeaderParameters): Promise<JoseCryptoKey> {
  // Check if cache is expired
  if (!remoteJWKSet || !cache || new Date() >= cache.current.expiresAt) {
    await refreshKeys();
  }

  // Try current keys first
  try {
    return await remoteJWKSet(header);
  } catch {
    // If current keys don't work, try previous keys (if within grace period)
    if (
      previousRemoteJWKSet &&
      cache?.previous &&
      new Date() < cache.previous.gracePeriodExpiresAt
    ) {
      try {
        logger.info("Attempting JWT verification with previous JWKS", {
          kid: header.kid,
          alg: header.alg,
        });
        return await previousRemoteJWKSet(header);
      } catch {
        // Previous keys also failed, fall through to refresh
      }
    }

    // Refresh and try again with new keys
    await refreshKeys();
    return await remoteJWKSet(header);
  }
}

The remoteJWKSet uses the JWT header's kid (key ID) to find the matching public key in the JWKS.

Key Rotation Handling

Seed handles IdP key rotation gracefully by maintaining multiple key versions with a configurable grace period:

Dual-Cache Architecture:

typescript
interface JWKSCacheEntry {
  keys: JSONWebKeySet["keys"];
  fetchedAt: Date;
  expiresAt: Date;
  gracePeriodExpiresAt: Date;
}

interface JWKSCache {
  current: JWKSCacheEntry;   // Currently active keys
  previous: JWKSCacheEntry | null;  // Previous keys (during grace period)
}

Key Rotation Detection: When refreshing keys, Seed compares the new key IDs with the current cache:

  • If any current key IDs are missing from the new set → key rotation detected
  • Current keys are moved to previous if still within grace period
  • New keys become current
  • Two separate RemoteJWKSet instances maintained for current and previous keys

Fallback Strategy:

  1. Try verifying JWT with current keys
  2. If verification fails and previous keys exist within grace period → try previous keys
  3. If both fail → refresh cache and retry with new keys

Configuration:

  • OIDC_JWKS_GRACE_PERIOD_MS - Duration to maintain previous keys (default: 600000ms / 10 minutes)
  • Grace period starts when key rotation is detected
  • Previous keys are automatically cleaned up after grace period expires

Benefits:

  • Zero downtime during IdP key rotation
  • JWTs signed with old keys remain valid during grace period
  • Automatic detection and logging of key rotation events
  • Configurable grace period for different IdP rotation practices

See JWKS Key Rotation for implementation details.

Public Paths

The following endpoints bypass authentication:

Standard Public Paths

PathPurpose
/healthHealth check endpoint
/.well-known/oauth-protected-resourceOAuth resource metadata (RFC 9728)
/.well-known/oauth-authorization-serverOAuth server metadata (RFC 8414)
/oauth/tokenToken exchange endpoint
/oauth/authorizeAuthorization endpoint
/oauth/registerDynamic client registration

Conditional Public Paths

Metrics Endpoint:

  • /metrics - Prometheus metrics endpoint (only public if METRICS_ENABLED=true)

Documentation Path Patterns

The following path patterns bypass authentication to serve VitePress documentation:

PatternPurpose
/Documentation home page
/*.{html,css,js}Root-level static assets
/assets/**Assets directory (images, fonts, etc.)
/mcp-server/**MCP server documentation
/tools/**Tools documentation
/prompts/**Prompts documentation
/404.htmlVitePress 404 error page
/hashmap.jsonVitePress route hash map

These patterns enable serving documentation without authentication while keeping MCP endpoints protected.

Error Responses

Authentication failures return JSON-RPC 2.0 compliant error responses:

json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32001,
    "message": "Unauthorized",
    "data": {
      "reason": "invalid_token",
      "details": "JWT signature verification failed"
    }
  },
  "id": null
}

Error Reason Codes

ReasonDescription
missing_tokenAuthorization header not provided
invalid_formatHeader doesn't follow "Bearer <token>" format
invalid_tokenSignature verification failed
expired_tokenToken's exp claim is in the past
invalid_issuerToken issuer doesn't match OIDC_ISSUER
invalid_audienceToken audience doesn't match OIDC_AUDIENCE
missing_claimRequired sub claim missing from token

WWW-Authenticate Header

All 401 responses include a standards-compliant WWW-Authenticate header:

http
WWW-Authenticate: Bearer resource_metadata="https://your-server.com/.well-known/oauth-protected-resource"

This header points clients to the OAuth Protected Resource metadata endpoint for discovery.

Observability & Monitoring

The authentication system includes comprehensive observability through Prometheus metrics and structured logging.

Authentication Metrics:

  • auth_attempts_total{result} - Counter tracking successful/failed authentication attempts
  • auth_token_validation_duration_seconds - Histogram tracking JWT validation latency

Authentication Logging:

  • token_validated - Successful JWT validation with user context
  • token_rejected - Failed validation with specific reason codes

See Also: Observability for comprehensive documentation including all metrics, structured logging patterns, and monitoring best practices.

Security Considerations

Signature Verification

  • All JWTs are verified using public keys from JWKS
  • Supports RSA and ECDSA signature algorithms
  • Key rotation handled automatically via cache refresh

Claim Validation

  • Issuer validation: Ensures token came from configured IdP
  • Audience validation: Ensures token is intended for this server
  • Expiration validation: Prevents use of expired tokens
  • Subject requirement: User ID (sub) must be present

Cache Security

  • JWKS cache prevents unnecessary network calls
  • Background refresh prevents stale keys
  • Fallback refresh handles key rotation edge cases

Configuration Reference

Environment Variables

bash
# Required (if AUTH_REQUIRED=true)
OIDC_ISSUER=https://auth.example.com/application/o/my-app/
OIDC_AUDIENCE=my-client-id

# Optional
OIDC_JWKS_URL=https://auth.example.com/application/o/my-app/jwks/
AUTH_REQUIRED=true  # Set to false to disable authentication

Code Configuration

typescript
// src/config/oidc.ts
export const oidcConfig = {
  issuer: process.env.OIDC_ISSUER ?? "",
  audience: process.env.OIDC_AUDIENCE ?? "",
  jwksUrl: process.env.OIDC_JWKS_URL ?? "",
  jwks: {
    cacheTtlMs: 3600000,           // 1 hour
    refreshBeforeExpiryMs: 300000, // 5 minutes
  },
};

Implementation Files

  • Middleware: src/middleware/auth.ts - JWT validation middleware
  • JWKS Service: src/services/jwks.ts - JWKS fetching and caching
  • Logger Service: src/services/logger.ts - Winston logger with logAuthEvent()
  • Metrics Service: src/services/metrics.ts - Prometheus metrics definitions
  • OIDC Config: src/config/oidc.ts - OIDC provider configuration
  • Tests: src/middleware/auth.test.ts, src/services/jwks.test.ts

Released under the MIT License.