NitroStack LogoNitroStack
/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
// 10 lines of code!
import { OAuthModule, UseGuards, OAuthGuard } from '@nitrostack/core';

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

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 templates/typescript-oauth/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 { Module, OAuthModule } from "@nitrostack/core";

@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

Bash
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

  1. Open Studio at http://localhost:3000
  2. Auth โ†’ OAuth 2.1
  3. Discover: http://localhost:3002
  4. Enter Client ID/Secret
  5. Start OAuth Flow
  6. Login & Authorize
  7. Done! Test protected tools

๐Ÿ›ก๏ธ Protecting Tools

Basic Protection

Typescript
import { Tool, UseGuards, OAuthGuard } 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(@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

Typescript
import { Tool, UseGuards, OAuthGuard, createScopeGuard } from '@nitrostack/core';

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?

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

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

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/

2. Configure HTTPS

OAuth requires HTTPS in production:

Typescript
// Use reverse proxy (nginx, Cloudflare, etc.)
// Or configure HTTPS directly:
OAuthModule.forRoot({
  resourceUri: "https://mcp.yourapp.com", // HTTPS!
  // ...
});

3. Monitor Token Usage

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

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

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

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