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:
parent
500c263176
commit
1140815c7b
2 changed files with 121 additions and 56 deletions
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue