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
This commit is contained in:
Basil Hosmer 2025-09-04 22:01:57 -04:00
commit 5c408eb43d
No known key found for this signature in database
5 changed files with 381 additions and 2 deletions

89
auth-server/README.md Normal file
View file

@ -0,0 +1,89 @@
# MCP Standalone Authorization Server
This is a demonstration OAuth 2.0 authorization server for MCP.
## Purpose
This server demonstrates how MCP servers can delegate authentication to a separate
authorization server (Mode 2 in our implementation). In production environments,
you would typically use established OAuth providers like:
- Auth0
- Okta
- Google OAuth
- GitHub OAuth
- Microsoft Azure AD
## Architecture
When running in separate mode, the architecture looks like:
1. MCP Client (e.g., Inspector) discovers auth server URL from MCP server metadata
2. Client registers and authenticates directly with this auth server
3. Auth server issues tokens
4. MCP server validates tokens by calling this auth server's introspection endpoint
## Endpoints
- `/.well-known/oauth-authorization-server` - OAuth 2.0 server metadata
- `/oauth/authorize` - Authorization endpoint
- `/oauth/token` - Token endpoint
- `/oauth/register` - Dynamic client registration
- `/oauth/introspect` - Token introspection (for MCP server validation)
- `/fakeupstreamauth/authorize` - Fake upstream auth page (demo only)
- `/fakeupstreamauth/callback` - Fake upstream callback (demo only)
- `/health` - Health check endpoint
## Development
This server shares Redis with the MCP server for development convenience.
In production, these would typically be separate.
## Running the Auth Server
### Standalone
```bash
# From the repository root
npm run dev:auth-server
```
### With MCP Server (Separate Mode)
```bash
# Start both servers together
npm run dev:with-separate-auth
```
## Testing
### Health Check
```bash
curl http://localhost:3001/health
```
### OAuth Metadata
```bash
curl http://localhost:3001/.well-known/oauth-authorization-server
```
### With MCP Inspector
1. Start this auth server: `npm run dev:auth-server`
2. Start MCP server in separate mode: `AUTH_MODE=separate npm run dev`
3. Open Inspector: `npx -y @modelcontextprotocol/inspector`
4. Connect to `http://localhost:3232`
5. Auth flow will redirect to this server (port 3001)
## Configuration
The auth server uses the same environment variables as the main server:
- `AUTH_SERVER_PORT` - Port to run on (default: 3001)
- `AUTH_SERVER_URL` - Base URL (default: http://localhost:3001)
- `REDIS_URL` - Redis connection (shared with MCP server)
## Production Considerations
In production:
- Use real OAuth providers instead of this demonstration server
- Separate Redis instances for auth and resource servers
- Enable HTTPS with proper certificates
- Implement proper rate limiting and monitoring
- Use secure client secrets and token rotation

146
auth-server/index.ts Normal file
View file

@ -0,0 +1,146 @@
import express from 'express';
import cors from 'cors';
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}/oauth/authorize`,
token: `${AUTH_SERVER_URL}/oauth/token`,
register: `${AUTH_SERVER_URL}/oauth/register`,
introspect: `${AUTH_SERVER_URL}/oauth/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 }
}
}));
// Token introspection endpoint (RFC 7662)
app.post('/oauth/introspect', 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', cors(), handleFakeAuthorize);
app.get('/fakeupstreamauth/callback', 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', (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}/oauth/authorize`,
token: `${AUTH_SERVER_URL}/oauth/token`,
register: `${AUTH_SERVER_URL}/oauth/register`,
introspect: `${AUTH_SERVER_URL}/oauth/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');
});

View file

@ -0,0 +1,9 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../dist/auth-server",
"rootDir": "../"
},
"include": ["index.ts", "../src/**/*.ts", "../shared/**/*.ts"],
"exclude": ["../dist", "../node_modules", "../**/*.test.ts"]
}

125
package-lock.json generated
View file

@ -22,6 +22,7 @@
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.0",
"concurrently": "^8.2.0",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"tsx": "^4.19.2",
@ -484,6 +485,16 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.3",
"resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@babel/runtime/-/runtime-7.28.3.tgz",
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
@ -3114,6 +3125,50 @@
"dev": true,
"license": "MIT"
},
"node_modules/concurrently": {
"version": "8.2.2",
"resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/concurrently/-/concurrently-8.2.2.tgz",
"integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2",
"date-fns": "^2.30.0",
"lodash": "^4.17.21",
"rxjs": "^7.8.1",
"shell-quote": "^1.8.1",
"spawn-command": "0.0.2",
"supports-color": "^8.1.1",
"tree-kill": "^1.2.2",
"yargs": "^17.7.2"
},
"bin": {
"conc": "dist/bin/concurrently.js",
"concurrently": "dist/bin/concurrently.js"
},
"engines": {
"node": "^14.13.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/concurrently/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -3206,6 +3261,23 @@
"node": ">= 8"
}
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@ -5319,6 +5391,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@ -6224,6 +6303,16 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -6350,6 +6439,19 @@
"node": ">=8"
}
},
"node_modules/shell-quote": {
"version": "1.8.3",
"resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/shell-quote/-/shell-quote-1.8.3.tgz",
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
@ -6467,6 +6569,12 @@
"source-map": "^0.6.0"
}
},
"node_modules/spawn-command": {
"version": "0.0.2",
"resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/spawn-command/-/spawn-command-0.0.2.tgz",
"integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==",
"dev": true
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@ -6651,6 +6759,16 @@
"node": ">=0.6"
}
},
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"dev": true,
"license": "MIT",
"bin": {
"tree-kill": "cli.js"
}
},
"node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@ -6743,6 +6861,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.20.5",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz",

View file

@ -6,12 +6,21 @@
"main": "dist/index.js",
"scripts": {
"start": "node dist/index.js",
"start:auth-server": "node dist/auth-server/index.js",
"dev": "tsx watch --inspect src/index.ts",
"dev:break": "tsx --inspect-brk watch src/index.ts",
"dev:integrated": "AUTH_MODE=integrated npm run dev",
"dev:separate": "AUTH_MODE=separate AUTH_SERVER_URL=http://localhost:3001 npm run dev",
"dev:auth-server": "AUTH_SERVER_PORT=3001 tsx watch --inspect auth-server/index.ts",
"dev:with-separate-auth": "concurrently -n \"AUTH,MCP\" -c \"yellow,cyan\" \"npm run dev:auth-server\" \"npm run dev:separate\"",
"build": "tsc && npm run copy-static",
"build:auth-server": "tsc -p auth-server/tsconfig.json",
"build:all": "tsc && tsc -p auth-server/tsconfig.json && npm run copy-static",
"copy-static": "mkdir -p dist/static && cp -r src/static/* dist/static/",
"lint": "eslint src/",
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
"lint": "eslint src/ auth-server/",
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
"test:integrated": "AUTH_MODE=integrated npm test",
"test:separate": "AUTH_MODE=separate npm test"
},
"devDependencies": {
"@eslint/js": "^9.15.0",
@ -20,6 +29,7 @@
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.0",
"concurrently": "^8.2.0",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"tsx": "^4.19.2",