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
JavaScript
/**
* 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