Spaces:
Paused
Paused
| /** | |
| * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| * β 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 | |
| }; | |