@wazobiatech/auth-middleware
v1.0.16
Published
Framework-agnostic JWT authentication library for Wazobia microservices platform
Downloads
538
Maintainers
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_servicesarray 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-middlewarePeer 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 dataProjectAuthMiddleware
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 typeGraphQLAuthHelper
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: ServiceContextExpress 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 defaultRevocation 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 handlingJWKS 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: ECONNREFUSEDSolutions:
- 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 reachableSolutions:
- 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.json3. Token Validation Failures
Error: Invalid JWT token: Key kid123 not found in JWKSSolutions:
- 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 projectSolutions:
- Verify service name matches exactly (case-sensitive)
- Check project token
enabled_servicesarray - 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:readSolutions:
- 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
AuthUserinterface now includes optionalpermissionsarray- Project tokens now validate against
enabled_servicesarray - Redis connection management changed to singleton pattern
Migration Steps
- 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
}- Update Project Token Validation
// Before
const middleware = new ProjectAuthMiddleware();
// After
const middleware = new ProjectAuthMiddleware('your-service-name');- 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
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Commit your changes:
git commit -m 'Add amazing feature' - Push to the branch:
git push origin feature/amazing-feature - Open a Pull Request
Support
- GitHub Issues: Report bugs or request features
- Discord: Join our developer community
- Email: [email protected]
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
Made with ❤️ by the Wazobia Platform Team
