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
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:
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.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:
| 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 |
These paths are essential for OAuth discovery and token acquisition flows.
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.
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 - JWKS Service:
src/services/jwks.ts - OIDC Config:
src/config/oidc.ts - 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