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('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');
|
||||||
|
|
|
||||||
|
|
@ -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,71 +142,59 @@ 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,
|
|
||||||
// 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 with external verifier
|
|
||||||
const externalVerifier = new ExternalAuthVerifier(AUTH_SERVER_URL);
|
const externalVerifier = new ExternalAuthVerifier(AUTH_SERVER_URL);
|
||||||
|
|
||||||
const bearerAuthOptions: BearerAuthMiddlewareOptions = {
|
const bearerAuthOptions: BearerAuthMiddlewareOptions = {
|
||||||
verifier: externalVerifier,
|
verifier: externalVerifier,
|
||||||
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(new URL(BASE_URI)),
|
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(new URL(BASE_URI)),
|
||||||
};
|
};
|
||||||
|
const realBearerAuth = requireBearerAuth(bearerAuthOptions);
|
||||||
const bearerAuth = requireBearerAuth(bearerAuthOptions);
|
realBearerAuth(req, res, next);
|
||||||
|
};
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue