UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.

350 lines (349 loc) 13.8 kB
/** * Authentication and Role-Based Access Control (RBAC) Middleware * * Implements JWT-based authentication and role-based access control for * sensitive operations like skill promotion, approval, and deployment. * * Features: * - JWT token validation and expiration checks * - Role-based access control with granular permissions * - Session-based authentication fallback * - Audit logging for authorization failures * - Per-operation permission validation * * Roles: * - admin: Full access to all promotion operations * - developer: Can initiate promotions, but not approve/deploy * - readonly: Can view audit trails, but no promotion access */ import { StandardError, ErrorCode } from '../lib/errors.js'; import { createLogger } from '../lib/logging.js'; import * as jwt from 'jsonwebtoken'; const logger = createLogger('auth-middleware'); /** * User role enum */ export var UserRole = /*#__PURE__*/ function(UserRole) { UserRole["ADMIN"] = "admin"; UserRole["DEVELOPER"] = "developer"; UserRole["READONLY"] = "readonly"; return UserRole; }({}); /** * Promotion operation enum */ export var PromotionOperation = /*#__PURE__*/ function(PromotionOperation) { PromotionOperation["INITIATE"] = "initiate-promotion"; PromotionOperation["VALIDATE"] = "validate-skill"; PromotionOperation["TEST"] = "test-skill"; PromotionOperation["APPROVE"] = "approve-promotion"; PromotionOperation["DEPLOY"] = "deploy-to-production"; PromotionOperation["ROLLBACK"] = "rollback-deployment"; return PromotionOperation; }({}); /** * Permission mapping: role -> allowed operations */ const ROLE_PERMISSIONS = { ["admin"]: [ "initiate-promotion", "validate-skill", "test-skill", "approve-promotion", "deploy-to-production", "rollback-deployment" ], ["developer"]: [ "initiate-promotion", "validate-skill", "test-skill" ], ["readonly"]: [] }; /** * Authentication middleware for validating user identity * * SECURITY CRITICAL: JWT_SECRET must be configured via environment variable * or explicitly provided. No default secrets are allowed in production. */ export class AuthMiddleware { jwtSecret; tokenExpirationSeconds; sessions; // List of insecure default secrets that must be rejected (CVSS 9.8 vulnerability) static INSECURE_SECRETS = [ 'dev-secret-key', 'secret', 'password', 'test', 'default', '123456', 'changeme' ]; /** * Create authentication middleware * * @param jwtSecret - JWT signing secret (REQUIRED). If not provided, will attempt * to load from JWT_SECRET environment variable. Throws error if * neither is available. * @param tokenExpirationSeconds - Token expiration time in seconds (default: 3600) * @throws StandardError with CONFIGURATION_ERROR if JWT_SECRET is not configured * @throws StandardError with VALIDATION_FAILED if JWT_SECRET is empty, too short * (<16 chars), or matches known insecure defaults * * @example * // Explicit secret (for testing) * const auth = new AuthMiddleware('strong-secret-key-at-least-16-chars'); * * @example * // From environment variable (production) * process.env.JWT_SECRET = 'production-secret-at-least-16-chars'; * const auth = new AuthMiddleware(); */ constructor(jwtSecret, tokenExpirationSeconds = 3600){ // Attempt to resolve JWT secret from parameter or environment const resolvedSecret = jwtSecret ?? process.env.JWT_SECRET; // Fail fast if JWT_SECRET is not configured if (!resolvedSecret) { throw new StandardError(ErrorCode.CONFIGURATION_ERROR, 'JWT_SECRET is required but not configured. Please set the JWT_SECRET environment variable or provide it explicitly to the constructor.', { hint: 'Set JWT_SECRET in your .env file or environment: export JWT_SECRET="your-secret-key"', securityNote: 'Never use default secrets in production. Generate a strong random secret.' }); } // Trim and validate secret is not empty or whitespace const trimmedSecret = resolvedSecret.trim(); if (trimmedSecret.length === 0) { throw new StandardError(ErrorCode.VALIDATION_FAILED, 'JWT_SECRET cannot be empty or whitespace only.', { hint: 'Provide a strong secret key of at least 16 characters' }); } // Validate minimum length (prevent weak secrets - CVSS 7.5) if (trimmedSecret.length < 16) { throw new StandardError(ErrorCode.VALIDATION_FAILED, 'JWT_SECRET must be at least 16 characters long for security.', { providedLength: trimmedSecret.length, requiredLength: 16, hint: 'Use a strong random secret of at least 16 characters' }); } // Reject known insecure default secrets (CVSS 9.8 vulnerability) // Only reject if secret exactly matches known insecure defaults const normalizedSecret = trimmedSecret.toLowerCase().replace(/[_-]/g, ''); const isInsecure = AuthMiddleware.INSECURE_SECRETS.some((insecure)=>{ const normalizedInsecure = insecure.toLowerCase().replace(/[_-]/g, ''); // Only exact match - do not match if contains return normalizedSecret === normalizedInsecure; }); if (isInsecure) { throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Detected insecure default secret. Please use a strong, unique JWT_SECRET in production.', { securityRisk: 'CVSS 9.8 - Default secrets allow authentication bypass and token forgery', hint: 'Generate a secure random secret: openssl rand -base64 32' }); } this.jwtSecret = trimmedSecret; this.tokenExpirationSeconds = tokenExpirationSeconds; this.sessions = new Map(); logger.debug('AuthMiddleware initialized with secure JWT secret'); } /** * Generate a JWT token for a user * * @param userId - User ID * @param username - Username * @param role - User role * @param email - User email (optional) * @returns JWT token */ generateToken(userId, username, role, email) { const payload = { userId, username, role, email }; return jwt.sign(payload, this.jwtSecret, { algorithm: 'HS256', expiresIn: this.tokenExpirationSeconds }); } /** * Validate JWT token and extract user context * * @param token - JWT token * @returns User context if valid * @throws StandardError if token is invalid or expired */ validateToken(token) { try { if (!token || typeof token !== 'string') { throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Missing or invalid authentication token'); } // Remove "Bearer " prefix if present const cleanToken = token.startsWith('Bearer ') ? token.substring(7) : token; const decoded = jwt.verify(cleanToken, this.jwtSecret, { algorithms: [ 'HS256' ] }); // Validate required fields if (!decoded.userId || !decoded.username || !decoded.role) { throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Invalid token structure: missing required fields'); } // Validate role is one of the allowed roles if (!Object.values(UserRole).includes(decoded.role)) { throw new StandardError(ErrorCode.VALIDATION_FAILED, `Invalid role: ${decoded.role}`); } return { userId: decoded.userId, username: decoded.username, role: decoded.role, email: decoded.email, issuedAt: decoded.iat || Math.floor(Date.now() / 1000), expiresAt: decoded.exp || Math.floor(Date.now() / 1000) + this.tokenExpirationSeconds }; } catch (error) { if (error instanceof StandardError) { throw error; } if (error instanceof jwt.TokenExpiredError) { throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Authentication token has expired', { expiredAt: error.expiredAt?.toISOString() }, error); } if (error instanceof jwt.JsonWebTokenError) { throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Invalid authentication token', {}, error); } throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Token validation failed', {}, error); } } /** * Register a session (for session-based authentication fallback) * * @param sessionId - Session ID * @param userContext - User context */ registerSession(sessionId, userContext) { this.sessions.set(sessionId, { ...userContext, sessionId }); logger.debug('Session registered', { sessionId, userId: userContext.userId }); } /** * Validate session * * @param sessionId - Session ID * @returns User context if valid * @throws StandardError if session is invalid or expired */ validateSession(sessionId) { const session = this.sessions.get(sessionId); if (!session) { throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Invalid or expired session'); } // Check if session has expired if (session.expiresAt < Math.floor(Date.now() / 1000)) { this.sessions.delete(sessionId); throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Session has expired'); } return session; } /** * Invalidate a session * * @param sessionId - Session ID */ invalidateSession(sessionId) { this.sessions.delete(sessionId); logger.debug('Session invalidated', { sessionId }); } /** * Extract user context from Authorization header * * @param authHeader - Authorization header value * @returns User context * @throws StandardError if authorization header is invalid */ extractUserContext(authHeader, sessionId) { // Try JWT token first if (authHeader) { return this.validateToken(authHeader); } // Fallback to session if (sessionId) { return this.validateSession(sessionId); } throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Missing authentication credentials (JWT token or session required)'); } } /** * Role-Based Access Control (RBAC) enforcer */ export class RBACEnforcer { authMiddleware; constructor(authMiddleware){ this.authMiddleware = authMiddleware; } /** * Check if user has permission for an operation * * @param userContext - User context * @param operation - Operation to perform * @returns True if user has permission */ hasPermission(userContext, operation) { const allowedOperations = ROLE_PERMISSIONS[userContext.role]; return allowedOperations.includes(operation); } /** * Enforce permission check - throws if user lacks permission * * @param userContext - User context * @param operation - Operation to perform * @param skillId - Skill ID (for audit context) * @throws StandardError if user lacks permission */ enforcePermission(userContext, operation, skillId) { if (!this.hasPermission(userContext, operation)) { logger.warn('Authorization denied', { userId: userContext.userId, role: userContext.role, operation, skillId }); throw new StandardError(ErrorCode.VALIDATION_FAILED, `User does not have permission to perform operation: ${operation}`, { userId: userContext.userId, role: userContext.role, operation, skillId, allowedOperations: ROLE_PERMISSIONS[userContext.role] }); } logger.debug('Authorization granted', { userId: userContext.userId, role: userContext.role, operation, skillId }); } /** * Get description of allowed operations for a role * * @param role - User role * @returns List of allowed operations */ getAllowedOperations(role) { return ROLE_PERMISSIONS[role]; } } /** * Authorization decorator factory * Wrap promotion operations to enforce RBAC */ export function requirePermission(operation) { return function(target, propertyKey, descriptor) { const originalMethod = descriptor.value; descriptor.value = async function(...args) { // Extract userContext and rbac from 'this' context if (!this.userContext) { throw new StandardError(ErrorCode.VALIDATION_FAILED, 'User context not available - authentication required'); } if (!this.rbacEnforcer) { throw new StandardError(ErrorCode.VALIDATION_FAILED, 'RBAC enforcer not configured'); } const skillId = args[0]?.skillId || args[1]?.skillId; this.rbacEnforcer.enforcePermission(this.userContext, operation, skillId); return originalMethod.apply(this, args); }; return descriptor; }; } export default AuthMiddleware; //# sourceMappingURL=auth-middleware.js.map