mcpresso-oauth-server
Version:
Production-ready OAuth 2.1 server implementation for Model Context Protocol (MCP) with PKCE support
445 lines (357 loc) • 13.2 kB
Markdown
A production-ready OAuth 2.1 server implementation designed specifically for Model Context Protocol (MCP) servers. Provides seamless authentication integration with `mcpresso` servers.
## Features
- ✅ **OAuth 2.1 Compliant** - Full implementation of OAuth 2.1 draft specification
- ✅ **MCP Integration** - Designed specifically for Model Context Protocol
- ✅ **PKCE Support** - Proof Key for Code Exchange (required for MCP)
- ✅ **Custom User Authentication** - Pluggable login logic and UI customization
- ✅ **Production Ready** - Security headers, rate limiting, compression, CORS
- ✅ **Flexible Deployment** - Run standalone or integrated with MCP servers
- ✅ **Dynamic Client Registration** - RFC 7591 compliant
- ✅ **Multiple Grant Types** - Authorization code, refresh token, client credentials
- ✅ **Token Introspection** - RFC 7662 compliant
- ✅ **Discovery Endpoints** - RFC 8414 and RFC 9728 compliant
## Quick Start
### Installation
```bash
npm install mcpresso-oauth-server
# or
bun add mcpresso-oauth-server
```
### Basic Usage
```typescript
import {
MCPOAuthServer,
MemoryStorage
} from 'mcpresso-oauth-server'
// Initialize storage with demo data
const storage = new MemoryStorage()
await storage.createClient({
id: 'demo-client',
secret: 'demo-secret',
name: 'Demo Client',
type: 'confidential',
redirectUris: ['http://localhost:4000/callback'],
scopes: ['read', 'write'],
grantTypes: ['authorization_code']
})
// Create OAuth server with custom authentication
const oauthServer = new MCPOAuthServer({
issuer: 'http://localhost:4000',
serverUrl: 'http://localhost:4000',
jwtSecret: 'your-secret-key',
auth: {
authenticateUser: async (credentials, context) => {
// Your login logic here
const user = users.find(u => u.username === credentials.username)
return user && validatePassword(credentials.password, user.password) ? user : null
},
getCurrentUser: async (sessionData, context) => {
// Check if user is already logged in via session/cookies
return null // Force login for this example
},
renderLoginPage: async (context, error) => {
// Optional: Customize login page
return `<html>...custom login form...</html>`
}
}
}, storage)
```
Run OAuth and MCP server together:
```typescript
import { createMCPServer } from 'mcpresso'
import { MCPOAuthServer } from 'mcpresso-oauth-server'
// Create OAuth server
const oauthServer = new MCPOAuthServer({...}, storage)
// Create MCP server with integrated OAuth
const app = createMCPServer({
name: "integrated_server",
resources: [userResource],
auth: {
oauth: oauthServer, // Integrate OAuth server
userLookup: async (jwtPayload) => {
// Fetch full user profiles from your database
return await db.users.findById(jwtPayload.sub)
}
}
})
app.listen(4000) // Both OAuth and MCP on port 4000
```
**Architecture:**
```
┌─────────────────────────────────┐
│ Integrated Server │
│ (Port 4000) │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │OAuth Service│ │ MCP Service │ │
│ │• /authorize │ │• MCP API │ │
│ │• /token │ │• Resources │ │
│ │• Login UI │ │• Tools │ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────┘
```
Run OAuth and MCP on different ports:
```typescript
// OAuth Server (Port 4001)
const oauthApp = express()
oauthApp.use(cors()) // Important for cross-origin requests
registerOAuthEndpoints(oauthApp, oauthServer)
oauthApp.listen(4001)
// MCP Server (Port 4000)
const mcpApp = createMCPServer({
name: "api_server",
resources: [userResource],
auth: {
issuer: "http://localhost:4001", // OAuth server URL
serverUrl: "http://localhost:4000", // This MCP server URL
jwtSecret: "shared-secret", // Same secret as OAuth server
userLookup: async (jwtPayload) => {
return await db.users.findById(jwtPayload.sub)
}
}
})
mcpApp.listen(4000)
```
**Architecture:**
```
┌─────────────────┐ ┌──────────────────┐
│ OAuth Server │ │ MCP Server │
│ (Port 4001) │ │ (Port 4000) │
│ │ │ │
│ • /authorize │ │ • MCP API │
│ • /token │ │ • Token Validation│
│ • Login UI │ │ • Resources │
└─────────────────┘ └──────────────────┘
```
Implement your own user authentication logic:
```typescript
const oauthServer = new MCPOAuthServer({
// ... other config
auth: {
// Required: Validate user credentials
authenticateUser: async (credentials, context) => {
const { username, password } = credentials
// Example with database lookup
const user = await db.users.findByEmail(username)
if (!user) return null
// Verify password (use bcrypt in production)
const isValid = await bcrypt.compare(password, user.hashedPassword)
if (!isValid) return null
// Return user object
return {
id: user.id,
username: user.email,
email: user.email,
scopes: user.permissions // e.g., ['read', 'write', 'admin']
}
},
// Optional: Check existing sessions
getCurrentUser: async (sessionData, context) => {
if (sessionData?.userId) {
return await db.users.findById(sessionData.userId)
}
return null
},
// Optional: Custom login page
renderLoginPage: async (context, error) => {
return `
<!DOCTYPE html>
<html>
<head><title>Login - ${context.clientId}</title></head>
<body>
<h2>Login to ${context.clientId}</h2>
${error ? `<p style="color:red">${error}</p>` : ''}
<form method="POST" action="/authorize">
<!-- Hidden OAuth parameters -->
<input type="hidden" name="response_type" value="code">
<input type="hidden" name="client_id" value="${context.clientId}">
<input type="hidden" name="redirect_uri" value="${context.redirectUri}">
<input type="hidden" name="scope" value="${context.scope || ''}">
<!-- Login form -->
<div>
<label>Email:</label>
<input type="email" name="username" required>
</div>
<div>
<label>Password:</label>
<input type="password" name="password" required>
</div>
<button type="submit">Login</button>
</form>
</body>
</html>
`
}
}
}, storage)
```
```typescript
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key)
const oauthServer = new MCPOAuthServer({
auth: {
authenticateUser: async ({ username, password }) => {
const { data, error } = await supabase.auth.signInWithPassword({
email: username,
password: password
})
if (error || !data.user) return null
return {
id: data.user.id,
username: data.user.email,
email: data.user.email,
scopes: data.user.user_metadata?.scopes || ['read']
}
}
}
})
```
```typescript
import { signInWithEmailAndPassword } from 'firebase/auth'
const oauthServer = new MCPOAuthServer({
auth: {
authenticateUser: async ({ username, password }) => {
try {
const userCredential = await signInWithEmailAndPassword(auth, username, password)
const user = userCredential.user
return {
id: user.uid,
username: user.email,
email: user.email,
scopes: user.customClaims?.scopes || ['read']
}
} catch (error) {
return null
}
}
}
})
```
Configure the server using environment variables:
```bash
OAUTH_ISSUER=https://auth.yourdomain.com
OAUTH_SERVER_URL=https://auth.yourdomain.com
OAUTH_JWT_SECRET=your-super-secret-jwt-key
CORS_ORIGIN=https://yourdomain.com,https://api.yourdomain.com
TRUST_PROXY=true
PORT=4001
```
The server includes production-ready security features:
```typescript
const oauthServer = new MCPOAuthServer({
// ... OAuth config
http: {
cors: {
origin: ['https://yourdomain.com'],
credentials: true,
methods: ['GET', 'POST', 'OPTIONS']
},
enableHelmet: true,
enableRateLimit: true,
rateLimitConfig: {
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // requests per window
}
}
})
```
- `GET /authorize` - Authorization endpoint (shows login page)
- `POST /authorize` - Authorization endpoint (processes login)
- `POST /token` - Token endpoint
- `POST /introspect` - Token introspection
- `POST /revoke` - Token revocation
- `GET /userinfo` - User info endpoint
- `POST /register` - Dynamic client registration
### Discovery Endpoints
- `GET /.well-known/oauth-authorization-server` - OAuth metadata (RFC 8414)
- `GET /.well-known/jwks.json` - JSON Web Key Set
- `GET /.well-known/oauth-protected-resource` - MCP protected resource metadata
### Admin Endpoints
- `GET /health` - Health check
- `GET /admin/clients` - List clients
- `GET /admin/users` - List users
- `GET /admin/stats` - Server statistics
## Storage
The package includes an in-memory storage implementation for development. For production, implement the `MCPOAuthStorage` interface:
```typescript
import type { MCPOAuthStorage } from 'mcpresso-oauth-server'
class DatabaseStorage implements MCPOAuthStorage {
async createClient(client: OAuthClient): Promise<void> {
// Store client in your database
}
async getClient(clientId: string): Promise<OAuthClient | null> {
// Fetch client from your database
}
// ... implement all required methods
}
```
See complete working examples:
- **Integrated OAuth**: [`mcpresso/examples/oauth2-simple-demo.ts`](https://github.com/granular-software/mcpresso/tree/main/examples/oauth2-simple-demo.ts)
- **Separate Servers**: [`mcpresso/examples/separate-servers-demo.ts`](https://github.com/granular-software/mcpresso/tree/main/examples/separate-servers-demo.ts)
1. **Get Authorization Code:**
```bash
curl "http://localhost:4001/authorize?response_type=code&client_id=demo-client&redirect_uri=http://localhost:4001/callback&scope=read&resource=http://localhost:4000"
```
2. **Exchange for Token:**
```bash
curl -X POST "http://localhost:4001/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code&client_id=demo-client&client_secret=demo-secret&code=YOUR_CODE&redirect_uri=http://localhost:4001/callback&resource=http://localhost:4000"
```
3. **Use Token with MCP API:**
```bash
curl -X POST "http://localhost:4000" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"whoami_user","arguments":{}}}'
```
```bash
bun install
bun run build
bun test
bun run dev
```
For production deployments:
1. **Use Strong JWT Secrets** - Generate cryptographically secure secrets
2. **Configure CORS Properly** - Only allow trusted origins
3. **Enable HTTPS** - Always use HTTPS in production
4. **Implement Proper Storage** - Use a production database, not memory storage
5. **Monitor & Log** - Set up proper logging and monitoring
6. **Keep Dependencies Updated** - Regularly update packages
7. **Rate Limiting** - Configure appropriate rate limits
8. **Input Validation** - Validate all user inputs
- **Documentation**: [mcpresso documentation](https://github.com/granular-software/mcpresso)
- **Issues**: [GitHub Issues](https://github.com/granular-software/mcpresso-oauth-server/issues)
- **Examples**: [Working examples](https://github.com/granular-software/mcpresso/tree/main/examples)
MIT - See LICENSE file for details.