Kraft102's picture
Initial deployment - WidgeTDC Cortex Backend v2.1.0
529090e
/**
* ╔═══════════════════════════════════════════════════════════════════════════╗
* β•‘ JWT AUTHENTICATION MIDDLEWARE β•‘
* ╠═══════════════════════════════════════════════════════════════════════════╣
* β•‘ Security Gate for WidgeTDC API β•‘
* β•‘ β€’ JWT token validation β•‘
* β•‘ β€’ Role-based access control β•‘
* β•‘ β€’ API key fallback for service-to-service communication β•‘
* β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
*/
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
// ═══════════════════════════════════════════════════════════════════════════
// Types
// ═══════════════════════════════════════════════════════════════════════════
export interface JWTPayload {
sub: string; // User ID
email?: string;
name?: string;
roles: string[]; // ['admin', 'user', 'agent', 'service']
iat: number; // Issued at
exp: number; // Expiration
}
export interface AuthenticatedRequest extends Request {
user?: JWTPayload;
authMethod?: 'jwt' | 'api-key' | 'bypass';
}
// ═══════════════════════════════════════════════════════════════════════════
// Configuration
// ═══════════════════════════════════════════════════════════════════════════
const JWT_SECRET = process.env.JWT_SECRET || 'widgetdc-dev-secret-change-in-production';
const API_KEY = process.env.API_KEY || 'widgetdc-dev-api-key';
const AUTH_ENABLED = process.env.AUTH_ENABLED !== 'false'; // Default: enabled
const DEV_BYPASS = process.env.NODE_ENV === 'development' && process.env.AUTH_BYPASS === 'true';
// Routes that don't require authentication
const PUBLIC_ROUTES = [
'/health',
'/api/health',
'/api/mcp/route', // MCP uses its own auth
'/api/mcp/ws', // WebSocket has its own handshake
];
// ═══════════════════════════════════════════════════════════════════════════
// Middleware Functions
// ═══════════════════════════════════════════════════════════════════════════
/**
* Main authentication middleware
* Checks JWT token or API key
*/
export function authenticate(req: AuthenticatedRequest, res: Response, next: NextFunction): void {
// Skip auth in dev bypass mode
if (DEV_BYPASS) {
req.user = {
sub: 'dev-user',
email: 'dev@widgetdc.local',
name: 'Development User',
roles: ['admin', 'user', 'agent', 'service'],
iat: Date.now() / 1000,
exp: Date.now() / 1000 + 86400
};
req.authMethod = 'bypass';
return next();
}
// Skip auth for public routes
if (PUBLIC_ROUTES.some(route => req.path.startsWith(route))) {
return next();
}
// Skip if auth is disabled
if (!AUTH_ENABLED) {
req.authMethod = 'bypass';
return next();
}
// Try JWT token first (Authorization: Bearer <token>)
const authHeader = req.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
const token = authHeader.substring(7);
try {
const payload = jwt.verify(token, JWT_SECRET) as JWTPayload;
req.user = payload;
req.authMethod = 'jwt';
return next();
} catch (error: any) {
if (error.name === 'TokenExpiredError') {
res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
return;
}
// Token invalid, try API key
}
}
// Try API key (X-API-Key header or query param)
const apiKey = req.headers['x-api-key'] || req.query.api_key;
if (apiKey === API_KEY) {
req.user = {
sub: 'api-service',
roles: ['service'],
iat: Date.now() / 1000,
exp: Date.now() / 1000 + 86400
};
req.authMethod = 'api-key';
return next();
}
// No valid auth found
res.status(401).json({
error: 'Authentication required',
code: 'AUTH_REQUIRED',
hint: 'Provide Bearer token or X-API-Key header'
});
}
/**
* Role-based authorization middleware factory
* Usage: app.use('/admin', requireRole('admin'))
*/
export function requireRole(...roles: string[]) {
return (req: AuthenticatedRequest, res: Response, next: NextFunction): void => {
if (!req.user) {
res.status(401).json({ error: 'Not authenticated', code: 'NOT_AUTHENTICATED' });
return;
}
const hasRole = roles.some(role => req.user!.roles.includes(role));
if (!hasRole) {
res.status(403).json({
error: 'Insufficient permissions',
code: 'FORBIDDEN',
required: roles,
actual: req.user.roles
});
return;
}
next();
};
}
/**
* Optional authentication - populates req.user if token present, but doesn't require it
*/
export function optionalAuth(req: AuthenticatedRequest, res: Response, next: NextFunction): void {
const authHeader = req.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
const token = authHeader.substring(7);
try {
const payload = jwt.verify(token, JWT_SECRET) as JWTPayload;
req.user = payload;
req.authMethod = 'jwt';
} catch {
// Invalid token, continue without user
}
}
next();
}
// ═══════════════════════════════════════════════════════════════════════════
// Token Generation (for login endpoints)
// ═══════════════════════════════════════════════════════════════════════════
/**
* Generate a JWT token for a user
*/
export function generateToken(payload: Omit<JWTPayload, 'iat' | 'exp'>, expiresIn: string = '24h'): string {
return jwt.sign(payload, JWT_SECRET, { expiresIn } as jwt.SignOptions);
}
/**
* Generate a refresh token (longer lived)
*/
export function generateRefreshToken(userId: string): string {
return jwt.sign(
{ sub: userId, type: 'refresh' },
JWT_SECRET,
{ expiresIn: '7d' }
);
}
/**
* Verify a refresh token and return user ID
*/
export function verifyRefreshToken(token: string): string | null {
try {
const payload = jwt.verify(token, JWT_SECRET) as any;
if (payload.type === 'refresh') {
return payload.sub;
}
return null;
} catch {
return null;
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Export
// ═══════════════════════════════════════════════════════════════════════════
export default {
authenticate,
requireRole,
optionalAuth,
generateToken,
generateRefreshToken,
verifyRefreshToken
};