NitroStack Logo
/sdk
/typescript
/auth
/oauth

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 ❌

Typescript
// 500+ lines of code
// - Token validation
// - JWKS fetching & caching
// - Audience validation
// - Issuer validation
// - Scope checking
// - Metadata endpoints
// - Error handling
// ... and more

NitroStack OAuth Implementation ✅

Typescript
// 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

Bash
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:

  1. Create Auth0 Application (get Client ID/Secret)
  2. Create Auth0 API (set Identifier = your RESOURCE_URI)
  3. Add scopes: read, write, admin
  4. Configure .env with your Auth0 settings

3. Configure OAuth Module

Typescript
// 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

Bash
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

  1. Open Studio at http://localhost:3000
  2. Auth → OAuth 2.1
  3. Discover: http://localhost:3005 (or http://localhost:3000 in HTTP mode)
  4. Enter Client ID/Secret
  5. Start OAuth Flow
  6. Login & Authorize
  7. Done! Test protected tools

🛡️ Protecting Tools

Basic Protection

Typescript
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

Typescript
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?

  1. STDIO - Fast MCP protocol for tool calls
  2. HTTP - Standards-compliant OAuth metadata
  3. Best of Both - Performance + Compatibility
  4. 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

Typescript
// 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

JSON
{
  "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

Typescript
@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):

Typescript
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:

Typescript
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:

Typescript
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.

Typescript
// 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!

Typescript
// 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:

  1. Auth → OAuth 2.1
  2. Discover server
  3. Enter credentials
  4. Start OAuth flow
  5. Login & authorize
  6. Test tools

Programmatic Testing

Typescript
// 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

Bash
# 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:

Typescript
// 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

Typescript
// 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

Typescript
// 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

Typescript
// 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:

Typescript
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