Fix security vulnerabilities, package-lock.json, misc other changes
- Add rate limiting to prevent DoS attacks on vulnerable routes - Update package-lock.json to use public npm registry - Document intentionally permissive CORS for public test server - Remove health endpoints to match original codebase (no health checks) - Fix auth server splash page to list all OAuth endpoints - Update test scripts to check server availability via splash page
This commit is contained in:
parent
bc3619c73c
commit
38c30a4e70
6 changed files with 818 additions and 791 deletions
1529
package-lock.json
generated
1529
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -44,8 +44,8 @@ AUTH_MODE=auth_server PORT=3001 BASE_URI=$AUTH_SERVER node dist/index.js &
|
||||||
AUTH_PID=$!
|
AUTH_PID=$!
|
||||||
sleep 5
|
sleep 5
|
||||||
|
|
||||||
# Check auth server
|
# Check auth server by accessing splash page
|
||||||
if ! curl -s -f "$AUTH_SERVER/health" > /dev/null; then
|
if ! curl -s -f "$AUTH_SERVER/" > /dev/null 2>&1; then
|
||||||
echo "❌ Auth server failed to start at $AUTH_SERVER"
|
echo "❌ Auth server failed to start at $AUTH_SERVER"
|
||||||
kill $AUTH_PID 2>/dev/null || true
|
kill $AUTH_PID 2>/dev/null || true
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -58,8 +58,8 @@ AUTH_MODE=external AUTH_SERVER_URL=$AUTH_SERVER PORT=8080 BASE_URI=$MCP_SERVER n
|
||||||
MCP_PID=$!
|
MCP_PID=$!
|
||||||
sleep 5
|
sleep 5
|
||||||
|
|
||||||
# Check MCP server
|
# Check MCP server by accessing splash page
|
||||||
if ! curl -s -f "$MCP_SERVER/health" > /dev/null; then
|
if ! curl -s -f "$MCP_SERVER/" > /dev/null 2>&1; then
|
||||||
echo "❌ MCP server failed to start at $MCP_SERVER"
|
echo "❌ MCP server failed to start at $MCP_SERVER"
|
||||||
kill $AUTH_PID 2>/dev/null || true
|
kill $AUTH_PID 2>/dev/null || true
|
||||||
kill $MCP_PID 2>/dev/null || true
|
kill $MCP_PID 2>/dev/null || true
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,8 @@ AUTH_MODE=internal PORT=8080 BASE_URI=$SERVER_URL node dist/index.js &
|
||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
sleep 5
|
sleep 5
|
||||||
|
|
||||||
# Check server health
|
# Check server is running by accessing splash page
|
||||||
if ! curl -s -f "$SERVER_URL/health" > /dev/null; then
|
if ! curl -s -f "$SERVER_URL/" > /dev/null 2>&1; then
|
||||||
echo "❌ Server failed to start at $SERVER_URL"
|
echo "❌ Server failed to start at $SERVER_URL"
|
||||||
kill $SERVER_PID 2>/dev/null || true
|
kill $SERVER_PID 2>/dev/null || true
|
||||||
exit 1
|
exit 1
|
||||||
|
|
|
||||||
19
src/index.ts
19
src/index.ts
|
|
@ -15,6 +15,7 @@ import cors from 'cors';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
import rateLimit from 'express-rate-limit';
|
||||||
import { config } from './config.js';
|
import { config } from './config.js';
|
||||||
import { AuthModule } from './modules/auth/index.js';
|
import { AuthModule } from './modules/auth/index.js';
|
||||||
import { MCPModule } from './modules/mcp/index.js';
|
import { MCPModule } from './modules/mcp/index.js';
|
||||||
|
|
@ -38,6 +39,8 @@ async function main() {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// Basic middleware
|
// Basic middleware
|
||||||
|
// Intentionally permissive CORS for public MCP reference server
|
||||||
|
// This allows any MCP client to test against this reference implementation
|
||||||
app.use(cors({ origin: true, credentials: true }));
|
app.use(cors({ origin: true, credentials: true }));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: false }));
|
app.use(express.urlencoded({ extended: false }));
|
||||||
|
|
@ -125,6 +128,7 @@ async function main() {
|
||||||
console.log(` Authorize: GET ${config.baseUri}/authorize`);
|
console.log(` Authorize: GET ${config.baseUri}/authorize`);
|
||||||
console.log(` Get Token: POST ${config.baseUri}/token`);
|
console.log(` Get Token: POST ${config.baseUri}/token`);
|
||||||
console.log(` Introspect: POST ${config.baseUri}/introspect`);
|
console.log(` Introspect: POST ${config.baseUri}/introspect`);
|
||||||
|
console.log(` Revoke: POST ${config.baseUri}/revoke`);
|
||||||
|
|
||||||
} else if (config.auth.mode === 'external') {
|
} else if (config.auth.mode === 'external') {
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
@ -161,12 +165,20 @@ async function main() {
|
||||||
console.log('MCP Endpoints:');
|
console.log('MCP Endpoints:');
|
||||||
console.log(` Streamable HTTP: ${config.baseUri}/mcp`);
|
console.log(` Streamable HTTP: ${config.baseUri}/mcp`);
|
||||||
console.log(` SSE (legacy): ${config.baseUri}/sse`);
|
console.log(` SSE (legacy): ${config.baseUri}/sse`);
|
||||||
console.log(` Health Check: ${config.baseUri}/health`);
|
|
||||||
console.log(` OAuth Metadata: ${config.baseUri}/.well-known/oauth-authorization-server`);
|
console.log(` OAuth Metadata: ${config.baseUri}/.well-known/oauth-authorization-server`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rate limiter for splash page (moderate limit)
|
||||||
|
const splashPageLimiter = rateLimit({
|
||||||
|
windowMs: 60 * 1000, // 1 minute
|
||||||
|
max: 50, // 50 requests per minute
|
||||||
|
message: 'Too many requests to splash page',
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
});
|
||||||
|
|
||||||
// Splash page (customize based on mode)
|
// Splash page (customize based on mode)
|
||||||
app.get('/', (req, res) => {
|
app.get('/', splashPageLimiter, (req, res) => {
|
||||||
if (config.auth.mode === 'auth_server') {
|
if (config.auth.mode === 'auth_server') {
|
||||||
// Simple splash page for standalone auth server
|
// Simple splash page for standalone auth server
|
||||||
res.send(`
|
res.send(`
|
||||||
|
|
@ -199,13 +211,14 @@ async function main() {
|
||||||
<div class="endpoint">GET ${config.baseUri}/authorize - Authorization endpoint</div>
|
<div class="endpoint">GET ${config.baseUri}/authorize - Authorization endpoint</div>
|
||||||
<div class="endpoint">POST ${config.baseUri}/token - Token endpoint</div>
|
<div class="endpoint">POST ${config.baseUri}/token - Token endpoint</div>
|
||||||
<div class="endpoint">POST ${config.baseUri}/introspect - Token introspection</div>
|
<div class="endpoint">POST ${config.baseUri}/introspect - Token introspection</div>
|
||||||
|
<div class="endpoint">POST ${config.baseUri}/revoke - Token revocation</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`);
|
||||||
} else {
|
} else {
|
||||||
const srcStaticDir = path.join(__dirname, 'static');
|
const srcStaticDir = path.join(__dirname, 'static');
|
||||||
const splashPath = path.join(srcStaticDir, 'index.html');
|
const splashPath = path.join(srcStaticDir, 'index.html');
|
||||||
let html = fs.readFileSync(splashPath, 'utf8');
|
const html = fs.readFileSync(splashPath, 'utf8');
|
||||||
res.send(html);
|
res.send(html);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { Router } from 'express';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
import rateLimit from 'express-rate-limit';
|
||||||
import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js';
|
import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js';
|
||||||
import { FeatureReferenceAuthProvider } from './auth/provider.js';
|
import { FeatureReferenceAuthProvider } from './auth/provider.js';
|
||||||
import { handleMockUpstreamAuthorize, handleMockUpstreamCallback } from './handlers/mock-upstream-idp.js';
|
import { handleMockUpstreamAuthorize, handleMockUpstreamCallback } from './handlers/mock-upstream-idp.js';
|
||||||
|
|
@ -73,6 +74,23 @@ export class AuthModule {
|
||||||
private setupRouter(): Router {
|
private setupRouter(): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
// Rate limiters for different route types
|
||||||
|
const authLimiter = rateLimit({
|
||||||
|
windowMs: 60 * 1000, // 1 minute
|
||||||
|
max: 20, // 20 requests per minute for auth endpoints
|
||||||
|
message: 'Too many authentication attempts',
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const staticAssetLimiter = rateLimit({
|
||||||
|
windowMs: 60 * 1000, // 1 minute
|
||||||
|
max: 100, // 100 requests per minute for static assets
|
||||||
|
message: 'Too many requests for static assets',
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
});
|
||||||
|
|
||||||
// OAuth endpoints via SDK's mcpAuthRouter
|
// OAuth endpoints via SDK's mcpAuthRouter
|
||||||
router.use(mcpAuthRouter({
|
router.use(mcpAuthRouter({
|
||||||
provider: this.provider,
|
provider: this.provider,
|
||||||
|
|
@ -108,24 +126,15 @@ export class AuthModule {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mock upstream IDP endpoints (for demo purposes)
|
// Mock upstream IDP endpoints (for demo purposes)
|
||||||
router.get('/mock-upstream-idp/authorize', handleMockUpstreamAuthorize);
|
router.get('/mock-upstream-idp/authorize', authLimiter, handleMockUpstreamAuthorize);
|
||||||
router.get('/mock-upstream-idp/callback', handleMockUpstreamCallback);
|
router.get('/mock-upstream-idp/callback', authLimiter, handleMockUpstreamCallback);
|
||||||
|
|
||||||
// Static assets for auth pages
|
// Static assets for auth pages
|
||||||
router.get('/mcp-logo.png', (req, res) => {
|
router.get('/mcp-logo.png', staticAssetLimiter, (req, res) => {
|
||||||
const logoPath = path.join(__dirname, 'static', 'mcp.png');
|
const logoPath = path.join(__dirname, 'static', 'mcp.png');
|
||||||
res.sendFile(logoPath);
|
res.sendFile(logoPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Health check
|
|
||||||
router.get('/auth/health', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
status: 'healthy',
|
|
||||||
module: 'auth',
|
|
||||||
mode: 'internal'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
import rateLimit from 'express-rate-limit';
|
||||||
import { BearerAuthMiddlewareOptions, requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
|
import { BearerAuthMiddlewareOptions, requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
|
||||||
import { getOAuthProtectedResourceMetadataUrl } from '@modelcontextprotocol/sdk/server/auth/router.js';
|
import { getOAuthProtectedResourceMetadataUrl } from '@modelcontextprotocol/sdk/server/auth/router.js';
|
||||||
import { ITokenValidator } from '../../interfaces/auth-validator.js';
|
import { ITokenValidator } from '../../interfaces/auth-validator.js';
|
||||||
|
|
@ -47,7 +48,18 @@ export class MCPModule {
|
||||||
private setupRouter(): Router {
|
private setupRouter(): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
// Rate limiter for static assets
|
||||||
|
const staticAssetLimiter = rateLimit({
|
||||||
|
windowMs: 60 * 1000, // 1 minute
|
||||||
|
max: 100, // 100 requests per minute for static assets
|
||||||
|
message: 'Too many requests for static assets',
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
});
|
||||||
|
|
||||||
// CORS configuration for MCP endpoints
|
// CORS configuration for MCP endpoints
|
||||||
|
// Intentionally permissive CORS for public MCP reference server
|
||||||
|
// This allows any MCP client to test against this reference implementation
|
||||||
const corsOptions = {
|
const corsOptions = {
|
||||||
origin: true, // Allow any origin
|
origin: true, // Allow any origin
|
||||||
methods: ['GET', 'POST', 'DELETE'],
|
methods: ['GET', 'POST', 'DELETE'],
|
||||||
|
|
@ -88,20 +100,8 @@ export class MCPModule {
|
||||||
router.get('/sse', cors(corsOptions), bearerAuth, sseHeaders, handleSSEConnection);
|
router.get('/sse', cors(corsOptions), bearerAuth, sseHeaders, handleSSEConnection);
|
||||||
router.post('/message', cors(corsOptions), bearerAuth, securityHeaders, handleMessage);
|
router.post('/message', cors(corsOptions), bearerAuth, securityHeaders, handleMessage);
|
||||||
|
|
||||||
// Health check
|
|
||||||
router.get('/health', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
status: 'healthy',
|
|
||||||
service: 'mcp',
|
|
||||||
endpoints: {
|
|
||||||
streamable: '/mcp',
|
|
||||||
sse: '/sse'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Static files for MCP
|
// Static files for MCP
|
||||||
router.get('/styles.css', (req, res) => {
|
router.get('/styles.css', staticAssetLimiter, (req, res) => {
|
||||||
const cssPath = path.join(__dirname, '../../static', 'styles.css');
|
const cssPath = path.join(__dirname, '../../static', 'styles.css');
|
||||||
res.setHeader('Content-Type', 'text/css');
|
res.setHeader('Content-Type', 'text/css');
|
||||||
res.sendFile(cssPath);
|
res.sendFile(cssPath);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue