Use single tsconfig.json for both main server and auth-server to avoid nested directory issues during build. Update e2e test scripts to run directly instead of using concurrently.
166 lines
No EOL
5.6 KiB
TypeScript
166 lines
No EOL
5.6 KiB
TypeScript
import express from 'express';
|
|
import cors from 'cors';
|
|
import rateLimit from 'express-rate-limit';
|
|
import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js';
|
|
import { EverythingAuthProvider } from '../src/auth/provider.js';
|
|
import { handleFakeAuthorize, handleFakeAuthorizeRedirect } from '../src/handlers/fakeauth.js';
|
|
import { redisClient } from '../src/redis.js';
|
|
import { logger } from '../src/utils/logger.js';
|
|
import { AUTH_SERVER_PORT, AUTH_SERVER_URL } from '../src/config.js';
|
|
|
|
const app = express();
|
|
|
|
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('');
|
|
console.log('This is for demonstration purposes only.');
|
|
console.log('In production, you would use a real OAuth provider');
|
|
console.log('like Auth0, Okta, Google, GitHub, etc.');
|
|
console.log('=====================================');
|
|
|
|
// CORS for Inspector and MCP server
|
|
app.use(cors({
|
|
origin: true,
|
|
credentials: true
|
|
}));
|
|
|
|
app.use(express.json());
|
|
app.use(logger.middleware());
|
|
|
|
// Health check endpoint
|
|
app.get('/health', (req, res) => {
|
|
res.json({
|
|
status: 'healthy',
|
|
mode: 'authorization-server',
|
|
endpoints: {
|
|
metadata: `${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`,
|
|
authorize: `${AUTH_SERVER_URL}/authorize`,
|
|
token: `${AUTH_SERVER_URL}/token`,
|
|
register: `${AUTH_SERVER_URL}/register`,
|
|
introspect: `${AUTH_SERVER_URL}/introspect`
|
|
}
|
|
});
|
|
});
|
|
|
|
// Create auth provider instance for reuse
|
|
const authProvider = new EverythingAuthProvider();
|
|
|
|
// OAuth endpoints via SDK's mcpAuthRouter
|
|
app.use(mcpAuthRouter({
|
|
provider: authProvider,
|
|
issuerUrl: new URL(AUTH_SERVER_URL),
|
|
tokenOptions: {
|
|
rateLimit: { windowMs: 5000, limit: 100 }
|
|
},
|
|
clientRegistrationOptions: {
|
|
rateLimit: { windowMs: 60000, limit: 10 }
|
|
}
|
|
}));
|
|
|
|
// Rate limiting for custom endpoints
|
|
const introspectRateLimit = rateLimit({
|
|
windowMs: 5 * 60 * 1000, // 5 minutes
|
|
limit: 100, // 100 introspections per 5 minutes
|
|
message: { error: 'too_many_requests', error_description: 'Token introspection rate limit exceeded' }
|
|
});
|
|
|
|
const fakeAuthRateLimit = rateLimit({
|
|
windowMs: 60 * 1000, // 1 minute
|
|
limit: 20, // 20 auth attempts per minute
|
|
message: { error: 'too_many_requests', error_description: 'Authentication rate limit exceeded' }
|
|
});
|
|
|
|
const staticFileRateLimit = rateLimit({
|
|
windowMs: 10 * 60 * 1000, // 10 minutes
|
|
limit: 25, // 25 requests per 10 minutes for static files
|
|
message: { error: 'too_many_requests', error_description: 'Static file rate limit exceeded' }
|
|
});
|
|
|
|
// Token introspection endpoint (RFC 7662)
|
|
app.post('/introspect', introspectRateLimit, express.urlencoded({ extended: false }), async (req, res) => {
|
|
try {
|
|
const { token } = req.body;
|
|
|
|
if (!token) {
|
|
return res.status(400).json({ error: 'invalid_request', error_description: 'Missing token parameter' });
|
|
}
|
|
|
|
// Verify the token using the auth provider
|
|
const authInfo = await authProvider.verifyAccessToken(token);
|
|
|
|
// Return RFC 7662 compliant response
|
|
res.json({
|
|
active: true,
|
|
client_id: authInfo.clientId,
|
|
scope: authInfo.scopes.join(' '),
|
|
exp: authInfo.expiresAt,
|
|
sub: authInfo.extra?.userId || 'unknown',
|
|
userId: authInfo.extra?.userId, // Custom field for our implementation
|
|
username: authInfo.extra?.username,
|
|
iss: AUTH_SERVER_URL,
|
|
aud: authInfo.clientId,
|
|
token_type: 'Bearer'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.debug('Token introspection failed', { error: (error as Error).message });
|
|
|
|
// Return inactive token response (don't leak error details)
|
|
res.json({
|
|
active: false
|
|
});
|
|
}
|
|
});
|
|
|
|
// Fake upstream auth endpoints (for user authentication simulation)
|
|
app.get('/fakeupstreamauth/authorize', fakeAuthRateLimit, cors(), handleFakeAuthorize);
|
|
app.get('/fakeupstreamauth/callback', fakeAuthRateLimit, cors(), handleFakeAuthorizeRedirect);
|
|
|
|
// Static assets (for auth page styling)
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
app.get('/mcp-logo.png', staticFileRateLimit, (req, res) => {
|
|
// Serve from the main server's static directory
|
|
const logoPath = path.join(__dirname, '../src/static/mcp.png');
|
|
res.sendFile(logoPath);
|
|
});
|
|
|
|
// Connect to Redis (shared with MCP server in dev)
|
|
try {
|
|
await redisClient.connect();
|
|
logger.info('Connected to Redis', { url: redisClient.options?.url });
|
|
} catch (error) {
|
|
logger.error('Could not connect to Redis', error as Error);
|
|
process.exit(1);
|
|
}
|
|
|
|
app.listen(AUTH_SERVER_PORT, () => {
|
|
logger.info('Authorization server started', {
|
|
port: AUTH_SERVER_PORT,
|
|
url: AUTH_SERVER_URL,
|
|
endpoints: {
|
|
metadata: `${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`,
|
|
authorize: `${AUTH_SERVER_URL}/authorize`,
|
|
token: `${AUTH_SERVER_URL}/token`,
|
|
register: `${AUTH_SERVER_URL}/register`,
|
|
introspect: `${AUTH_SERVER_URL}/introspect`
|
|
}
|
|
});
|
|
|
|
console.log('');
|
|
console.log('🚀 Auth server ready! Test with:');
|
|
console.log(` curl ${AUTH_SERVER_URL}/health`);
|
|
console.log(` curl ${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`);
|
|
console.log('');
|
|
console.log('💡 To test separate mode:');
|
|
console.log(' 1. Keep this server running');
|
|
console.log(' 2. In another terminal: AUTH_MODE=separate npm run dev');
|
|
console.log(' 3. Connect Inspector to http://localhost:3232/mcp');
|
|
}); |