/sdk
/typescript
/auth
/overview

Authentication Guide

Overview

NitroStack v3.0 supports multiple authentication methods:

  • JWT (JSON Web Tokens) - For authenticated API calls
  • API Keys - For service-to-service authentication
  • OAuth 2.1 - For third-party integrations (including OpenAI Apps SDK)

Guards

What are Guards?

Guards determine whether a request should be processed. They're used primarily for authentication and authorization.

import { Guard, ExecutionContext } from 'nitrostack';

export class JWTGuard implements Guard {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    // Check if user is authenticated
    const token = this.extractToken(context);
    
    if (!token) {
      return false;
    }
    
    try {
      const payload = await this.verifyToken(token);
      context.auth = {
        subject: payload.sub,
        token: token,
        ...payload
      };
      return true;
    } catch (error) {
      return false;
    }
  }
  
  private extractToken(context: ExecutionContext): string | null {
    // Extract from metadata or arguments
    return context.metadata?.token || null;
  }
  
  private async verifyToken(token: string): Promise<any> {
    // Verify JWT token
    // Implementation depends on your JWT library
  }
}

Using Guards

import { UseGuards } from 'nitrostack';

export class UserTools {
  @Tool({ name: 'get_profile' })
  @UseGuards(JWTGuard)  // ← Requires JWT authentication
  async getProfile(input: any, ctx: ExecutionContext) {
    const userId = ctx.auth?.subject;  // ← Available from guard
    return await this.userService.findById(userId);
  }
  
  @Tool({ name: 'delete_user' })
  @UseGuards(JWTGuard, AdminGuard)  // ← Multiple guards
  async deleteUser(input: any, ctx: ExecutionContext) {
    // Requires both JWT auth AND admin role
  }
}

JWT Authentication

Setup JWTModule

import { JWTModule, Module, McpApp } from 'nitrostack';

@McpApp({
  server: { name: 'my-server', version: '1.0.0' }
})
@Module({
  imports: [
    JWTModule.forRoot({
      secret: process.env.JWT_SECRET!,
      expiresIn: '7d'
    }),
    // Other modules...
  ]
})
export class AppModule {}

Create JWT Guard

import { Guard, ExecutionContext, Injectable } from 'nitrostack';
import * as jwt from 'jsonwebtoken';

@Injectable()
export class JWTGuard implements Guard {
  constructor(private config: ConfigService) {}
  
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const token = this.extractToken(context);
    
    if (!token) {
      context.logger.warn('No token provided');
      return false;
    }
    
    try {
      const secret = this.config.get('JWT_SECRET');
      const payload = jwt.verify(token, secret) as any;
      
      // Attach auth info to context
      context.auth = {
        subject: payload.sub,
        email: payload.email,
        role: payload.role,
        token: token
      };
      
      context.logger.info(`Authenticated user: ${payload.sub}`);
      return true;
      
    } catch (error) {
      context.logger.error('Token verification failed:', error);
      return false;
    }
  }
  
  private extractToken(context: ExecutionContext): string | null {
    // From metadata (MCP protocol)
    if (context.metadata?.authorization) {
      const auth = context.metadata.authorization;
      if (auth.startsWith('Bearer ')) {
        return auth.substring(7);
      }
    }
    
    // From tool arguments (for testing)
    if (context.metadata?.token) {
      return context.metadata.token;
    }
    
    return null;
  }
}

Login Tool

import * as jwt from 'jsonwebtoken';
import * as bcrypt from 'bcrypt';

export class AuthTools {
  constructor(
    private userService: UserService,
    private config: ConfigService
  ) {}
  
  @Tool({
    name: 'login',
    description: 'Authenticate user and get JWT token',
    inputSchema: z.object({
      email: z.string().email(),
      password: z.string()
    })
  })
  @Widget('login-success')
  async login(input: any, ctx: ExecutionContext) {
    // Find user
    const user = await this.userService.findByEmail(input.email);
    if (!user) {
      throw new Error('Invalid credentials');
    }
    
    // Verify password
    const valid = await bcrypt.compare(input.password, user.password_hash);
    if (!valid) {
      throw new Error('Invalid credentials');
    }
    
    // Generate JWT
    const secret = this.config.get('JWT_SECRET');
    const token = jwt.sign(
      {
        sub: user.id,
        email: user.email,
        role: user.role
      },
      secret,
      { expiresIn: '7d' }
    );
    
    return {
      token,
      user: {
        id: user.id,
        email: user.email,
        name: user.name
      }
    };
  }
  
  @Tool({ name: 'whoami' })
  @UseGuards(JWTGuard)
  @Widget('user-profile')
  async whoami(input: any, ctx: ExecutionContext) {
    const userId = ctx.auth?.subject;
    const user = await this.userService.findById(userId);
    return user;
  }
}

API Key Authentication

NitroStack provides built-in ApiKeyModule for simple and secure API key authentication.

Setup ApiKeyModule

import { ApiKeyModule, Module, McpApp } from 'nitrostack';

@McpApp({
  server: { name: 'my-server', version: '1.0.0' }
})
@Module({
  name: 'app',
  imports: [
    // Enable API key authentication
    ApiKeyModule.forRoot({
      // Read keys from environment variables: API_KEY_1, API_KEY_2, etc.
      keysEnvPrefix: 'API_KEY',
      
      // Header name (default: 'x-api-key')
      headerName: 'x-api-key',
      
      // Metadata field name in MCP requests (default: 'apiKey')
      metadataField: 'apiKey',
      
      // Set to true to store keys as SHA-256 hashes (recommended for production)
      hashed: false,
    }),
    // Other modules...
  ]
})
export class AppModule {}

Environment Variables

# .env
API_KEY_1=sk_live_abc123xyz
API_KEY_2=sk_test_demo456
API_KEY_3=sk_prod_789secure

The module automatically loads all API_KEY_* variables.

Create API Key Guard

import { Guard, ExecutionContext, ApiKeyModule } from 'nitrostack';

export class ApiKeyGuard implements Guard {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    // Extract API key from metadata (sent by Studio or client)
    const apiKey = context.metadata?.apiKey || context.metadata?.['x-api-key'];
    
    if (!apiKey) {
      throw new Error('API key required');
    }
    
    // Validate using ApiKeyModule
    const isValid = await ApiKeyModule.validate(apiKey as string);
    
    if (!isValid) {
      throw new Error('Invalid API key');
    }
    
    // Populate context.auth
    context.auth = {
      subject: `apikey_${(apiKey as string).substring(0, 12)}`,
      scopes: ['*'], // API keys typically have full access
    };
    
    return true;
  }
}

Using API Key Guard

import { Injectable, ToolDecorator as Tool, UseGuards } from 'nitrostack';
import { ApiKeyGuard } from './guards/apikey.guard.js';

@Injectable()
export class ProtectedTools {
  
  // PUBLIC: No authentication required
  @Tool({
    name: 'get_public_info',
    description: 'Get public information (no auth required)',
  })
  async getPublicInfo(args: any) {
    return { data: 'public' };
  }
  
  // PROTECTED: Requires API key
  @Tool({
    name: 'get_protected_data',
    description: 'Get protected data (requires API key)',
  })
  @UseGuards(ApiKeyGuard)  // 🔒 Protected!
  async getProtectedData(args: any, context?: ExecutionContext) {
    const subject = context?.auth?.subject;
    return { data: 'protected', accessedBy: subject };
  }
}

Generate API Keys

import { ApiKeyModule } from 'nitrostack';

// Generate a cryptographically secure API key
const apiKey = ApiKeyModule.generateKey('sk'); // sk_...

// Hash a key for secure storage
const hashed = ApiKeyModule.hashKey(apiKey);

// Store hashed key in environment
// API_KEY_1=<hashed_value>

Production Best Practices

// Use hashed keys in production
ApiKeyModule.forRoot({
  keysEnvPrefix: 'API_KEY',
  hashed: true,  // Keys stored as SHA-256 hashes
})

// Custom validation
ApiKeyModule.forRoot({
  keysEnvPrefix: 'API_KEY',
  customValidation: async (key) => {
    // Check against database, rate limits, expiration, etc.
    const keyRecord = await db.apiKeys.findOne({ key });
    return keyRecord && !keyRecord.expired;
  },
})

Multi-Auth Patterns

Option 1: Either JWT OR API Key

export class MultiAuthGuard implements Guard {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    // Try JWT first
    const jwt = context.metadata?._jwt || context.metadata?.authorization;
    if (jwt) {
      // Validate JWT...
      return true;
    }
    
    // Try API key
    const apiKey = context.metadata?.apiKey;
    if (apiKey) {
      return await ApiKeyModule.validate(apiKey);
    }
    
    throw new Error('Either JWT or API key required');
  }
}

// Use on tool
@Tool({ name: 'flexible_tool' })
@UseGuards(MultiAuthGuard)  // Accepts JWT OR API key
async flexibleTool() {
  // Accessible with either auth method
}

Option 2: Both JWT AND API Key Required

@Tool({ name: 'critical_operation' })
@UseGuards(JWTGuard, ApiKeyGuard)  // Both required!
async criticalOperation() {
  // Requires BOTH JWT token AND API key
}

API Key Service (Advanced)

@Injectable()
export class ApiKeyService {
  constructor(private db: DatabaseService) {}
  
  async validate(key: string): Promise<any | null> {
    const result = await this.db.queryOne(
      'SELECT * FROM api_keys WHERE key_hash = ?',
      [this.hashKey(key)]
    );
    
    return result;
  }
  
  async create(userId: string, name: string, scopes: string[]): Promise<string> {
    const key = this.generateKey();
    const keyHash = this.hashKey(key);
    
    await this.db.execute(
      'INSERT INTO api_keys (user_id, name, key_hash, scopes) VALUES (?, ?, ?, ?)',
      [userId, name, keyHash, JSON.stringify(scopes)]
    );
    
    return key;  // Return once, never shown again
  }
  
  private generateKey(): string {
    // Generate secure random key
    return `sk_${randomBytes(32).toString('hex')}`;
  }
  
  private hashKey(key: string): string {
    return createHash('sha256').update(key).digest('hex');
  }
}

OAuth 2.1

Setup OAuth Module

NitroStack supports OAuth 2.1 for integration with platforms like OpenAI Apps SDK.

import { OAuthModule } from 'nitrostack/auth';

@Module({
  imports: [
    OAuthModule.forRoot({
      clientId: process.env.OAUTH_CLIENT_ID!,
      clientSecret: process.env.OAUTH_CLIENT_SECRET!,
      redirectUri: process.env.OAUTH_REDIRECT_URI!,
      authorizationEndpoint: 'https://auth.example.com/oauth/authorize',
      tokenEndpoint: 'https://auth.example.com/oauth/token'
    })
  ]
})
export class AppModule {}

OAuth Guard

@Injectable()
export class OAuthGuard implements Guard {
  constructor(private oauthService: OAuthService) {}
  
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const accessToken = this.extractToken(context);
    
    if (!accessToken) {
      return false;
    }
    
    try {
      // Verify token with OAuth provider
      const userInfo = await this.oauthService.verifyToken(accessToken);
      
      context.auth = {
        subject: userInfo.sub,
        email: userInfo.email,
        provider: 'oauth',
        token: accessToken
      };
      
      return true;
    } catch (error) {
      context.logger.error('OAuth verification failed:', error);
      return false;
    }
  }
  
  private extractToken(context: ExecutionContext): string | null {
    if (context.metadata?.authorization) {
      const auth = context.metadata.authorization;
      if (auth.startsWith('Bearer ')) {
        return auth.substring(7);
      }
    }
    return null;
  }
}

OpenAI Apps SDK Integration

// For OpenAI Apps SDK compliance
@Tool({ name: 'openai_tool' })
@UseGuards(OAuthGuard)  // OAuth 2.1 required
async openaiTool(input: any, ctx: ExecutionContext) {
  const userId = ctx.auth?.subject;
  // Your logic here
}

Role-Based Access Control

Admin Guard

@Injectable()
export class AdminGuard implements Guard {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    // Assumes JWTGuard has already run
    const role = context.auth?.role;
    
    if (role !== 'admin') {
      context.logger.warn(`Access denied: User role is ${role}, requires admin`);
      return false;
    }
    
    return true;
  }
}

Usage

@Tool({ name: 'delete_all_users' })
@UseGuards(JWTGuard, AdminGuard)  // ← JWT + Admin role
async deleteAllUsers(input: any, ctx: ExecutionContext) {
  // Only admins can call this
}

Custom Role Guard

export function RoleGuard(...roles: string[]) {
  class DynamicRoleGuard implements Guard {
    async canActivate(context: ExecutionContext): Promise<boolean> {
      const userRole = context.auth?.role;
      return roles.includes(userRole);
    }
  }
  return DynamicRoleGuard;
}

// Usage
@Tool({ name: 'moderator_action' })
@UseGuards(JWTGuard, RoleGuard('admin', 'moderator'))
async moderatorAction(input: any, ctx: ExecutionContext) {
  // Admins OR moderators can call this
}

Scope-Based Access

Scope Guard

@Injectable()
export class ScopeGuard implements Guard {
  constructor(private requiredScopes: string[]) {}
  
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const userScopes = context.auth?.scopes || [];
    
    const hasAllScopes = this.requiredScopes.every(
      scope => userScopes.includes(scope)
    );
    
    if (!hasAllScopes) {
      context.logger.warn(
        `Missing scopes. Required: ${this.requiredScopes}, Has: ${userScopes}`
      );
      return false;
    }
    
    return true;
  }
}

// Usage
@Tool({ name: 'write_data' })
@UseGuards(ApiKeyGuard, ScopeGuard(['read', 'write']))
async writeData(input: any, ctx: ExecutionContext) {
  // Requires API key with read AND write scopes
}

Best Practices

1. Chain Guards Properly

// ✅ Good - JWT first, then role check
@UseGuards(JWTGuard, AdminGuard)

// ❌ Avoid - Role check without auth
@UseGuards(AdminGuard)  // Will fail, no auth info

2. Store Secrets Securely

// ✅ Good - Use ConfigService
constructor(private config: ConfigService) {}
const secret = this.config.get('JWT_SECRET');

// ❌ Avoid - Hardcoded secrets
const secret = 'my-secret-key';

3. Log Auth Events

// ✅ Good
context.logger.info(`User ${userId} authenticated`);
context.logger.warn('Authentication failed');

// ❌ Avoid - Silent failures
if (!token) return false;

4. Set Appropriate Token Expiry

// ✅ Good - Reasonable expiry
expiresIn: '7d'  // 1 week for user tokens
expiresIn: '1h'  // 1 hour for sensitive operations

// ❌ Avoid - Too long
expiresIn: '365d'  // 1 year is too long

5. Hash Sensitive Data

// ✅ Good - Hash passwords and API keys
const hash = await bcrypt.hash(password, 10);

// ❌ Avoid - Storing plain text
await db.execute('INSERT INTO users VALUES (?, ?)', [email, password]);

Security Checklist

  • ✅ Use HTTPS in production
  • ✅ Store secrets in environment variables
  • ✅ Hash passwords with bcrypt (cost factor ≥ 10)
  • ✅ Use secure random for tokens/keys
  • ✅ Set appropriate token expiry
  • ✅ Implement rate limiting on auth endpoints
  • ✅ Log authentication events
  • ✅ Validate input on all auth endpoints
  • ✅ Use prepared statements (prevent SQL injection)
  • ✅ Implement account lockout after failed attempts

Next Steps


Pro Tip: Use multiple authentication methods (JWT, API Keys, OAuth) to support different use cases - JWT for users, API keys for services, OAuth for third-party integrations!