npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wazobiatech/auth-middleware

v1.0.16

Published

Framework-agnostic JWT authentication library for Wazobia microservices platform

Readme

@wazobiatech/auth-middleware

A comprehensive TypeScript authentication library for Wazobia microservices platform, supporting multiple token types (User JWT, Project, Platform, Service) and frameworks (Express.js, NestJS, GraphQL) with advanced Redis caching and JWKS validation.

🚀 Features

  • 🔐 Multi-Token Authentication: Supports User JWT, Project, Platform, and Service tokens
  • 🏗️ Framework Agnostic: Works seamlessly with Express.js, NestJS, FastAPI, and GraphQL
  • 🔑 JWKS Integration: Dynamic public key fetching and caching from JWKS endpoints
  • Redis Caching: High-performance caching for tokens, JWKS, and metadata
  • 🚫 Token Revocation: Real-time revocation checking and secret version management
  • 🏢 Service Authorization: Service-to-service authentication with project-specific access control
  • 🎯 Scope-based Access: Granular permission system with scopes and permissions
  • 📝 TypeScript: Full TypeScript support with comprehensive type definitions
  • 🛡️ Security First: RS512 algorithm, signature verification, and comprehensive validation
  • 📊 Performance Optimized: Intelligent caching strategies and graceful error handling

🏗️ Architecture Overview

Token Types

1. User JWT Tokens

  • Purpose: User authentication and authorization
  • Header: Authorization: Bearer <token>
  • JWKS Endpoint: auth/projects/{tenant_id}/.well-known/jwks.json
  • Features: User context, permissions, tenant association
  • Use Cases: User profile operations, authenticated API calls

2. Project Tokens

  • Purpose: Service access to project resources
  • Header: x-project-token: <token>
  • Features: Service enablement, secret versioning, project scoping
  • Use Cases: Cross-service communication, project management
  • Validation: Checks enabled_services array and secret version

3. Platform Tokens

  • Purpose: Platform-level operations and administration
  • Header: x-project-token: <token>
  • Features: Platform-wide access, administrative operations
  • Use Cases: System administration, platform management

4. Service Tokens

  • Purpose: Service-to-service authentication
  • Header: x-project-token: <token>
  • JWKS Endpoint: auth/service/.well-known/jwks.json
  • Features: Client credentials flow, scope-based access
  • Use Cases: Background jobs, inter-service communication

Security Architecture

┌─────────────────┐    ┌──────────────┐    ┌─────────────────┐
│   Client App    │───▶│              │───▶│  Mercury Auth   │
│                 │    │  Auth        │    │   Service       │
│ User JWT Token  │    │  Middleware  │    │                 │
└─────────────────┘    │              │    │ • JWKS Endpoint │
                       │  Features:   │    │ • Token Signing │
┌─────────────────┐    │  • Caching   │    │ • Revocation    │
│  Service Call   │───▶│  • Validation│    └─────────────────┘
│                 │    │  • Scopes    │              │
│ Project Token   │    │  • Redis     │              │
└─────────────────┘    └──────────────┘              │
                              │                      │
                              ▼                      ▼
                       ┌──────────────┐    ┌─────────────────┐
                       │    Redis     │    │   Protected     │
                       │   Cache      │    │   Resource      │
                       │              │    │                 │
                       │ • JWKS Cache │    │ • User Data     │
                       │ • Token Cache│    │ • Project Data  │
                       │ • Revocation │    │ • Service APIs  │
                       └──────────────┘    └─────────────────┘

📦 Installation

npm install @wazobiatech/auth-middleware

Peer Dependencies

The library requires the following peer dependencies based on your framework:

{
  "@nestjs/common": "^10.0.0 || ^11.0.0",
  "@nestjs/graphql": "^12.0.0", 
  "@nestjs/passport": "^10.0.0 || ^11.0.0",
  "express": "^4.18.0 || ^5.0.0",
  "fastify": "^4.0.0 || ^5.0.0",
  "passport-jwt": "^4.0.0"
}

⚙️ Configuration

Environment Variables

# Required
REDIS_URL=redis://localhost:6379
MERCURY_BASE_URL=http://localhost:4000
SIGNATURE_SHARED_SECRET=your-shared-secret

# For Service Authentication
CLIENT_ID=your-service-client-id
CLIENT_SECRET=your-service-client-secret

# Optional
CACHE_EXPIRY_TIME=3600  # Token cache TTL in seconds (default: 1 hour)

Redis Connection

The library automatically manages Redis connections with:

  • Connection pooling and health checks
  • Automatic reconnection with exponential backoff
  • Graceful shutdown handling
  • Error recovery and cleanup
import RedisConnectionManager from '@wazobiatech/auth-middleware/utils/redis.connection';

// Setup graceful shutdown (optional)
RedisConnectionManager.setupGracefulShutdown();

🚀 Usage Guide

Express.js Integration

Basic Usage

import express from 'express';
import { jwtAuthMiddleware, projectAuthMiddleware } from '@wazobiatech/auth-middleware/express';

const app = express();

// User authentication middleware
app.use('/api/user', jwtAuthMiddleware());

// Project authentication middleware  
app.use('/api/project', projectAuthMiddleware('your-service-name'));

// Combined authentication
app.use('/api/secure', jwtAuthMiddleware(), projectAuthMiddleware('your-service-name'));

// Access authenticated data
app.get('/api/user/profile', jwtAuthMiddleware(), (req, res) => {
  const user = req.user;  // AuthUser object
  res.json({ user });
});

app.get('/api/project/data', projectAuthMiddleware('billing-service'), (req, res) => {
  const project = req.project;  // ProjectContext object
  const platform = req.platform; // PlatformContext object (if platform token)
  const service = req.service;   // ServiceContext object (if service token)
  res.json({ project, platform, service });
});

Advanced Express Usage

import { ProjectAuthMiddleware, JwtAuthMiddleware } from '@wazobiatech/auth-middleware';

// Custom middleware with error handling
const customProjectAuth = async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
  try {
    const middleware = new ProjectAuthMiddleware('payment-service');
    await middleware.authenticate(req);

    // Additional custom validation
    if (!req.project?.scopes.includes('payments:process')) {
      return res.status(403).json({ error: 'Insufficient permissions for payment processing' });
    }

    next();
  } catch (error) {
    res.status(401).json({ error: error.message });
  }
};

app.use('/api/payments', customProjectAuth);

NestJS Integration

Module Setup

// app.module.ts
import { Module } from '@nestjs/common';
import { JwtAuthModule } from '@wazobiatech/auth-middleware/nestjs';

@Module({
  imports: [
    JwtAuthModule.forRoot({
      serviceName: 'your-service-name'
    }),
    // ... other modules
  ],
})
export class AppModule {}

Controller Guards

import { Controller, Get, UseGuards } from '@nestjs/common';
import { 
  ProjectAndUserAuth, 
  ProjectAuth, 
  UserAuth, 
  ServiceAuth,
  CurrentUser,
  CurrentProject 
} from '@wazobiatech/auth-middleware/nestjs';
import { AuthUser, ProjectContext } from '@wazobiatech/auth-middleware';

@Controller('api')
export class ApiController {

  // User authentication only
  @Get('profile')
  @UserAuth(['users:read'])  // Optional scopes
  getUserProfile(@CurrentUser() user: AuthUser) {
    return { user };
  }

  // Project authentication only  
  @Get('project/settings')
  @ProjectAuth(['projects:read'])
  getProjectSettings(@CurrentProject() project: ProjectContext) {
    return { project };
  }

  // Combined user + project authentication
  @Get('secure/data')
  @ProjectAndUserAuth({
    projectScopes: ['data:read'],
    userScopes: ['users:read']
  })
  getSecureData(
    @CurrentUser() user: AuthUser,
    @CurrentProject() project: ProjectContext
  ) {
    return { user, project };
  }

  // Service-to-service authentication
  @Get('internal/sync')
  @ServiceAuth(['data:sync', 'users:read'])
  syncData() {
    return { status: 'synced' };
  }
}

Custom Decorators for Business Logic

import { SetMetadata } from '@nestjs/common';

// Custom permission decorator
export const RequirePermission = (permission: string) => 
  SetMetadata('required-permission', permission);

// Usage
@Get('admin/users')
@UserAuth()
@RequirePermission('admin:users:manage')
async getUsers(@CurrentUser() user: AuthUser) {
  // Custom logic for permission checking
  if (!user.permissions?.includes('admin:users:manage')) {
    throw new ForbiddenException('Admin access required');
  }
  return await this.userService.getAllUsers();
}

GraphQL Integration

Basic Setup

import { GraphQLAuthHelper } from '@wazobiatech/auth-middleware';
import { Resolver, Query, Args, Context } from '@nestjs/graphql';
import { GqlContext, AuthUser } from '@wazobiatech/auth-middleware';

@Resolver()
export class UserResolver {
  private authHelper = new GraphQLAuthHelper('user-service');

  // User authentication required
  @Query()
  async getMe(
    @Args() args: any,
    @Context() context: GqlContext
  ) {
    return this.authHelper.withUserAuth(async (parent, args, ctx, info) => {
      const user = ctx.req.user;  // Authenticated user
      return { user };
    })(null, args, context, null);
  }

  // Project authentication required
  @Query() 
  async getProject(
    @Args() args: any,
    @Context() context: GqlContext
  ) {
    return this.authHelper.withProjectAuth(['projects:read'], async (parent, args, ctx, info) => {
      const project = ctx.req.project;  // Authenticated project
      return { project };
    })(null, args, context, null);
  }

  // Combined authentication
  @Query()
  async getSecureData(
    @Args() args: any, 
    @Context() context: GqlContext
  ) {
    return this.authHelper.withCombinedAuth({
      projectScopes: ['data:read'],
      userScopes: ['users:read']
    }, async (parent, args, ctx, info) => {
      const { user, project } = ctx.req;
      return { user, project, data: 'secure data' };
    })(null, args, context, null);
  }

  // Optional authentication (graceful degradation)
  @Query()
  async getPublicData(
    @Args() args: any,
    @Context() context: GqlContext  
  ) {
    return this.authHelper.withUserAuthNoStrict(async (parent, args, ctx, info) => {
      const user = ctx.req.user; // May be null if not authenticated
      const isAuthenticated = !!user;
      
      return { 
        data: 'public data',
        personalizedData: isAuthenticated ? 'personalized content' : null,
        isAuthenticated
      };
    })(null, args, context, null);
  }
}

Advanced GraphQL Patterns

@Resolver()
export class ProjectResolver {
  private authHelper = new GraphQLAuthHelper('project-service');

  // Service token authentication
  @Mutation()
  async syncProjectData(
    @Args('projectId') projectId: string,
    @Context() context: GqlContext
  ) {
    return this.authHelper.withServiceAuth(
      ['projects:sync'], 
      async (parent, args, ctx, info) => {
        // Only service tokens can access this
        const service = ctx.req.service;
        console.log(`Service ${service.service_name} syncing project ${projectId}`);
        return { success: true };
      }
    )(null, { projectId }, context, null);
  }

  // Conditional authentication based on operation type
  @Query()
  async getProjectStats(
    @Args('includePrivateData', { defaultValue: false }) includePrivateData: boolean,
    @Context() context: GqlContext
  ) {
    if (includePrivateData) {
      return this.authHelper.withProjectAuth(['projects:read:private'], 
        async (parent, args, ctx, info) => {
          return { stats: 'private stats', private: true };
        }
      )(null, { includePrivateData }, context, null);
    }
    
    // Public data - no authentication required
    return { stats: 'public stats', private: false };
  }
}

📚 API Reference

Core Classes

JwtAuthMiddleware

Handles user JWT token authentication with JWKS validation and Redis caching.

class JwtAuthMiddleware {
  constructor()
  
  // Main authentication method
  async authenticate(req: AuthenticatedRequest): Promise<void>
  
  // Internal methods (not typically used directly)
  private async getSigningKey(token: string): Promise<string>
  private async validate(token: string, publicKey: string): Promise<AuthUser>
  private async cacheValidatedToken(payload: JwtPayload, token: string): Promise<void>
  private async getCachedToken(token: string): Promise<JwtPayload | null>
}

Usage:

const middleware = new JwtAuthMiddleware();
await middleware.authenticate(req);
// req.user is now populated with AuthUser data

ProjectAuthMiddleware

Handles project, platform, and service token authentication with comprehensive validation.

class ProjectAuthMiddleware {
  constructor(serviceName: string)
  
  // Main authentication method
  async authenticate(req: AuthenticatedRequest): Promise<void>
  
  // Static middleware factory for Express
  static middleware(serviceName: string): ExpressMiddleware
  
  // Configuration
  setCacheTTL(seconds: number): void
  async cleanup(): Promise<void>
}

Usage:

const middleware = new ProjectAuthMiddleware('billing-service');
await middleware.authenticate(req);
// req.project, req.platform, or req.service is populated based on token type

GraphQLAuthHelper

Provides wrapper methods for GraphQL resolver authentication with flexible authorization patterns.

class GraphQLAuthHelper {
  constructor(serviceName: string)
  
  // Authentication wrappers
  withUserAuth<T>(resolver: ResolverFunction<T>): ResolverFunction<T>
  withUserAuth<T>(scopes: string[], resolver: ResolverFunction<T>): ResolverFunction<T>
  
  withProjectAuth<T>(resolver: ResolverFunction<T>): ResolverFunction<T>  
  withProjectAuth<T>(scopes: string[], resolver: ResolverFunction<T>): ResolverFunction<T>
  
  withCombinedAuth<T>(options: AuthOptions, resolver: ResolverFunction<T>): ResolverFunction<T>
  withCombinedAuth<T>(resolver: ResolverFunction<T>): ResolverFunction<T>
  
  withServiceAuth<T>(scopes: string[], resolver: ResolverFunction<T>): ResolverFunction<T>
  
  // Optional authentication (graceful degradation)
  withUserAuthNoStrict<T>(resolver: ResolverFunction<T>): ResolverFunction<T>
  withProjectAuthNoStrict<T>(resolver: ResolverFunction<T>): ResolverFunction<T>
  withCombinedAuthNoUserStrict<T>(options: AuthOptions, resolver: ResolverFunction<T>): ResolverFunction<T>
  withCombinedAuthNoProjectStrict<T>(options: AuthOptions, resolver: ResolverFunction<T>): ResolverFunction<T>
  
  // Direct authentication methods
  async authenticateUser(context: GqlContext): Promise<void>
  async authenticateProject(context: GqlContext): Promise<void>
}

RedisConnectionManager

Manages Redis connections with automatic reconnection and graceful shutdown.

class RedisConnectionManager {
  // Get singleton Redis instance
  static async getInstance(): Promise<RedisClient>
  
  // Connection management
  static async closeConnection(): Promise<void>
  static isConnected(): boolean
  static setupGracefulShutdown(): void
}

Usage:

// Get Redis instance (automatically connects if needed)
const redis = await RedisConnectionManager.getInstance();
await redis.set('key', 'value');

// Setup graceful shutdown (recommended)
RedisConnectionManager.setupGracefulShutdown();

Type Definitions

Token Payload Types

interface PlatformTokenPayload {
  tenant_id: string;
  secret_version: number;
  token_id: string;
  type: 'platform';
  scopes: string[];
  iat: number;
  nbf: number; 
  exp: number;
  iss: string;
  aud: string;
}

interface ProjectTokenPayload {
  tenant_id: string;
  secret_version: number;
  enabled_services: string[];
  token_id: string;
  type: 'project';
  scopes: string[];
  iat: number;
  nbf: number;
  exp: number; 
  iss: string;
  aud: string;
}

interface ServiceTokenPayload {
  type: 'service';
  client_id: string;
  service_name: string;
  scope: string; // space-separated scopes
  jti: string;
  iat: number;
  nbf: number;
  exp: number;
  iss: string;
  aud: string;
}

Context Types

interface AuthUser {
  uuid: string;
  email: string;
  name: string;
  tenant_id?: string;
  permissions?: string[];
  role?: string;
  token_id?: string;
}

interface ProjectContext {
  tenant_id: string;
  project_uuid: string;
  enabled_services: string[];
  scopes: string[];
  secret_version: number;
  token_id: string;
  expires_at: number;
}

interface PlatformContext {
  tenant_id: string;
  project_uuid: string;
  scopes: string[];
  token_id: string;
  expires_at: number;
}

interface ServiceContext {
  client_id: string;
  service_name: string;
  scopes: string[];
  token_id: string;
  issued_at: number;
  expires_at: number;
}

Request Enhancement

interface AuthenticatedRequest extends Request {
  user?: AuthUser;           // Set by JWT authentication
  project?: ProjectContext;  // Set by project token authentication
  platform?: PlatformContext; // Set by platform token authentication  
  service?: ServiceContext;  // Set by service token authentication
}

interface GqlContext {
  req: AuthenticatedRequest;
}

NestJS Decorators

Authentication Decorators

// User authentication with optional permissions
@UserAuth(scopes?: string[])

// Project authentication with optional scopes
@ProjectAuth(scopes?: string[])

// Combined user + project authentication
@ProjectAndUserAuth(options?: {
  projectScopes?: string[];
  userScopes?: string[];
})

// Service authentication with required scopes
@ServiceAuth(scopes: string[])

Parameter Decorators

// Extract authenticated user
@CurrentUser() user: AuthUser

// Extract project context  
@CurrentProject() project: ProjectContext

// Extract platform context
@CurrentPlatform() platform: PlatformContext

// Extract service context
@CurrentService() service: ServiceContext

Express Helper Functions

// User JWT authentication middleware
function jwtAuthMiddleware(): ExpressMiddleware

// Project/platform/service token authentication middleware
function projectAuthMiddleware(serviceName: string): ExpressMiddleware

🔧 Advanced Configuration

Token Validation Flow

// 1. Extract token from appropriate header
const token = req.headers.authorization || req.headers['x-project-token'];

// 2. Decode JWT header to get key ID (kid)
const { kid } = decodeJwtHeader(token);

// 3. Fetch public key from JWKS (cached)
const publicKey = await getPublicKeyFromJWKS(kid, tenantId);

// 4. Verify JWT signature and claims
const payload = jwt.verify(token, publicKey, { algorithms: ['RS512'] });

// 5. Check token revocation (if applicable)
const isRevoked = await checkTokenRevocation(payload.jti);

// 6. Validate business logic (scopes, services, etc.)
validateBusinessRules(payload);

// 7. Cache validated token
await cacheValidatedToken(payload, token);

Caching Strategy

JWKS Caching

// Per-tenant JWKS caching
const cacheKey = `jwks_cache:${tenantId}`;
const cacheTTL = 18000; // 5 hours

// Service JWKS caching
const serviceCacheKey = 'service_jwks_cache';

Token Caching

// Validated token caching
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
const cacheKey = `validated_token:${tokenHash.substring(0, 32)}`;
const cacheTTL = process.env.CACHE_EXPIRY_TIME || 3600; // 1 hour default

Revocation Tracking

// Token revocation keys
const userRevocationKey = `revoked_token:${jti}`;
const projectRevocationKey = `project_token:${token_id}`;
const platformRevocationKey = `platform_token:${token_id}`;

Custom Validation

Express Custom Middleware

import { JwtAuthMiddleware, ProjectAuthMiddleware } from '@wazobiatech/auth-middleware';

const customAuthMiddleware = async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
  try {
    // Step 1: Authenticate user
    const jwtMiddleware = new JwtAuthMiddleware();
    await jwtMiddleware.authenticate(req);
    
    // Step 2: Authenticate project  
    const projectMiddleware = new ProjectAuthMiddleware('billing-service');
    await projectMiddleware.authenticate(req);
    
    // Step 3: Custom business logic validation
    if (req.user?.tenant_id !== req.project?.tenant_id) {
      return res.status(403).json({ 
        error: 'User and project tenant mismatch' 
      });
    }
    
    // Step 4: Check custom permissions
    const requiredPermission = 'billing:process:payments';
    if (!req.user?.permissions?.includes(requiredPermission)) {
      return res.status(403).json({ 
        error: `Missing required permission: ${requiredPermission}` 
      });
    }
    
    next();
  } catch (error) {
    res.status(401).json({ error: error.message });
  }
};

NestJS Custom Guard

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { JwtAuthGuard, ProjectAuthGuard } from '@wazobiatech/auth-middleware/nestjs';

@Injectable()
export class CustomBusinessLogicGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    // Get request object
    const request = context.switchToHttp().getRequest();
    
    // Ensure both user and project are authenticated
    if (!request.user || !request.project) {
      throw new UnauthorizedException('Both user and project authentication required');
    }
    
    // Custom business logic
    if (request.user.tenant_id !== request.project.tenant_id) {
      throw new ForbiddenException('Tenant mismatch between user and project');
    }
    
    // Check if user has admin role for sensitive operations
    const isAdminOperation = request.url.includes('/admin/');
    if (isAdminOperation && request.user.role !== 'admin') {
      throw new ForbiddenException('Admin privileges required');
    }
    
    return true;
  }
}

// Usage
@Controller('billing')
@UseGuards(JwtAuthGuard, ProjectAuthGuard, CustomBusinessLogicGuard)
export class BillingController {
  @Post('process-payment')
  async processPayment() {
    // Both authentication and custom business logic passed
    return { status: 'processing' };
  }
}

Error Handling

Comprehensive Error Handling Strategy

// Express error handler
app.use((error: any, req: Request, res: Response, next: NextFunction) => {
  // Authentication errors
  if (error.message.includes('JWT')) {
    return res.status(401).json({
      error: 'Authentication failed',
      message: error.message,
      type: 'AUTH_ERROR'
    });
  }
  
  // Authorization errors  
  if (error.message.includes('permissions') || error.message.includes('scopes')) {
    return res.status(403).json({
      error: 'Insufficient permissions',
      message: error.message,
      type: 'AUTHORIZATION_ERROR'
    });
  }
  
  // Service errors
  if (error.message.includes('service')) {
    return res.status(403).json({
      error: 'Service access denied',
      message: error.message, 
      type: 'SERVICE_ERROR'
    });
  }
  
  // Redis/infrastructure errors
  if (error.message.includes('Redis') || error.message.includes('JWKS')) {
    return res.status(503).json({
      error: 'Service temporarily unavailable',
      message: 'Authentication service unavailable',
      type: 'INFRASTRUCTURE_ERROR'
    });
  }
  
  // Generic server error
  res.status(500).json({
    error: 'Internal server error',
    type: 'UNKNOWN_ERROR'
  });
});

NestJS Global Exception Filter

import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';

@Catch()
export class AuthExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    
    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let message = 'Unknown error';
    let type = 'UNKNOWN_ERROR';
    
    if (exception instanceof HttpException) {
      status = exception.getStatus();
      message = exception.message;
    } else if (exception instanceof Error) {
      if (exception.message.includes('JWT') || exception.message.includes('token')) {
        status = HttpStatus.UNAUTHORIZED;
        type = 'AUTH_ERROR';
      } else if (exception.message.includes('permission') || exception.message.includes('scope')) {
        status = HttpStatus.FORBIDDEN;
        type = 'AUTHORIZATION_ERROR';
      } else if (exception.message.includes('Redis') || exception.message.includes('JWKS')) {
        status = HttpStatus.SERVICE_UNAVAILABLE;
        type = 'INFRASTRUCTURE_ERROR';
      }
      message = exception.message;
    }
    
    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      message,
      type
    });
  }
}

// Apply globally
app.useGlobalFilters(new AuthExceptionFilter());

🔍 Debugging and Monitoring

Debug Logging

// Enable debug logging for JWKS operations
process.env.DEBUG = 'auth-middleware:jwks';

// Custom logger integration
import { Logger } from 'your-logging-library';

class CustomJwtMiddleware extends JwtAuthMiddleware {
  private logger = new Logger('JwtAuth');
  
  async authenticate(req: AuthenticatedRequest): Promise<void> {
    this.logger.info('Starting JWT authentication', {
      headers: Object.keys(req.headers),
      url: req.url,
      method: req.method
    });
    
    try {
      await super.authenticate(req);
      this.logger.info('JWT authentication successful', {
        userId: req.user?.uuid,
        tenantId: req.user?.tenant_id
      });
    } catch (error) {
      this.logger.error('JWT authentication failed', {
        error: error.message,
        url: req.url
      });
      throw error;
    }
  }
}

Health Check Integration

// Express health check
app.get('/health/auth', async (req, res) => {
  try {
    // Check Redis connection
    const redis = await RedisConnectionManager.getInstance();
    await redis.ping();
    
    // Check Mercury service connectivity
    const testJwksUrl = `${process.env.MERCURY_BASE_URL}/auth/service/.well-known/jwks.json`;
    const response = await fetch(testJwksUrl, { timeout: 5000 });
    
    if (!response.ok) {
      throw new Error(`Mercury service returned ${response.status}`);
    }
    
    res.json({
      status: 'healthy',
      redis: 'connected',
      mercury: 'reachable',
      timestamp: new Date().toISOString()
    });
  } catch (error) {
    res.status(503).json({
      status: 'unhealthy',
      error: error.message,
      timestamp: new Date().toISOString()
    });
  }
});

Performance Monitoring

// Metrics collection middleware
const authMetrics = {
  jwtValidations: 0,
  projectValidations: 0,
  cacheHits: 0,
  cacheMisses: 0,
  errors: 0
};

class MetricsJwtMiddleware extends JwtAuthMiddleware {
  async authenticate(req: AuthenticatedRequest): Promise<void> {
    const startTime = Date.now();
    
    try {
      await super.authenticate(req);
      authMetrics.jwtValidations++;
      
      const duration = Date.now() - startTime;
      console.log(`JWT validation completed in ${duration}ms`);
    } catch (error) {
      authMetrics.errors++;
      throw error;
    }
  }
}

// Metrics endpoint
app.get('/metrics/auth', (req, res) => {
  res.json({
    ...authMetrics,
    uptime: process.uptime(),
    redisConnected: RedisConnectionManager.isConnected()
  });
});

🧪 Testing

Testing Configuration

// test/setup.ts
import { RedisConnectionManager } from '@wazobiatech/auth-middleware/utils/redis.connection';

beforeAll(async () => {
  // Setup test Redis instance
  process.env.REDIS_URL = 'redis://localhost:6379/1'; // Use test database
  process.env.MERCURY_BASE_URL = 'http://localhost:4000';
  process.env.SIGNATURE_SHARED_SECRET = 'test-secret';
});

afterAll(async () => {
  // Cleanup Redis connection
  await RedisConnectionManager.closeConnection();
});

Unit Testing

// test/jwt-middleware.test.ts
import { JwtAuthMiddleware } from '@wazobiatech/auth-middleware';
import { AuthenticatedRequest } from '@wazobiatech/auth-middleware';

describe('JwtAuthMiddleware', () => {
  let middleware: JwtAuthMiddleware;
  let mockRequest: Partial<AuthenticatedRequest>;

  beforeEach(() => {
    middleware = new JwtAuthMiddleware();
    mockRequest = {
      headers: {},
      user: undefined
    };
  });

  it('should reject requests without authorization header', async () => {
    await expect(middleware.authenticate(mockRequest as AuthenticatedRequest))
      .rejects.toThrow('No authorization header provided');
  });

  it('should reject malformed JWT tokens', async () => {
    mockRequest.headers!.authorization = 'Bearer invalid.jwt.token';
    
    await expect(middleware.authenticate(mockRequest as AuthenticatedRequest))
      .rejects.toThrow('Invalid JWT token');
  });

  it('should authenticate valid JWT tokens', async () => {
    // Mock valid JWT token and JWKS response
    const validToken = 'Bearer valid.jwt.token';
    mockRequest.headers!.authorization = validToken;
    
    // Mock JWKS and Redis responses
    jest.spyOn(middleware as any, 'getSigningKey').mockResolvedValue('mock-public-key');
    jest.spyOn(middleware as any, 'validate').mockResolvedValue({
      uuid: 'user-123',
      email: '[email protected]',
      name: 'Test User'
    });
    
    await middleware.authenticate(mockRequest as AuthenticatedRequest);
    
    expect(mockRequest.user).toBeDefined();
    expect(mockRequest.user!.uuid).toBe('user-123');
  });
});

Integration Testing

// test/integration/express.test.ts
import express from 'express';
import request from 'supertest';
import { jwtAuthMiddleware, projectAuthMiddleware } from '@wazobiatech/auth-middleware/express';

describe('Express Integration', () => {
  let app: express.Application;

  beforeEach(() => {
    app = express();
    
    app.get('/user/profile', jwtAuthMiddleware(), (req, res) => {
      res.json({ user: req.user });
    });
    
    app.get('/project/data', projectAuthMiddleware('test-service'), (req, res) => {
      res.json({ project: req.project });
    });
  });

  it('should protect user routes', async () => {
    const response = await request(app)
      .get('/user/profile')
      .expect(401);
      
    expect(response.body).toHaveProperty('error');
  });

  it('should allow authenticated user requests', async () => {
    const validToken = generateTestJWT(); // Helper function to create test tokens
    
    const response = await request(app)
      .get('/user/profile')
      .set('Authorization', `Bearer ${validToken}`)
      .expect(200);
      
    expect(response.body).toHaveProperty('user');
  });
});

E2E Testing

// test/e2e/auth-flow.test.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { JwtAuthModule } from '@wazobiatech/auth-middleware/nestjs';

describe('Authentication Flow (E2E)', () => {
  let app: INestApplication;

  beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [
        JwtAuthModule.forRoot({ serviceName: 'test-service' }),
        // ... your app modules
      ],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('should handle complete authentication flow', async () => {
    // Step 1: Get service token
    const serviceToken = await getServiceToken();
    
    // Step 2: Use service token for authenticated request
    const response = await request(app.getHttpServer())
      .get('/api/secure/data')
      .set('x-project-token', serviceToken)
      .expect(200);
      
    expect(response.body).toHaveProperty('data');
  });

  it('should handle user + project authentication', async () => {
    const userToken = await getUserToken();
    const projectToken = await getProjectToken();
    
    const response = await request(app.getHttpServer())
      .get('/api/user/projects')
      .set('Authorization', `Bearer ${userToken}`)
      .set('x-project-token', projectToken)
      .expect(200);
      
    expect(response.body).toHaveProperty('user');
    expect(response.body).toHaveProperty('project');
  });
});

🚀 Performance Optimization

Redis Connection Pooling

The library automatically manages Redis connections efficiently:

// Automatic connection management
const redis = await RedisConnectionManager.getInstance();

// Connections are reused across requests
// Health checks prevent stale connections
// Automatic reconnection on failures
// Graceful shutdown handling

JWKS Caching Strategy

// JWKS keys are cached per tenant for 5 hours
const jwksCacheTTL = 18000; // 5 hours

// Cache key format
const cacheKey = `jwks_cache:${tenantId}`;

// Automatic refresh on key miss
if (!keyFoundInCache) {
  await refreshJWKSFromMercury();
}

Token Validation Optimization

// Token caching reduces validation overhead
const tokenCacheKey = `validated_token:${tokenHash}`;

// Cached tokens skip expensive operations:
// - JWKS fetching
// - JWT signature verification  
// - Claims validation
// - Redis revocation checks (for user tokens)

// Cache TTL matches token expiration
const cacheTTL = Math.min(tokenExpiry - now, maxCacheTTL);

Performance Monitoring

// Built-in performance metrics
interface AuthMetrics {
  cacheHitRate: number;        // Token cache efficiency
  jwksCacheHitRate: number;    // JWKS cache efficiency  
  avgValidationTime: number;   // Average validation duration
  redisConnectionHealth: boolean;
  activeConnections: number;
}

// Access metrics programmatically
const metrics = await getAuthMetrics();
console.log(`Cache hit rate: ${metrics.cacheHitRate}%`);

🛠️ Troubleshooting

Common Issues

1. Redis Connection Errors

Error: Redis connection failed: ECONNREFUSED

Solutions:

  • Verify Redis server is running: redis-cli ping
  • Check REDIS_URL environment variable
  • Ensure network connectivity
  • Check Redis authentication credentials
// Debug Redis connection
try {
  const redis = await RedisConnectionManager.getInstance();
  console.log('Redis connected:', await redis.ping());
} catch (error) {
  console.error('Redis connection failed:', error.message);
}

2. JWKS Endpoint Unreachable

Error: JWKS endpoint not reachable

Solutions:

  • Verify MERCURY_BASE_URL is correct
  • Check network connectivity to Mercury service
  • Ensure SIGNATURE_SHARED_SECRET is properly configured
  • Test JWKS endpoint manually:
curl -H "X-Timestamp: $(date +%s)" \
     -H "X-Signature: your-signature" \
     http://your-mercury-url/auth/projects/tenant-id/.well-known/jwks.json

3. Token Validation Failures

Error: Invalid JWT token: Key kid123 not found in JWKS

Solutions:

  • Clear JWKS cache: Delete Redis key jwks_cache:${tenantId}
  • Verify token was issued by correct Mercury instance
  • Check token header contains valid kid (key ID)
  • Ensure tenant_id extraction is working
// Debug token validation
const middleware = new JwtAuthMiddleware();
try {
  await middleware.authenticate(req);
} catch (error) {
  console.error('Validation failed:', {
    error: error.message,
    tokenHeader: req.headers.authorization?.substring(0, 20),
    tenantId: extractTenantFromToken(token)
  });
}

4. Service Access Denied

Error: Service 'billing-service' is not enabled for this project

Solutions:

  • Verify service name matches exactly (case-sensitive)
  • Check project token enabled_services array
  • Ensure CLIENT_ID and CLIENT_SECRET are correct
  • Verify service is registered in Mercury
// Debug service validation
const projectMiddleware = new ProjectAuthMiddleware('billing-service');
try {
  await projectMiddleware.authenticate(req);
} catch (error) {
  console.error('Service validation failed:', {
    error: error.message,
    expectedService: 'billing-service',
    enabledServices: req.project?.enabled_services,
    tokenType: req.project ? 'project' : req.platform ? 'platform' : req.service ? 'service' : 'unknown'
  });
}

5. Scope/Permission Errors

Error: Insufficient permissions. Required: billing:write, Provided: billing:read

Solutions:

  • Verify user/token has required permissions
  • Check scope definitions in Mercury
  • Ensure permission names match exactly
  • Review token payload for granted permissions
// Debug permissions
console.log('Available permissions:', {
  userPermissions: req.user?.permissions,
  projectScopes: req.project?.scopes,
  serviceScopes: req.service?.scopes
});

Debugging Tools

Environment Variable Checker

// utils/env-check.ts
export function validateEnvironment(): string[] {
  const required = ['REDIS_URL', 'MERCURY_BASE_URL', 'SIGNATURE_SHARED_SECRET'];
  const missing = required.filter(key => !process.env[key]);
  
  if (missing.length > 0) {
    console.error('Missing required environment variables:', missing);
  }
  
  return missing;
}

Token Inspector

// utils/token-inspector.ts
import * as jwt from 'jsonwebtoken';

export function inspectToken(token: string) {
  try {
    const decoded = jwt.decode(token, { complete: true });
    
    console.log('Token Analysis:', {
      header: decoded?.header,
      payload: decoded?.payload,
      isExpired: decoded?.payload && typeof decoded.payload === 'object' 
        ? decoded.payload.exp < Date.now() / 1000 
        : 'unknown'
    });
    
    return decoded;
  } catch (error) {
    console.error('Token decode failed:', error.message);
    return null;
  }
}

Redis Cache Inspector

// utils/cache-inspector.ts
export async function inspectCache(pattern: string = '*') {
  try {
    const redis = await RedisConnectionManager.getInstance();
    const keys = await redis.keys(pattern);
    
    console.log('Cache Contents:', {
      totalKeys: keys.length,
      jwksKeys: keys.filter(k => k.startsWith('jwks_cache:')),
      tokenKeys: keys.filter(k => k.startsWith('validated_token:')),
      revocationKeys: keys.filter(k => k.includes('revoked_token:'))
    });
    
    return keys;
  } catch (error) {
    console.error('Cache inspection failed:', error.message);
    return [];
  }
}

Performance Debugging

Slow Authentication

// Enable performance timing
process.env.AUTH_DEBUG_TIMING = 'true';

class TimedJwtMiddleware extends JwtAuthMiddleware {
  async authenticate(req: AuthenticatedRequest): Promise<void> {
    const timings = {
      start: Date.now(),
      keyFetch: 0,
      validation: 0,
      caching: 0,
      total: 0
    };
    
    try {
      // Time key fetching
      const keyStart = Date.now();
      // ... key fetching logic
      timings.keyFetch = Date.now() - keyStart;
      
      // Time validation
      const validationStart = Date.now();
      await super.authenticate(req);
      timings.validation = Date.now() - validationStart;
      
      timings.total = Date.now() - timings.start;
      
      if (timings.total > 1000) { // Log slow authentications
        console.warn('Slow authentication detected:', timings);
      }
    } catch (error) {
      throw error;
    }
  }
}

🔒 Security Considerations

Production Security Checklist

  • [ ] Environment Variables: Never commit sensitive values to version control
  • [ ] Redis Security: Use Redis AUTH and enable TLS for production
  • [ ] Network Security: Restrict Mercury service access to known IPs
  • [ ] Token Storage: Never log complete tokens, only previews
  • [ ] Error Handling: Avoid exposing internal details in error messages
  • [ ] HTTPS: Always use HTTPS in production environments
  • [ ] Key Rotation: Implement regular JWT signing key rotation
  • [ ] Monitoring: Set up alerts for authentication failures

Security Best Practices

Secure Token Handling

// ✅ Good: Log only token preview
console.log('Token received:', token.substring(0, 20) + '...');

// ❌ Bad: Never log complete tokens
console.log('Token:', token); // Security risk!

Rate Limiting

import rateLimit from 'express-rate-limit';

// Apply rate limiting to authentication endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many authentication requests',
  standardHeaders: true,
  legacyHeaders: false
});

app.use('/api', authLimiter);

Secure Headers

import helmet from 'helmet';

// Apply security headers
app.use(helmet({
  crossOriginEmbedderPolicy: false,
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
    },
  },
}));

📈 Migration Guide

From v1.0.x to v1.1.x

Breaking Changes

  • AuthUser interface now includes optional permissions array
  • Project tokens now validate against enabled_services array
  • Redis connection management changed to singleton pattern

Migration Steps

  1. Update Type Definitions
// Before
interface AuthUser {
  uuid: string;
  email: string;
  name: string;
}

// After  
interface AuthUser {
  uuid: string;
  email: string;
  name: string;
  tenant_id?: string;
  permissions?: string[];  // New field
  token_id?: string;       // New field
}
  1. Update Project Token Validation
// Before
const middleware = new ProjectAuthMiddleware();

// After
const middleware = new ProjectAuthMiddleware('your-service-name');
  1. Update Redis Connection Usage
// Before
import { redis } from '@wazobiatech/auth-middleware';

// After
import RedisConnectionManager from '@wazobiatech/auth-middleware/utils/redis.connection';
const redis = await RedisConnectionManager.getInstance();

Legacy JWT Payload Support

The library maintains backward compatibility with legacy JWT payloads:

// Legacy payload structure still supported
interface LegacyJwtPayload {
  sub?: {
    uuid: string;
    email: string;
    name: string;
  };
  project_uuid?: string;  // Mapped to tenant_id
  permissions?: string[];
  // ... other legacy fields
}

📚 Additional Resources

Related Documentation

Example Projects

Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/amazing-feature
  3. Commit your changes: git commit -m 'Add amazing feature'
  4. Push to the branch: git push origin feature/amazing-feature
  5. Open a Pull Request

Support


📄 License

This project is licensed under the MIT License - see the LICENSE file for details.


Made with ❤️ by the Wazobia Platform Team