A production-ready OAuth 2.1 server implementation designed specifically for Model Context Protocol (MCP) servers. Provides seamless authentication integration with mcpresso
servers.
- ✅ 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
npm install mcpresso-oauth-server
# or
bun add mcpresso-oauth-server
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:
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:
// 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:
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)
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']
}
}
}
})
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:
# Server configuration
OAUTH_ISSUER=https://auth.yourdomain.com
OAUTH_SERVER_URL=https://auth.yourdomain.com
OAUTH_JWT_SECRET=your-super-secret-jwt-key
# CORS configuration (for separate server deployment)
CORS_ORIGIN=https://yourdomain.com,https://api.yourdomain.com
TRUST_PROXY=true
# Server port
PORT=4001
The server includes production-ready security features:
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 endpointPOST /introspect
- Token introspectionPOST /revoke
- Token revocationGET /userinfo
- User info endpointPOST /register
- Dynamic client registration
GET /.well-known/oauth-authorization-server
- OAuth metadata (RFC 8414)GET /.well-known/jwks.json
- JSON Web Key SetGET /.well-known/oauth-protected-resource
- MCP protected resource metadata
GET /health
- Health checkGET /admin/clients
- List clientsGET /admin/users
- List usersGET /admin/stats
- Server statistics
The package includes an in-memory storage implementation for development. For production, implement the MCPOAuthStorage
interface:
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
- Separate Servers:
mcpresso/examples/separate-servers-demo.ts
- Get Authorization Code:
# Open in browser or use curl
curl "http://localhost:4001/authorize?response_type=code&client_id=demo-client&redirect_uri=http://localhost:4001/callback&scope=read&resource=http://localhost:4000"
- Exchange for Token:
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"
- Use Token with MCP API:
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":{}}}'
# Install dependencies
bun install
# Build the package
bun run build
# Run tests
bun test
# Start development server
bun run dev
For production deployments:
- Use Strong JWT Secrets - Generate cryptographically secure secrets
- Configure CORS Properly - Only allow trusted origins
- Enable HTTPS - Always use HTTPS in production
- Implement Proper Storage - Use a production database, not memory storage
- Monitor & Log - Set up proper logging and monitoring
- Keep Dependencies Updated - Regularly update packages
- Rate Limiting - Configure appropriate rate limits
- Input Validation - Validate all user inputs
- Documentation: mcpresso documentation
- Issues: GitHub Issues
- Examples: Working examples
MIT - See LICENSE file for details.