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

mermaid
flowchart TD
    A[Incoming Request] --> B{AUTH_REQUIRED?}
    B -->|false| Z[Skip Auth]
    B -->|true| C{Public Path?}
    C -->|yes| Z
    C -->|no| D[Extract Authorization Header]
    D --> E{Format: Bearer token?}
    E -->|no| F[Return 401: invalid_format]
    E -->|yes| G[Call jwtVerify with token]
    G --> H[JWKS Service: Get Key]
    H --> I{Key Found?}
    I -->|no| J[Refresh JWKS & Retry]
    J --> K{Key Found?}
    K -->|no| L[Return 401: invalid_token]
    K -->|yes| M[Verify Signature]
    I -->|yes| M
    M --> N{Valid Signature?}
    N -->|no| L
    N -->|yes| O[Validate Claims]
    O --> P{Claims Valid?}
    P -->|no| Q[Return 401: invalid_claim]
    P -->|yes| R[Extract User Context]
    R --> S[Attach to req.user]
    S --> T[Call next]

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.expiresAt) {
    await refreshKeys();
  }

  // Attempt key lookup
  try {
    return await remoteJWKSet(header);
  } catch (error) {
    // If lookup fails, refresh and retry once
    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.

Public Paths

The following endpoints bypass authentication:

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

These paths are essential for OAuth discovery and token acquisition flows.

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.

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
  • JWKS Service: src/services/jwks.ts
  • OIDC Config: src/config/oidc.ts
  • Tests: src/middleware/auth.test.ts, src/services/jwks.test.ts

Released under the MIT License.