example-remote-server/auth-server/index.ts
Basil Hosmer b32d638c87
Consolidate TypeScript build configuration
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.
2025-09-15 02:10:52 -04:00

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');
});