OAuth 2.1 Authentication
OAuth 2.1 is the gold standard for modern API authentication. NitroStack makes it incredibly easy to implement production-grade OAuth - no complex setup, no security pitfalls.
๐ฏ Why OAuth 2.1?
โ Industry Standard - Used by Google, Microsoft, Auth0, etc. โ User-Facing Apps - Perfect for applications with end users โ Fine-Grained Permissions - Scope-based access control โ Temporary Access - Short-lived tokens with refresh โ Federated Identity - Users login with existing accounts โ MCP Compliant - Follows MCP & OpenAI Apps SDK specs
๐ How Easy Is It?
Traditional OAuth Implementation โ
// 500+ lines of code
// - Token validation
// - JWKS fetching & caching
// - Audience validation
// - Issuer validation
// - Scope checking
// - Metadata endpoints
// - Error handling
// ... and more
NitroStack OAuth Implementation โ
// 10 lines of code!
import { OAuthModule, UseGuards, OAuthGuard } from 'nitrostack';
OAuthModule.forRoot({
resourceUri: 'http://localhost:3002',
authorizationServers: ['https://auth.example.com'],
scopesSupported: ['read', 'write', 'admin'],
});
@Tool({ name: 'protected' })
@UseGuards(OAuthGuard)
async protected() { /* ... */ }
That's it! NitroStack handles everything else automatically.
๐ฆ Built-in OAuth Module
NitroStack's OAuthModule is a complete, production-ready OAuth 2.1 implementation.
What It Does Automatically
โ Token Validation - JWT signature, expiry, audience, issuer โ JWKS Management - Fetches & caches public keys โ Metadata Endpoints - RFC 9728 Protected Resource Metadata โ Audience Binding - RFC 8707 Resource Indicators โ Token Introspection - RFC 7662 for opaque tokens โ Scope Validation - Fine-grained permissions โ Error Handling - Clear, actionable error messages โ Dual Transport - STDIO for MCP + HTTP for OAuth metadata
๐ Quick Start
1. Install Template
npx nitrostack init my-oauth-server
# Select: typescript-oauth
cd my-oauth-server
npm install
2. Setup Auth0 (5 Minutes)
Follow the complete guide in templates/typescript-oauth/OAUTH_SETUP.md
Quick Summary:
- Create Auth0 Application (get Client ID/Secret)
- Create Auth0 API (set Identifier = your RESOURCE_URI)
- Add scopes:
read,write,admin - Configure
.envwith your Auth0 settings
3. Configure OAuth Module
// src/app.module.ts
import { Module, OAuthModule } from 'nitrostack';
@Module({
name: 'app',
imports: [
OAuthModule.forRoot({
// Your MCP server's public URL
resourceUri: process.env.RESOURCE_URI!,
// Your OAuth provider(s)
authorizationServers: [process.env.AUTH_SERVER_URL!],
// Supported scopes
scopesSupported: ['read', 'write', 'admin'],
// Optional: Token validation
audience: process.env.TOKEN_AUDIENCE,
issuer: process.env.TOKEN_ISSUER,
}),
],
})
export class AppModule {}
4. Start Server
npm run dev
You'll see:
๐ HTTP MCP Server listening on http://0.0.0.0:3002/mcp
๐ OAuth 2.1 enabled
๐ Server started successfully (DUAL: STDIO + HTTP)
๐ก MCP Protocol: STDIO (for Studio/Claude)
๐ OAuth Metadata: HTTP (port 3002)
5. Test in Studio
- Open Studio at
http://localhost:3000 - Auth โ OAuth 2.1
- Discover:
http://localhost:3002 - Enter Client ID/Secret
- Start OAuth Flow
- Login & Authorize
- โ Done! Test protected tools
๐ก๏ธ Protecting Tools
Basic Protection
import { Tool, UseGuards, OAuthGuard } from 'nitrostack';
export class DemoTools {
// Public tool - no authentication
@Tool({
name: 'get_server_info',
description: 'Get public server information',
})
async getServerInfo() {
return {
name: 'My MCP Server',
version: '1.0.0',
};
}
// Protected tool - requires valid OAuth token
@Tool({
name: 'get_user_profile',
description: 'Get authenticated user profile',
})
@UseGuards(OAuthGuard)
async getUserProfile(@ExecutionContext() context: ExecutionContext) {
return {
user: context.auth.subject, // 'auth0|abc123'
scopes: context.auth.scopes, // ['read', 'write']
claims: context.auth.claims, // Full token payload
};
}
}
Scope-Based Access
import { Tool, UseGuards, OAuthGuard, createScopeGuard } from 'nitrostack';
export class ResourceTools {
// Requires 'read' scope
@Tool({ name: 'list_resources' })
@UseGuards(OAuthGuard, createScopeGuard('read'))
async listResources() {
return { resources: [...] };
}
// Requires 'write' scope
@Tool({ name: 'create_resource' })
@UseGuards(OAuthGuard, createScopeGuard('write'))
async createResource() {
// Only users with 'write' scope can call this
}
// Requires BOTH 'read' AND 'admin' scopes
@Tool({ name: 'admin_stats' })
@UseGuards(OAuthGuard, createScopeGuard('read', 'admin'))
async adminStats() {
// Only admins with read access
}
}
๐๏ธ Architecture: Dual Transport
NitroStack runs two transports simultaneously for OAuth servers:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Your OAuth 2.1 MCP Server โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ ๐ก STDIO Transport โ
โ โโ MCP Protocol Communication โ
โ โโ Tool Execution โ
โ โโ Connected to Studio/Claude โ
โ โโ Fast, efficient, standard โ
โ โ
โ ๐ HTTP Server (Port 3002) โ
โ โโ OAuth Metadata Endpoints โ
โ โโ /.well-known/oauth-protected- โ
โ โ resource (RFC 9728) โ
โ โโ Token Validation โ
โ โโ Discovery & Registration โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Why Dual Transport?
- STDIO - Fast MCP protocol for tool calls
- HTTP - Standards-compliant OAuth metadata
- Best of Both - Performance + Compatibility
- Automatic - You don't configure anything!
When you enable OAuthModule, NitroStack automatically:
- โ Keeps STDIO for MCP protocol
- โ Starts HTTP server for OAuth metadata
- โ Exposes discovery endpoints
- โ Handles all the complexity
๐ Token Validation
NitroStack validates every aspect of OAuth tokens automatically.
What Gets Validated
// Token must be:
โ
Valid JWT format
โ
Not expired (exp claim)
โ
Correct audience (aud claim)
โ
Correct issuer (iss claim)
โ
Valid signature (verified against JWKS)
โ
Not used before valid time (nbf claim)
โ
Has required scopes
Example Token
{
"iss": "https://auth.example.com/",
"sub": "auth0|abc123",
"aud": "http://localhost:3002",
"exp": 1234567890,
"iat": 1234564290,
"scope": "read write admin",
"email": "user@example.com"
}
Access Token Claims
@Tool({ name: 'check_token' })
@UseGuards(OAuthGuard)
async checkToken(@ExecutionContext() context: ExecutionContext) {
const token = context.auth;
return {
// Standard claims
subject: token.subject, // User ID
scopes: token.scopes, // ['read', 'write']
expiresAt: token.expiresAt, // Expiration timestamp
// Full token payload
claims: token.claims, // All claims
email: token.claims.email, // Custom claims
};
}
๐ OAuth Flow Explained
Complete Flow Diagram
โโโโโโโโโโ โโโโโโโโโโโโโโ
โ Studio โ โ Your MCP โ
โ โ โ Server โ
โโโโโฌโโโโโ โโโโโโโฌโโโโโโโ
โ โ
โ 1. Discover OAuth Config โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโบ โ
โ GET /.well-known/oauth-protected-resource
โ โ
โ 2. OAuth Metadata Response โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ { resource, authorization_servers, ... }
โ โ
โโโโโดโโโโโโโ
โ โ
โ User โ
โ Enters โ 3. User Enters Client ID/Secret
โ Creds โ
โ โ
โโโโโฌโโโโโโโ
โ
โ 4. Start OAuth Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ โโโโโโโผโโโโโโโ
โ โ Auth0 โ
โ 5. Redirect โ Login โ
โ โโโโโโโโโโโโโโโโโโค โ
โ โโโโโโโฌโโโโโโโ
โ โ
โ 6. User Logs In โ
โโโโโโโโโโโโโโโโโโโโโโโโโโบโ
โ โ
โ 7. Authorization Code โ
โ โโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ 8. Exchange Code โ
โโโโโโโโโโโโโโโโโโโโโโโโโโบโ
โ โ
โ 9. JWT Access Token โ
โ โโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ 10. Store Token โ
โ โ
โ 11. Call Tool (+ Token) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโบโ
โ โ โ
โ โ 12. Validate โ
โ โ Token โ
โ โ โ
โ 13. Tool Response โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ โ
๐ง Advanced Configuration
Token Introspection (Opaque Tokens)
If your OAuth provider uses opaque tokens (not JWTs):
OAuthModule.forRoot({
resourceUri: process.env.RESOURCE_URI!,
authorizationServers: [process.env.AUTH_SERVER_URL!],
scopesSupported: ['read', 'write'],
// Token introspection (RFC 7662)
tokenIntrospectionEndpoint: process.env.INTROSPECTION_ENDPOINT,
tokenIntrospectionClientId: process.env.INTROSPECTION_CLIENT_ID,
tokenIntrospectionClientSecret: process.env.INTROSPECTION_CLIENT_SECRET,
})
Custom Token Validation
Add your own validation logic:
OAuthModule.forRoot({
resourceUri: process.env.RESOURCE_URI!,
authorizationServers: [process.env.AUTH_SERVER_URL!],
scopesSupported: ['read', 'write'],
// Custom validation
customValidation: async (tokenPayload) => {
// Check if user is active in your database
const user = await db.users.findOne({ id: tokenPayload.sub });
if (!user || !user.active) {
return false;
}
// Check subscription status
if (!user.subscription || user.subscription.expired) {
return false;
}
return true;
},
})
Multiple Authorization Servers
Support federated authentication:
OAuthModule.forRoot({
resourceUri: process.env.RESOURCE_URI!,
// Accept tokens from multiple providers
authorizationServers: [
'https://auth0.example.com',
'https://okta.example.com',
'https://azuread.example.com',
],
scopesSupported: ['read', 'write'],
})
๐ Security Features
1. Token Audience Binding (RFC 8707)
Critical for security! Tokens are validated to ensure they were issued specifically for your MCP server.
// Token MUST have your RESOURCE_URI in the audience claim
{
"aud": "http://localhost:3002", // โ Must match RESOURCE_URI
"sub": "user123",
"scope": "read write"
}
Why This Matters:
- โ Without audience binding: Tokens from ANY service work
- โ With audience binding: Only tokens for YOUR service work
2. No Token Passthrough
Never forward OAuth tokens to other services!
// โ WRONG - Security vulnerability!
async function callUpstream(clientToken: string) {
await upstreamAPI.call({
headers: { Authorization: clientToken }
});
}
// โ
CORRECT - Get separate token
async function callUpstream() {
const upstreamToken = await getTokenForUpstream();
await upstreamAPI.call({
headers: { Authorization: upstreamToken }
});
}
3. Short-Lived Tokens
Configure your OAuth provider to issue short-lived tokens:
Access Token: 1 hour
Refresh Token: 30 days
NitroStack Studio automatically handles token refresh.
๐งช Testing
In Studio
Complete flow:
- Auth โ OAuth 2.1
- Discover server
- Enter credentials
- Start OAuth flow
- Login & authorize
- Test tools
Programmatic Testing
// test/oauth.test.ts
import { createTestClient } from 'nitrostack/testing';
describe('OAuth Authentication', () => {
it('should protect tools with OAuth', async () => {
const client = await createTestClient(AppModule);
// Without token - should fail
await expect(
client.callTool('protected_tool', {})
).rejects.toThrow('OAuth token required');
// With token - should succeed
const result = await client.callTool('protected_tool', {}, {
token: 'eyJhbGciOiJSUzI1NiIs...' // Valid JWT
});
expect(result.success).toBe(true);
});
it('should validate scopes', async () => {
const client = await createTestClient(AppModule);
// Token without required scope
await expect(
client.callTool('admin_tool', {}, {
token: tokenWithoutAdminScope
})
).rejects.toThrow('Insufficient scope');
});
});
๐ Production Deployment
1. Use Production OAuth Provider
# Production environment variables
RESOURCE_URI=https://mcp.yourapp.com
AUTH_SERVER_URL=https://auth.yourapp.com
TOKEN_AUDIENCE=https://mcp.yourapp.com
TOKEN_ISSUER=https://auth.yourapp.com/
2. Configure HTTPS
OAuth requires HTTPS in production:
// Use reverse proxy (nginx, Cloudflare, etc.)
// Or configure HTTPS directly:
OAuthModule.forRoot({
resourceUri: 'https://mcp.yourapp.com', // HTTPS!
// ...
})
3. Monitor Token Usage
// Add logging/metrics
OAuthModule.forRoot({
customValidation: async (token) => {
// Log token usage
await metrics.increment('oauth.token.validated', {
user: token.sub,
scopes: token.scope,
});
return true;
},
})
๐ก Common Patterns
Role-Based Access Control
// Map OAuth scopes to roles
@Tool({ name: 'admin_panel' })
@UseGuards(OAuthGuard, createScopeGuard('admin'))
async adminPanel(@ExecutionContext() context: ExecutionContext) {
const userRoles = context.auth.claims.roles; // ['admin', 'moderator']
if (!userRoles.includes('admin')) {
throw new Error('Admin role required');
}
// ... admin operations
}
Tenant Isolation
// Multi-tenant applications
@Tool({ name: 'get_data' })
@UseGuards(OAuthGuard)
async getData(@ExecutionContext() context: ExecutionContext) {
const tenantId = context.auth.claims.tenant_id;
// Only return data for user's tenant
return await db.data.find({ tenantId });
}
๐ Standards Compliance
NitroStack implements:
โ OAuth 2.1 (draft-ietf-oauth-v2-1-13) โ RFC 9728 - Protected Resource Metadata โ RFC 8414 - Authorization Server Metadata โ RFC 7591 - Dynamic Client Registration โ RFC 8707 - Resource Indicators (Token Audience Binding) โ RFC 7636 - PKCE โ RFC 7662 - Token Introspection
Compatible with: โ MCP Specification โ OpenAI Apps SDK
๐ Supported Providers
NitroStack works with any RFC-compliant OAuth 2.1 provider:
- โ Auth0 - Easiest for testing
- โ Okta - Enterprise-ready
- โ Keycloak - Self-hosted
- โ Azure AD / Entra ID
- โ Google Identity Platform
- โ AWS Cognito
- โ Custom OAuth servers
See templates/typescript-oauth/OAUTH_SETUP.md for provider-specific setup guides.
๐ Why NitroStack Makes OAuth Easy
Traditional Approach โ
Complexity:
- 500+ lines of boilerplate code
- Manual JWKS fetching & caching
- Token validation logic
- Metadata endpoints
- Audience/issuer validation
- Scope checking
- Error handling
- Security pitfalls everywhere
Time: 2-3 days to implement correctly
NitroStack Approach โ
Simplicity:
OAuthModule.forRoot({ resourceUri, authorizationServers, scopesSupported });
@UseGuards(OAuthGuard)
Time: 5 minutes!
What You Get
โ Production-Grade - Battle-tested OAuth implementation โ Standards-Compliant - Follows all RFCs โ Secure by Default - No security pitfalls โ Zero Config - Sensible defaults, works out of the box โ Fully Tested - Comprehensive test suite โ Well Documented - Clear, actionable docs โ Active Maintenance - Regular updates
๐ Learn More
- OAuth 2.1 Template
- Complete Setup Guide
- Multi-Auth Patterns
- Guards Reference
- ExecutionContext Reference
Previous: โ API Key Authentication