Add graceful degradation when auth server unavailable

Server starts immediately and retries auth connection in background.
Splash page and health endpoint remain accessible in degraded mode.
This commit is contained in:
Basil Hosmer 2025-10-07 14:36:33 -04:00
commit 1140815c7b
No known key found for this signature in database
2 changed files with 121 additions and 56 deletions

View file

@ -14,7 +14,7 @@ console.log('=====================================');
console.log('MCP Demonstration Authorization Server');
console.log('=====================================');
console.log('This standalone server demonstrates OAuth 2.0');
console.log('authorization separate from the MCP resource server');
console.log('authorization for the MCP resource server');
console.log('');
console.log('This is for demonstration purposes only.');
console.log('In production, you would use a real OAuth provider');

View file

@ -17,6 +17,7 @@ import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from "@mo
import cors from "cors";
import rateLimit from "express-rate-limit";
import express from "express";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { ExternalAuthVerifier } from "./auth/external-verifier.js";
@ -141,72 +142,60 @@ const staticFileRateLimit = rateLimit({
message: { error: 'too_many_requests', error_description: 'Static file rate limit exceeded' }
});
// Separate mode: MCP server uses external auth server
logger.info('Starting MCP server in SEPARATE mode', {
// MCP server using external auth server
logger.info('Starting MCP server', {
baseUri: BASE_URI,
port: PORT,
authServerUrl: AUTH_SERVER_URL
});
// Fetch metadata from external auth server with retry logic
let authMetadata;
const maxRetries = 5;
const retryDelay = 3000; // 3 seconds
// Auth server state - will be populated asynchronously
let authMetadata: any;
let authServerAvailable = false;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
logger.info(`Attempting to connect to auth server (attempt ${attempt}/${maxRetries})`, {
authServerUrl: AUTH_SERVER_URL
// OAuth metadata endpoint - responds based on current auth server status
app.get('/.well-known/oauth-authorization-server', (req, res) => {
if (authServerAvailable && authMetadata) {
// Return the auth server metadata
res.json(authMetadata);
} else {
// Auth server unavailable
res.status(503).json({
error: 'service_unavailable',
error_description: 'Authentication server is currently unavailable. Please try again later.'
});
const authMetadataResponse = await fetch(`${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`);
if (!authMetadataResponse.ok) {
throw new Error(`Failed to fetch auth server metadata: ${authMetadataResponse.status} ${authMetadataResponse.statusText}`);
}
authMetadata = await authMetadataResponse.json();
logger.info('Successfully fetched auth server metadata', {
issuer: authMetadata.issuer,
authorizationEndpoint: authMetadata.authorization_endpoint,
tokenEndpoint: authMetadata.token_endpoint
});
break; // Success, exit retry loop
} catch (error) {
if (attempt < maxRetries) {
logger.info(`Failed to connect to auth server, retrying in ${retryDelay/1000} seconds...`, {
attempt,
maxRetries,
error: (error as Error).message
});
await new Promise(resolve => setTimeout(resolve, retryDelay));
} else {
logger.error('Failed to fetch auth server metadata after all retries', error as Error);
logger.error('Make sure the auth server is running at', undefined, { authServerUrl: AUTH_SERVER_URL });
process.exit(1);
}
}
}
});
// BACKWARDS COMPATIBILITY: We serve OAuth metadata from the MCP server even in separate mode
// This is technically redundant since the auth server handles all OAuth operations,
// but some clients may expect to find .well-known/oauth-authorization-server on the
// resource server itself. The metadata points to the external auth server endpoints.
app.use(mcpAuthMetadataRouter({
oauthMetadata: authMetadata,
resourceServerUrl: new URL(BASE_URI),
resourceName: "MCP Feature Reference Server"
}));
// Configure bearer auth middleware that checks auth availability dynamically
const bearerAuth: express.RequestHandler = (req, res, next) => {
if (!authServerAvailable) {
// Degraded mode: return 503 for protected endpoints
res.status(503).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Authentication service unavailable',
data: {
authServerUrl: AUTH_SERVER_URL,
hint: 'The authentication server is not responding. Please ensure it is running and try again.'
}
},
id: null
});
return;
}
// Configure bearer auth with external verifier
const externalVerifier = new ExternalAuthVerifier(AUTH_SERVER_URL);
const bearerAuthOptions: BearerAuthMiddlewareOptions = {
verifier: externalVerifier,
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(new URL(BASE_URI)),
// Auth is available, use the real bearer auth middleware
const externalVerifier = new ExternalAuthVerifier(AUTH_SERVER_URL);
const bearerAuthOptions: BearerAuthMiddlewareOptions = {
verifier: externalVerifier,
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(new URL(BASE_URI)),
};
const realBearerAuth = requireBearerAuth(bearerAuthOptions);
realBearerAuth(req, res, next);
};
const bearerAuth = requireBearerAuth(bearerAuthOptions);
// MCP routes (legacy SSE transport)
app.get("/sse", cors(corsOptions), bearerAuth, sseHeaders, handleSSEConnection);
app.post("/message", cors(corsOptions), bearerAuth, sensitiveDataHeaders, handleMessage);
@ -216,6 +205,19 @@ app.get("/mcp", cors(corsOptions), bearerAuth, handleStreamableHTTP);
app.post("/mcp", cors(corsOptions), bearerAuth, handleStreamableHTTP);
app.delete("/mcp", cors(corsOptions), bearerAuth, handleStreamableHTTP);
// Health check endpoint
app.get("/health", (req, res) => {
res.json({
status: authServerAvailable ? 'healthy' : 'degraded',
services: {
mcp: 'operational',
auth: authServerAvailable ? 'operational' : 'unavailable',
redis: 'operational' // Will be checked if Redis connection fails
},
authServerUrl: AUTH_SERVER_URL
});
});
// Static assets
app.get("/mcp-logo.png", staticFileRateLimit, (req, res) => {
const logoPath = path.join(__dirname, "static", "mcp.png");
@ -231,7 +233,23 @@ app.get("/styles.css", staticFileRateLimit, (req, res) => {
// Splash page
app.get("/", staticFileRateLimit, (req, res) => {
const splashPath = path.join(__dirname, "static", "index.html");
res.sendFile(splashPath);
if (!authServerAvailable) {
// Inject warning banner for degraded mode
let html = fs.readFileSync(splashPath, 'utf8');
const warningBanner = `
<div style="background: #ff6b6b; color: white; padding: 16px; text-align: center; font-weight: bold; border-bottom: 3px solid #c92a2a;">
⚠️ Authentication Service Unavailable - Server Running in Degraded Mode
<div style="font-weight: normal; margin-top: 8px; font-size: 14px;">
The authentication server at ${AUTH_SERVER_URL} is not responding.
MCP endpoints will return errors until the auth server is available.
</div>
</div>`;
html = html.replace('<body>', `<body>${warningBanner}`);
res.send(html);
} else {
res.sendFile(splashPath);
}
});
// Note: Fake upstream auth routes are not needed in separate mode
@ -250,4 +268,51 @@ app.listen(PORT, () => {
url: `http://localhost:${PORT}`,
environment: process.env.NODE_ENV || 'development'
});
// Try to connect to auth server in background (don't block server startup)
connectToAuthServer();
});
// Attempt to connect to auth server with retries
async function connectToAuthServer() {
const maxRetries = 5;
const retryDelay = 3000; // 3 seconds
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
logger.info(`Attempting to connect to auth server (attempt ${attempt}/${maxRetries})`, {
authServerUrl: AUTH_SERVER_URL
});
const authMetadataResponse = await fetch(`${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`);
if (!authMetadataResponse.ok) {
throw new Error(`Failed to fetch auth server metadata: ${authMetadataResponse.status} ${authMetadataResponse.statusText}`);
}
authMetadata = await authMetadataResponse.json();
authServerAvailable = true;
logger.info('Successfully connected to auth server', {
issuer: authMetadata.issuer,
authorizationEndpoint: authMetadata.authorization_endpoint,
tokenEndpoint: authMetadata.token_endpoint
});
break; // Success, exit retry loop
} catch (error) {
if (attempt < maxRetries) {
logger.info(`Failed to connect to auth server, retrying in ${retryDelay/1000} seconds...`, {
attempt,
maxRetries,
error: (error as Error).message
});
await new Promise(resolve => setTimeout(resolve, retryDelay));
} else {
logger.error('Failed to connect to auth server after all retries', error as Error);
logger.warning('MCP server running in degraded mode - authentication unavailable', {
authServerUrl: AUTH_SERVER_URL
});
logger.warning('Protected endpoints will return 503 until auth server is available');
// Server continues in degraded mode
}
}
}
}