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('MCP Demonstration Authorization Server');
console.log('====================================='); console.log('=====================================');
console.log('This standalone server demonstrates OAuth 2.0'); 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('');
console.log('This is for demonstration purposes only.'); console.log('This is for demonstration purposes only.');
console.log('In production, you would use a real OAuth provider'); 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 cors from "cors";
import rateLimit from "express-rate-limit"; import rateLimit from "express-rate-limit";
import express from "express"; import express from "express";
import fs from "fs";
import path from "path"; import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { ExternalAuthVerifier } from "./auth/external-verifier.js"; 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' } message: { error: 'too_many_requests', error_description: 'Static file rate limit exceeded' }
}); });
// Separate mode: MCP server uses external auth server // MCP server using external auth server
logger.info('Starting MCP server in SEPARATE mode', { logger.info('Starting MCP server', {
baseUri: BASE_URI, baseUri: BASE_URI,
port: PORT, port: PORT,
authServerUrl: AUTH_SERVER_URL authServerUrl: AUTH_SERVER_URL
}); });
// Fetch metadata from external auth server with retry logic // Auth server state - will be populated asynchronously
let authMetadata; let authMetadata: any;
const maxRetries = 5; let authServerAvailable = false;
const retryDelay = 3000; // 3 seconds
for (let attempt = 1; attempt <= maxRetries; attempt++) { // OAuth metadata endpoint - responds based on current auth server status
try { app.get('/.well-known/oauth-authorization-server', (req, res) => {
logger.info(`Attempting to connect to auth server (attempt ${attempt}/${maxRetries})`, { if (authServerAvailable && authMetadata) {
authServerUrl: AUTH_SERVER_URL // Return the auth server metadata
}); res.json(authMetadata);
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 { } else {
logger.error('Failed to fetch auth server metadata after all retries', error as Error); // Auth server unavailable
logger.error('Make sure the auth server is running at', undefined, { authServerUrl: AUTH_SERVER_URL }); res.status(503).json({
process.exit(1); error: 'service_unavailable',
error_description: 'Authentication server is currently unavailable. Please try again later.'
});
} }
});
// 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;
} }
}
// BACKWARDS COMPATIBILITY: We serve OAuth metadata from the MCP server even in separate mode // Auth is available, use the real bearer auth middleware
// This is technically redundant since the auth server handles all OAuth operations, const externalVerifier = new ExternalAuthVerifier(AUTH_SERVER_URL);
// but some clients may expect to find .well-known/oauth-authorization-server on the const bearerAuthOptions: BearerAuthMiddlewareOptions = {
// 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 with external verifier
const externalVerifier = new ExternalAuthVerifier(AUTH_SERVER_URL);
const bearerAuthOptions: BearerAuthMiddlewareOptions = {
verifier: externalVerifier, verifier: externalVerifier,
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(new URL(BASE_URI)), resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(new URL(BASE_URI)),
};
const realBearerAuth = requireBearerAuth(bearerAuthOptions);
realBearerAuth(req, res, next);
}; };
const bearerAuth = requireBearerAuth(bearerAuthOptions);
// MCP routes (legacy SSE transport) // MCP routes (legacy SSE transport)
app.get("/sse", cors(corsOptions), bearerAuth, sseHeaders, handleSSEConnection); app.get("/sse", cors(corsOptions), bearerAuth, sseHeaders, handleSSEConnection);
app.post("/message", cors(corsOptions), bearerAuth, sensitiveDataHeaders, handleMessage); 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.post("/mcp", cors(corsOptions), bearerAuth, handleStreamableHTTP);
app.delete("/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 // Static assets
app.get("/mcp-logo.png", staticFileRateLimit, (req, res) => { app.get("/mcp-logo.png", staticFileRateLimit, (req, res) => {
const logoPath = path.join(__dirname, "static", "mcp.png"); const logoPath = path.join(__dirname, "static", "mcp.png");
@ -231,7 +233,23 @@ app.get("/styles.css", staticFileRateLimit, (req, res) => {
// Splash page // Splash page
app.get("/", staticFileRateLimit, (req, res) => { app.get("/", staticFileRateLimit, (req, res) => {
const splashPath = path.join(__dirname, "static", "index.html"); const splashPath = path.join(__dirname, "static", "index.html");
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); res.sendFile(splashPath);
}
}); });
// Note: Fake upstream auth routes are not needed in separate mode // Note: Fake upstream auth routes are not needed in separate mode
@ -250,4 +268,51 @@ app.listen(PORT, () => {
url: `http://localhost:${PORT}`, url: `http://localhost:${PORT}`,
environment: process.env.NODE_ENV || 'development' 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
}
}
}
}