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 ✅
// Production OAuth 2.1 in your app module
import { McpApp, Module, OAuthModule, ToolDecorator as Tool, UseGuards, OAuthGuard, ExecutionContext } from '@nitrostack/core';
@McpApp({ module: AppModule, server: { name: 'my-oauth-server', version: '1.0.0' } })
@Module({
name: 'app',
imports: [
OAuthModule.forRoot({
required: process.env.OAUTH_REQUIRED === 'true',
resourceUri: process.env.RESOURCE_URI || 'https://mcplocal',
authorizationServers: [process.env.AUTH_SERVER_URL!],
jwksUri: process.env.JWKS_URI,
scopesSupported: ['read', 'write', 'admin'],
}),
],
})
export class AppModule {}
export class SecureTools {
@Tool({ name: 'protected' })
@UseGuards(OAuthGuard)
async protectedTool(input: any, context: ExecutionContext) {
return { user: context.auth?.subject };
}
}
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/cli init my-oauth-server
# Select: typescript-oauth
cd my-oauth-server
npm install
2. Setup Auth0 (5 Minutes)
Follow the complete guide in OAuth 2.1 Template Guide and the template's included 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 { McpApp, Module, ConfigModule, OAuthModule } from "@nitrostack/core";
@McpApp({
module: AppModule,
server: {
name: "my-oauth-server",
version: "1.0.0",
},
})
@Module({
name: "app",
imports: [
ConfigModule.forRoot(),
OAuthModule.forRoot({
// Enforce auth (defaults to false for dev friendliness)
required: process.env.OAUTH_REQUIRED === "true",
// Your MCP server's public URL for audience binding (RFC 8707)
resourceUri: process.env.RESOURCE_URI || "https://mcplocal",
// Your OAuth provider(s)
authorizationServers: [process.env.AUTH_SERVER_URL!],
// Supported scopes
scopesSupported: ["read", "write", "admin"],
// Cryptographic JWKS signature verification (recommended)
jwksUri: process.env.JWKS_URI,
// Token Introspection (RFC 7662) for opaque tokens (alternative to JWKS)
tokenIntrospectionEndpoint: process.env.INTROSPECTION_ENDPOINT,
tokenIntrospectionClientId: process.env.INTROSPECTION_CLIENT_ID,
tokenIntrospectionClientSecret: process.env.INTROSPECTION_CLIENT_SECRET,
// Optional audience & issuer 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://localhost:3000/mcp
🔐 OAuth 2.1 enabled
Server started successfully (DUAL: STDIO + HTTP)
📡 MCP Protocol: STDIO (for Studio/Claude)
🌐 OAuth Discovery: HTTP (port 3005)
In STDIO mode, the OAuth discovery HTTP server runs automatically (default port 3005, configurable via OAUTH_DISCOVERY_PORT), serving RFC 9728 and RFC 8414 metadata so MCP clients can inspect endpoints and start authentication flows.
5. Test in Studio
- Open Studio at
http://localhost:3000 - Auth → OAuth 2.1
- Discover:
http://localhost:3005(orhttp://localhost:3000in HTTP mode) - Enter Client ID/Secret
- Start OAuth Flow
- Login & Authorize
- Done! Test protected tools
🛡️ Protecting Tools
Basic Protection
import { ToolDecorator as Tool, UseGuards, OAuthGuard, ExecutionContext } from "@nitrostack/core";
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(input: any, context: ExecutionContext) {
return {
user: context.auth?.subject, // 'auth0|abc123'
scopes: context.auth?.scopes, // ['read', 'write']
tokenPayload: context.auth?.tokenPayload, // Decoded OAuthTokenPayload
};
}
}
Scope-Based Access
import { ToolDecorator as Tool, UseGuards, OAuthGuard, createScopeGuard, ExecutionContext } from '@nitrostack/core';
export class ResourceTools {
// Requires 'read' scope
@Tool({ name: 'list_resources' })
@UseGuards(OAuthGuard, createScopeGuard(['read']))
async listResources(input: any, context: ExecutionContext) {
return { resources: [] };
}
// Requires 'write' scope
@Tool({ name: 'create_resource' })
@UseGuards(OAuthGuard, createScopeGuard(['write']))
async createResource(input: any, context: ExecutionContext) {
// 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(input: any, context: ExecutionContext) {
// 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 3005 in STDIO / Port 3000 in HTTP)│
│ ├─ 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(input: any, context: ExecutionContext) {
const auth = context.auth;
const payload = auth?.tokenPayload;
return {
// Standard claims from AuthContext
subject: auth?.subject, // User ID (sub claim)
scopes: auth?.scopes, // ['read', 'write']
clientId: auth?.clientId, // Client identifier
exp: payload?.exp, // Expiration timestamp (Unix)
// Full decoded token payload
tokenPayload: payload, // OAuthTokenPayload
email: (payload as any)?.email, // Custom or profile claim
};
}
🌐 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 { createMockContext } from '@nitrostack/core/testing';
import { OAuthGuard, createScopeGuard, OAuthModule } from '@nitrostack/core';
describe('OAuth Authentication', () => {
let guard: OAuthGuard;
beforeEach(() => {
// Configure OAuthModule with enforcement enabled
OAuthModule.forRoot({
required: true,
resourceUri: 'https://mcplocal',
authorizationServers: ['https://auth.example.com'],
allowInsecureTokenDecode: true, // Allow decoding test tokens without remote JWKS
});
guard = new OAuthGuard();
});
it('should reject requests without authorization header when auth is enforced', async () => {
const ctx = createMockContext();
await expect(guard.canActivate(ctx)).rejects.toThrow('OAuth token required');
});
it('should allow requests with valid OAuth bearer token and required scopes', async () => {
const ctx = createMockContext({
metadata: {
authorization: 'Bearer valid.jwt.token',
},
});
const ScopeGuardClass = createScopeGuard(['flights:book']);
const scopeGuard = new ScopeGuardClass();
ctx.auth = {
authenticated: true,
subject: 'user-123',
scopes: ['flights:read', 'flights:book'],
};
expect(await scopeGuard.canActivate(ctx)).toBe(true);
});
it('should reject requests with insufficient scopes by throwing an error', async () => {
const ctx = createMockContext();
const AdminScopeGuardClass = createScopeGuard(['admin:all']);
const scopeGuard = new AdminScopeGuardClass();
ctx.auth = {
authenticated: true,
subject: 'user-123',
scopes: ['flights:read'],
};
await expect(scopeGuard.canActivate(ctx)).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/
JWKS_URI=https://auth.yourapp.com/.well-known/jwks.json
OAUTH_REQUIRED=true
2. Configure HTTPS
OAuth requires HTTPS in production:
// Use reverse proxy (nginx, Cloudflare, etc.)
// Or configure HTTPS directly:
OAuthModule.forRoot({
required: true,
resourceUri: "https://mcp.yourapp.com", // HTTPS!
authorizationServers: [process.env.AUTH_SERVER_URL!],
jwksUri: process.env.JWKS_URI,
});
3. Monitor Token Usage
// Add custom validation / audit hooks
OAuthModule.forRoot({
resourceUri: "https://mcp.yourapp.com",
authorizationServers: [process.env.AUTH_SERVER_URL!],
customValidation: async (token: any) => {
// Audit token validation event
console.log(`Validated token for subject: ${token.sub}`);
return true;
},
});
Common Patterns
Role-Based Access Control
// Map OAuth scopes and token claims to roles
@Tool({ name: 'admin_panel' })
@UseGuards(OAuthGuard, createScopeGuard(['admin']))
async adminPanel(input: any, context: ExecutionContext) {
const userRoles = (context.auth?.tokenPayload as any)?.roles; // ['admin', 'moderator']
if (!userRoles || !userRoles.includes('admin')) {
throw new Error('Admin role required');
}
// ... admin operations
}
Tenant Isolation
// Multi-tenant applications
@Tool({ name: 'get_data' })
@UseGuards(OAuthGuard)
async getData(input: any, context: ExecutionContext) {
const tenantId = (context.auth?.tokenPayload as any)?.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 (
/.well-known/oauth-protected-resource) - RFC 8414 - Authorization Server Metadata (
/.well-known/oauth-authorization-server) - Client ID Metadata Documents (CIMD) - Modern client identifier specification (MCP 2026-07-28 replacement for DCR)
- RFC 7591 - Dynamic Client Registration (optional legacy opt-in via
enableClientRegistration: true) - RFC 8707 - Resource Indicators (Token Audience Binding)
- RFC 7636 - PKCE
- RFC 7662 - Token Introspection
- RFC 9068 - JWT Profile for OAuth 2.0 Access Tokens
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 OAuth 2.1 Template Guide and the template's 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
Previous: ← API Key Authentication