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:
if (!config.authRequired) {
return next(); // Skip authentication entirely
}Set AUTH_REQUIRED=false in environment to disable authentication (useful for development/testing).
Public Path Check:
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
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 " prefix3. JWT Verification
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 matchOIDC_ISSUERif configured - Audience claim (
aud): Must matchOIDC_AUDIENCEif configured - Expiration (
exp): Token must not be expired - Not Before (
nbf): Token must be valid at current time (if present)
4. User Context Extraction
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):
OIDC_JWKS_URL=https://auth.example.com/application/o/my-app/jwks/2. Automatic Discovery (Priority 2):
OIDC_ISSUER=https://auth.example.com/application/o/my-app/When using automatic discovery:
- Fetch OpenID Configuration:
GET {OIDC_ISSUER}/.well-known/openid-configuration - Extract
jwks_urifrom response - Cache the discovered URL in memory
Caching Mechanism
Cache Configuration:
jwks: {
cacheTtlMs: 3600000, // 1 hour
refreshBeforeExpiryMs: 300000, // 5 minutes
}Cache Structure:
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
setTimeoutto 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:
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
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:
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
previousif still within grace period - New keys become
current - Two separate
RemoteJWKSetinstances maintained for current and previous keys
Fallback Strategy:
- Try verifying JWT with current keys
- If verification fails and previous keys exist within grace period → try previous keys
- 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
| Path | Purpose |
|---|---|
/health | Health check endpoint |
/.well-known/oauth-protected-resource | OAuth resource metadata (RFC 9728) |
/.well-known/oauth-authorization-server | OAuth server metadata (RFC 8414) |
/oauth/token | Token exchange endpoint |
/oauth/authorize | Authorization endpoint |
/oauth/register | Dynamic client registration |
Conditional Public Paths
Metrics Endpoint:
/metrics- Prometheus metrics endpoint (only public ifMETRICS_ENABLED=true)
Documentation Path Patterns
The following path patterns bypass authentication to serve VitePress documentation:
| Pattern | Purpose |
|---|---|
/ | 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.html | VitePress 404 error page |
/hashmap.json | VitePress 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:
{
"jsonrpc": "2.0",
"error": {
"code": -32001,
"message": "Unauthorized",
"data": {
"reason": "invalid_token",
"details": "JWT signature verification failed"
}
},
"id": null
}Error Reason Codes
| Reason | Description |
|---|---|
missing_token | Authorization header not provided |
invalid_format | Header doesn't follow "Bearer <token>" format |
invalid_token | Signature verification failed |
expired_token | Token's exp claim is in the past |
invalid_issuer | Token issuer doesn't match OIDC_ISSUER |
invalid_audience | Token audience doesn't match OIDC_AUDIENCE |
missing_claim | Required sub claim missing from token |
WWW-Authenticate Header
All 401 responses include a standards-compliant WWW-Authenticate header:
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 attemptsauth_token_validation_duration_seconds- Histogram tracking JWT validation latency
Authentication Logging:
token_validated- Successful JWT validation with user contexttoken_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
# 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 authenticationCode Configuration
// 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 withlogAuthEvent() - 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
Related Documentation
- OAuth 2.1 Implementation - Token exchange and authorization flows
- Session Management - MCP session tracking
- Configuration System - Environment-based configuration