Skip to main content

@helix/shared/middleware

Shared NestJS middleware for the Helix platform.

Overview

This library provides four essential middleware components for all Helix microservices:

  1. Authentication Middleware - JWT validation with WorkOS
  2. Tenant Context Middleware - Tenant resolution and caching
  3. Request Logging Middleware - Request ID generation and timing
  4. Rate Limiting Middleware - Tenant-based throttling

Installation

This is an internal library in the Nx monorepo. Import using the TypeScript path alias:

import {
AuthenticationMiddleware,
TenantContextMiddleware,
RequestLoggingMiddleware,
RateLimitMiddleware,
} from '@helix/shared/middleware';

Middleware Order

CRITICAL: Middleware must be applied in this exact order:

export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(
RequestLoggingMiddleware, // 1. Generate request ID
AuthenticationMiddleware, // 2. Validate JWT, extract user
TenantContextMiddleware, // 3. Resolve tenant context
RateLimitMiddleware, // 4. Check rate limits
)
.forRoutes('*'); // Apply to all routes except health checks
}
}

Why This Order?

  1. Logging first - Generates request ID for tracing
  2. Auth second - Validates user, extracts role from JWT
  3. Tenant third - Resolves tenant (needs user context)
  4. Rate limit last - Throttles based on tenant+user+role

1. Authentication Middleware

Validates JWT tokens from WorkOS and extracts user context.

Features

  • JWT validation against WorkOS
  • Token expiry checking
  • User ID and role extraction from JWT
  • Error handling for invalid tokens

WorkOS RBAC Integration

IMPORTANT: Roles and scopes are extracted from WorkOS JWT tokens, NOT from our databases.

// JWT payload from WorkOS contains:
{
sub: 'user_01HXYZ',
tenantId: 'tenant_abc',
role: 'tenant_admin', // From WorkOS RBAC
scopes: ['tenant:write', ...], // From WorkOS RBAC
email: 'user@example.com',
name: 'John Doe'
}

// Middleware extracts and attaches to request:
req.user = {
userId: 'user_01HXYZ',
tenantId: 'tenant_abc',
role: 'tenant_admin', // NOT from database
scopes: ['tenant:write', ...], // NOT from database
email: 'user@example.com',
displayName: 'John Doe'
};

Usage

import { AuthenticationMiddleware } from '@helix/shared/middleware';

@Module({
providers: [
{
provide: 'WORKOS_CLIENT',
useFactory: () => {
return new WorkOS(process.env.WORKOS_API_KEY);
},
},
AuthenticationMiddleware,
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AuthenticationMiddleware)
.forRoutes('*');
}
}

Error Responses

// Missing token
{
statusCode: 401,
message: 'No authorization token provided',
error: 'Unauthorized'
}

// Invalid/expired token
{
statusCode: 401,
message: 'Invalid or expired token',
error: 'Unauthorized'
}

2. Tenant Context Middleware

Resolves tenant information and caches it using Redis.

Features

  • Verifies user has access to tenant
  • Loads tenant display information
  • Redis caching (30-minute TTL)
  • Multi-tenant support

WorkOS RBAC Alignment

CRITICAL ARCHITECTURAL CHANGE: This middleware does NOT query for user roles.

// OLD (incorrect): Query role from database
const membership = await prisma.tenantMember.findFirst({
where: { workosUserId, tenantId },
select: { role: true } // ❌ NO! Role not in database
});

// NEW (correct): Role already in req.user from JWT
req.user.role; // ✅ From WorkOS JWT token

// Middleware only loads tenant information
req.tenant = {
tenantId: 'tenant_abc',
tenantName: 'Acme Corp',
tenantDomain: 'acme.com',
tenantStatus: 'active',
tenantSettings: { ... }
// NO role - comes from JWT
};

Usage

import { TenantContextMiddleware } from '@helix/shared/middleware';

@Module({
providers: [
TenantContextMiddleware,
CentralPrismaService,
RedisService,
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(
AuthenticationMiddleware,
TenantContextMiddleware, // After auth
)
.forRoutes('*');
}
}

Cache Management

// Clear cache for a user
await tenantContextMiddleware.clearCache(userId, tenantId);

// Clear all caches for a tenant (when tenant info changes)
await tenantContextMiddleware.clearTenantCache(tenantId);

Error Responses

// No user context (auth middleware not run)
{
statusCode: 403,
message: 'User context not found',
error: 'Forbidden'
}

// User doesn't have access to tenant
{
statusCode: 403,
message: 'User does not have access to this tenant',
error: 'Forbidden'
}

3. Request Logging Middleware

Generates unique request IDs and logs request/response details.

Features

  • UUID generation for each request
  • Request/response timing
  • User and tenant context logging
  • Sanitized query parameters
  • X-Request-ID header in response

Usage

import { RequestLoggingMiddleware } from '@helix/shared/middleware';

@Module({
providers: [RequestLoggingMiddleware],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(RequestLoggingMiddleware) // First middleware
.forRoutes('*');
}
}

Log Output

// Incoming request
{
"type": "request",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"method": "GET",
"path": "/api/users/123",
"query": {},
"userId": "user_01HXYZ",
"tenantId": "tenant_abc",
"ip": "192.168.1.1",
"userAgent": "Mozilla/5.0..."
}

// Outgoing response
{
"type": "response",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"method": "GET",
"path": "/api/users/123",
"statusCode": 200,
"duration": 45,
"userId": "user_01HXYZ",
"tenantId": "tenant_abc"
}

Accessing Request ID

// In a controller
@Get()
async getData(@Req() req: Request) {
const requestId = (req as any).requestId;
this.logger.log(`Processing request ${requestId}`);
}

// Request ID is also in response headers
// X-Request-ID: 550e8400-e29b-41d4-a716-446655440000

4. Rate Limiting Middleware

Enforces tenant-based rate limits using Redis.

Features

  • Tenant+user rate limiting
  • Configurable limits by role (from JWT)
  • Sliding window algorithm
  • Standard rate limit headers
  • Administrative controls

Default Limits

RoleRequests/Minute
tenant_admin1000
product_admin500
user100
read_only50

Usage

import { RateLimitMiddleware } from '@helix/shared/middleware';

// With default limits
@Module({
providers: [
{
provide: RateLimitMiddleware,
useFactory: (redis: Redis) => {
return new RateLimitMiddleware(redis);
},
inject: [RedisService],
},
],
})
export class AppModule {}

// With custom limits
@Module({
providers: [
{
provide: RateLimitMiddleware,
useFactory: (redis: Redis) => {
return new RateLimitMiddleware(redis, {
tenant_admin: { limit: 2000, window: 60 },
user: { limit: 200, window: 60 },
});
},
inject: [RedisService],
},
],
})
export class AppModule {}

Response Headers

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1705583640

Error Response

// Rate limit exceeded
{
statusCode: 429,
message: 'Rate limit exceeded. Limit: 100 requests per 60 seconds',
error: 'Too Many Requests',
retryAfter: 1705583640
}

Administrative Controls

// Get current rate limit status
const status = await rateLimitMiddleware.getRateLimitStatus(
tenantId,
userId,
role
);
// Returns: { count: 45, limit: 100, remaining: 55, resetTime: 1705583640 }

// Reset rate limit for a user
await rateLimitMiddleware.resetRateLimit(tenantId, userId);

Complete Example

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import {
AuthenticationMiddleware,
TenantContextMiddleware,
RequestLoggingMiddleware,
RateLimitMiddleware,
} from '@helix/shared/middleware';
import { WorkOS } from '@workos-inc/node';
import { PrismaService } from '@helix/prisma-central';
import { RedisService } from './redis.service';

@Module({
providers: [
// WorkOS client
{
provide: 'WORKOS_CLIENT',
useFactory: () => new WorkOS(process.env.WORKOS_API_KEY),
},
// Prisma
PrismaService,
// Redis
RedisService,
// Middleware
{
provide: AuthenticationMiddleware,
useFactory: (workos: WorkOS) => new AuthenticationMiddleware(workos),
inject: ['WORKOS_CLIENT'],
},
{
provide: TenantContextMiddleware,
useFactory: (prisma: PrismaService, redis: RedisService) =>
new TenantContextMiddleware(prisma, redis),
inject: [PrismaService, RedisService],
},
RequestLoggingMiddleware,
{
provide: RateLimitMiddleware,
useFactory: (redis: RedisService) =>
new RateLimitMiddleware(redis, {
tenant_admin: { limit: 2000, window: 60 },
user: { limit: 200, window: 60 },
}),
inject: [RedisService],
},
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(
RequestLoggingMiddleware, // 1. Logging
AuthenticationMiddleware, // 2. Auth
TenantContextMiddleware, // 3. Tenant
RateLimitMiddleware, // 4. Rate limit
)
.exclude(
{ path: 'health', method: RequestMethod.GET }, // Skip health checks
)
.forRoutes('*');
}
}

Request Context Shape

After all middleware runs, requests have this structure:

interface EnhancedRequest extends Request {
// From RequestLoggingMiddleware
requestId: string;

// From AuthenticationMiddleware
user: {
userId: string; // WorkOS user ID
tenantId: string; // From JWT
role: UserRole; // From JWT (NOT database)
scopes: PermissionScope[]; // From JWT (NOT database)
email: string;
displayName: string;
};

// From TenantContextMiddleware
tenant: {
tenantId: string;
tenantName: string;
tenantDomain: string;
tenantStatus: TenantStatus;
tenantSettings: Record<string, any>;
// NO role - comes from JWT
};
}

WorkOS RBAC Architecture

Key Principles

  1. Roles managed by WorkOS - Never stored in our databases
  2. JWT contains everything - Role, scopes, tenant ID
  3. No role synchronization - WorkOS is single source of truth
  4. Faster authorization - No database queries for roles
  5. Better security - Roles in signed tokens

Authorization Flow

// 1. User authenticates with WorkOS
// 2. WorkOS returns JWT with role and scopes
// 3. AuthenticationMiddleware validates JWT
// 4. Role and scopes attached to req.user

// In your guards
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const user = request.user;

// Role comes from JWT, not database!
return user.role === 'tenant_admin';
}
}

// Product access is SEPARATE (stored in Tenant DB)
const hasKiraAccess = await tenantDb.user_product_access.findFirst({
where: {
team_member: { workosUserId: user.userId },
product_type: 'kira',
revoked_at: null
}
});

Testing

describe('AuthenticationMiddleware', () => {
it('should validate JWT and attach user context', async () => {
// Test JWT validation
});

it('should reject invalid tokens', async () => {
// Test error handling
});
});

describe('TenantContextMiddleware', () => {
it('should resolve tenant from user ID', async () => {
// Test tenant resolution
});

it('should use Redis cache', async () => {
// Test caching behavior
});
});

describe('RequestLoggingMiddleware', () => {
it('should generate unique request IDs', () => {
// Test UUID generation
});

it('should log request timing', () => {
// Test timing logs
});
});

describe('RateLimitMiddleware', () => {
it('should enforce rate limits', async () => {
// Test rate limiting
});

it('should set rate limit headers', async () => {
// Test headers
});
});


Development

# Build the library
nx build shared-middleware

# Lint the library
nx lint shared-middleware

# Test the library
nx test shared-middleware

License

Proprietary - CleverChain Limited