Skip to content

Commit 5c408eb

Browse files
committed
Phase 4: Create standalone authorization server
- Created auth-server/index.ts with full OAuth endpoint support - Reuses EverythingAuthProvider from main server - Added custom /oauth/introspect endpoint for token validation - Includes fake upstream auth routes for user authentication - Added TypeScript configuration for auth server - Added comprehensive npm scripts for all modes: - dev:integrated, dev:separate, dev:auth-server - dev:with-separate-auth (runs both servers) - build:auth-server, build:all - Added concurrently dependency for running multiple servers - Added detailed README for auth server - Successfully serves OAuth metadata and health endpoints
1 parent 4771c6e commit 5c408eb

File tree

5 files changed

+381
-2
lines changed

5 files changed

+381
-2
lines changed

auth-server/README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# MCP Standalone Authorization Server
2+
3+
This is a demonstration OAuth 2.0 authorization server for MCP.
4+
5+
## Purpose
6+
7+
This server demonstrates how MCP servers can delegate authentication to a separate
8+
authorization server (Mode 2 in our implementation). In production environments,
9+
you would typically use established OAuth providers like:
10+
11+
- Auth0
12+
- Okta
13+
- Google OAuth
14+
- GitHub OAuth
15+
- Microsoft Azure AD
16+
17+
## Architecture
18+
19+
When running in separate mode, the architecture looks like:
20+
21+
1. MCP Client (e.g., Inspector) discovers auth server URL from MCP server metadata
22+
2. Client registers and authenticates directly with this auth server
23+
3. Auth server issues tokens
24+
4. MCP server validates tokens by calling this auth server's introspection endpoint
25+
26+
## Endpoints
27+
28+
- `/.well-known/oauth-authorization-server` - OAuth 2.0 server metadata
29+
- `/oauth/authorize` - Authorization endpoint
30+
- `/oauth/token` - Token endpoint
31+
- `/oauth/register` - Dynamic client registration
32+
- `/oauth/introspect` - Token introspection (for MCP server validation)
33+
- `/fakeupstreamauth/authorize` - Fake upstream auth page (demo only)
34+
- `/fakeupstreamauth/callback` - Fake upstream callback (demo only)
35+
- `/health` - Health check endpoint
36+
37+
## Development
38+
39+
This server shares Redis with the MCP server for development convenience.
40+
In production, these would typically be separate.
41+
42+
## Running the Auth Server
43+
44+
### Standalone
45+
```bash
46+
# From the repository root
47+
npm run dev:auth-server
48+
```
49+
50+
### With MCP Server (Separate Mode)
51+
```bash
52+
# Start both servers together
53+
npm run dev:with-separate-auth
54+
```
55+
56+
## Testing
57+
58+
### Health Check
59+
```bash
60+
curl http://localhost:3001/health
61+
```
62+
63+
### OAuth Metadata
64+
```bash
65+
curl http://localhost:3001/.well-known/oauth-authorization-server
66+
```
67+
68+
### With MCP Inspector
69+
1. Start this auth server: `npm run dev:auth-server`
70+
2. Start MCP server in separate mode: `AUTH_MODE=separate npm run dev`
71+
3. Open Inspector: `npx -y @modelcontextprotocol/inspector`
72+
4. Connect to `http://localhost:3232`
73+
5. Auth flow will redirect to this server (port 3001)
74+
75+
## Configuration
76+
77+
The auth server uses the same environment variables as the main server:
78+
- `AUTH_SERVER_PORT` - Port to run on (default: 3001)
79+
- `AUTH_SERVER_URL` - Base URL (default: http://localhost:3001)
80+
- `REDIS_URL` - Redis connection (shared with MCP server)
81+
82+
## Production Considerations
83+
84+
In production:
85+
- Use real OAuth providers instead of this demonstration server
86+
- Separate Redis instances for auth and resource servers
87+
- Enable HTTPS with proper certificates
88+
- Implement proper rate limiting and monitoring
89+
- Use secure client secrets and token rotation

auth-server/index.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import express from 'express';
2+
import cors from 'cors';
3+
import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js';
4+
import { EverythingAuthProvider } from '../src/auth/provider.js';
5+
import { handleFakeAuthorize, handleFakeAuthorizeRedirect } from '../src/handlers/fakeauth.js';
6+
import { redisClient } from '../src/redis.js';
7+
import { logger } from '../src/utils/logger.js';
8+
import { AUTH_SERVER_PORT, AUTH_SERVER_URL } from '../src/config.js';
9+
10+
const app = express();
11+
12+
console.log('=====================================');
13+
console.log('MCP Demonstration Authorization Server');
14+
console.log('=====================================');
15+
console.log('This standalone server demonstrates OAuth 2.0');
16+
console.log('authorization separate from the MCP resource server');
17+
console.log('');
18+
console.log('This is for demonstration purposes only.');
19+
console.log('In production, you would use a real OAuth provider');
20+
console.log('like Auth0, Okta, Google, GitHub, etc.');
21+
console.log('=====================================');
22+
23+
// CORS for Inspector and MCP server
24+
app.use(cors({
25+
origin: true,
26+
credentials: true
27+
}));
28+
29+
app.use(express.json());
30+
app.use(logger.middleware());
31+
32+
// Health check endpoint
33+
app.get('/health', (req, res) => {
34+
res.json({
35+
status: 'healthy',
36+
mode: 'authorization-server',
37+
endpoints: {
38+
metadata: `${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`,
39+
authorize: `${AUTH_SERVER_URL}/oauth/authorize`,
40+
token: `${AUTH_SERVER_URL}/oauth/token`,
41+
register: `${AUTH_SERVER_URL}/oauth/register`,
42+
introspect: `${AUTH_SERVER_URL}/oauth/introspect`
43+
}
44+
});
45+
});
46+
47+
// Create auth provider instance for reuse
48+
const authProvider = new EverythingAuthProvider();
49+
50+
// OAuth endpoints via SDK's mcpAuthRouter
51+
app.use(mcpAuthRouter({
52+
provider: authProvider,
53+
issuerUrl: new URL(AUTH_SERVER_URL),
54+
tokenOptions: {
55+
rateLimit: { windowMs: 5000, limit: 100 }
56+
},
57+
clientRegistrationOptions: {
58+
rateLimit: { windowMs: 60000, limit: 10 }
59+
}
60+
}));
61+
62+
// Token introspection endpoint (RFC 7662)
63+
app.post('/oauth/introspect', express.urlencoded({ extended: false }), async (req, res) => {
64+
try {
65+
const { token } = req.body;
66+
67+
if (!token) {
68+
return res.status(400).json({ error: 'invalid_request', error_description: 'Missing token parameter' });
69+
}
70+
71+
// Verify the token using the auth provider
72+
const authInfo = await authProvider.verifyAccessToken(token);
73+
74+
// Return RFC 7662 compliant response
75+
res.json({
76+
active: true,
77+
client_id: authInfo.clientId,
78+
scope: authInfo.scopes.join(' '),
79+
exp: authInfo.expiresAt,
80+
sub: authInfo.extra?.userId || 'unknown',
81+
userId: authInfo.extra?.userId, // Custom field for our implementation
82+
username: authInfo.extra?.username,
83+
iss: AUTH_SERVER_URL,
84+
aud: authInfo.clientId,
85+
token_type: 'Bearer'
86+
});
87+
88+
} catch (error) {
89+
logger.debug('Token introspection failed', { error: (error as Error).message });
90+
91+
// Return inactive token response (don't leak error details)
92+
res.json({
93+
active: false
94+
});
95+
}
96+
});
97+
98+
// Fake upstream auth endpoints (for user authentication simulation)
99+
app.get('/fakeupstreamauth/authorize', cors(), handleFakeAuthorize);
100+
app.get('/fakeupstreamauth/callback', cors(), handleFakeAuthorizeRedirect);
101+
102+
// Static assets (for auth page styling)
103+
import path from 'path';
104+
import { fileURLToPath } from 'url';
105+
106+
const __filename = fileURLToPath(import.meta.url);
107+
const __dirname = path.dirname(__filename);
108+
109+
app.get('/mcp-logo.png', (req, res) => {
110+
// Serve from the main server's static directory
111+
const logoPath = path.join(__dirname, '../src/static/mcp.png');
112+
res.sendFile(logoPath);
113+
});
114+
115+
// Connect to Redis (shared with MCP server in dev)
116+
try {
117+
await redisClient.connect();
118+
logger.info('Connected to Redis', { url: redisClient.options?.url });
119+
} catch (error) {
120+
logger.error('Could not connect to Redis', error as Error);
121+
process.exit(1);
122+
}
123+
124+
app.listen(AUTH_SERVER_PORT, () => {
125+
logger.info('Authorization server started', {
126+
port: AUTH_SERVER_PORT,
127+
url: AUTH_SERVER_URL,
128+
endpoints: {
129+
metadata: `${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`,
130+
authorize: `${AUTH_SERVER_URL}/oauth/authorize`,
131+
token: `${AUTH_SERVER_URL}/oauth/token`,
132+
register: `${AUTH_SERVER_URL}/oauth/register`,
133+
introspect: `${AUTH_SERVER_URL}/oauth/introspect`
134+
}
135+
});
136+
137+
console.log('');
138+
console.log('🚀 Auth server ready! Test with:');
139+
console.log(` curl ${AUTH_SERVER_URL}/health`);
140+
console.log(` curl ${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`);
141+
console.log('');
142+
console.log('💡 To test separate mode:');
143+
console.log(' 1. Keep this server running');
144+
console.log(' 2. In another terminal: AUTH_MODE=separate npm run dev');
145+
console.log(' 3. Connect Inspector to http://localhost:3232');
146+
});

auth-server/tsconfig.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "../tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "../dist/auth-server",
5+
"rootDir": "../"
6+
},
7+
"include": ["index.ts", "../src/**/*.ts", "../shared/**/*.ts"],
8+
"exclude": ["../dist", "../node_modules", "../**/*.test.ts"]
9+
}

0 commit comments

Comments
 (0)