UNPKG

codecrucible-synth

Version:

Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability

396 lines 14.7 kB
/** * Authentication Middleware for CLI Security * Integrates enterprise authentication with CLI request processing */ import { EnterpriseAuthManager, } from '../security/enterprise-auth-manager.js'; import { logger } from '../logger.js'; import { CLIError, CLIExitCode } from '../types.js'; import chalk from 'chalk'; export class AuthMiddleware { rbacSystem; secretsManager; authManager; config; isInitialized = false; constructor(rbacSystem, secretsManager, config = {}) { this.rbacSystem = rbacSystem; this.secretsManager = secretsManager; this.config = { enabled: process.env.NODE_ENV === 'production' || process.env.AUTH_ENABLED === 'true', requireAuth: process.env.REQUIRE_AUTH === 'true', allowedUnauthenticatedCommands: [ 'help', 'version', 'status', 'login', 'register', 'reset-password', ], tokenHeader: 'x-auth-token', apiKeyHeader: 'x-api-key', sessionTimeout: 30 * 60 * 1000, // 30 minutes ...config, }; this.authManager = new EnterpriseAuthManager(this.rbacSystem, this.secretsManager, { sessionTimeout: this.config.sessionTimeout, requireMFA: process.env.REQUIRE_MFA === 'true', }); } /** * Initialize authentication middleware */ async initialize() { try { if (this.config.enabled) { // Initialize secrets manager await this.secretsManager.initialize(); // Initialize RBAC system await this.rbacSystem.initialize(); // Create default admin user if none exists await this.ensureDefaultAdminUser(); logger.info('Authentication middleware initialized', { enabled: this.config.enabled, requireAuth: this.config.requireAuth, }); } else { logger.info('Authentication middleware disabled'); } this.isInitialized = true; } catch (error) { logger.error('Failed to initialize authentication middleware', error); throw error; } } /** * Authenticate CLI request */ async authenticateRequest(command, headers, ipAddress, interactive = false) { try { // If auth is disabled, allow all requests if (!this.config.enabled) { return { authenticated: true, authMethod: 'none', }; } // Check if command is allowed without authentication if (!this.config.requireAuth && this.isUnauthenticatedCommandAllowed(command)) { return { authenticated: true, authMethod: 'none', }; } // Try token authentication first if (headers && headers[this.config.tokenHeader]) { const tokenResult = await this.authenticateWithToken(headers[this.config.tokenHeader], ipAddress); if (tokenResult.authenticated) { return tokenResult; } } // Try API key authentication if (headers && headers[this.config.apiKeyHeader]) { const apiKeyResult = await this.authenticateWithAPIKey(headers[this.config.apiKeyHeader]); if (apiKeyResult.authenticated) { return apiKeyResult; } } // Interactive authentication for CLI if (interactive) { const interactiveResult = await this.authenticateInteractive(ipAddress); if (interactiveResult.authenticated) { return interactiveResult; } } // Authentication required but not provided throw new CLIError('Authentication required. Use --login or provide authentication headers.', CLIExitCode.AUTHENTICATION_REQUIRED); } catch (error) { if (error instanceof CLIError) { throw error; } logger.error('Authentication error', error); throw new CLIError('Authentication system error', CLIExitCode.AUTHENTICATION_FAILED); } } /** * Authenticate with JWT token */ async authenticateWithToken(token, ipAddress) { try { const validation = await this.authManager.validateToken(token, ipAddress); if (!validation.valid) { return { authenticated: false, authMethod: 'token', }; } return { authenticated: true, authMethod: 'token', userId: validation.user?.id, username: validation.user?.username, permissions: validation.permissions, sessionId: validation.session?.id, }; } catch (error) { logger.debug('Token authentication failed', { error: error.message }); return { authenticated: false, authMethod: 'token', }; } } /** * Authenticate with API key */ async authenticateWithAPIKey(apiKey) { try { const validation = await this.authManager.validateAPIKey(apiKey); if (!validation.valid) { return { authenticated: false, authMethod: 'apikey', }; } return { authenticated: true, authMethod: 'apikey', permissions: validation.apiKey?.permissions, userId: 'api-user', // API keys don't have specific users username: validation.apiKey?.name, }; } catch (error) { logger.debug('API key authentication failed', { error: error.message }); return { authenticated: false, authMethod: 'apikey', }; } } /** * Interactive authentication for CLI */ async authenticateInteractive(ipAddress) { try { const inquirer = await import('inquirer'); console.log(chalk.blue('\n🔐 Authentication Required\n')); const credentials = await inquirer.default.prompt([ { type: 'input', name: 'username', message: 'Username:', validate: (input) => input.length > 0 || 'Username is required', }, { type: 'password', name: 'password', message: 'Password:', mask: '*', validate: (input) => input.length > 0 || 'Password is required', }, ]); const authRequest = { username: credentials.username, password: credentials.password, ipAddress, userAgent: 'CodeCrucible-CLI', }; const authResult = await this.authManager.authenticate(authRequest); if (!authResult.success) { console.log(chalk.red(`\n❌ Authentication failed: ${authResult.error}\n`)); return { authenticated: false, authMethod: 'interactive', }; } // Handle MFA if required if (authResult.requiresMFA) { const mfaPrompt = await inquirer.default.prompt([ { type: 'input', name: 'mfaCode', message: 'MFA Code (6 digits):', validate: (input) => /^\d{6}$/.test(input) || 'MFA code must be 6 digits', }, ]); const mfaAuthRequest = { ...authRequest, mfaCode: mfaPrompt.mfaCode, }; const mfaResult = await this.authManager.authenticate(mfaAuthRequest); if (!mfaResult.success) { console.log(chalk.red(`\n❌ MFA authentication failed: ${mfaResult.error}\n`)); return { authenticated: false, authMethod: 'interactive', }; } // Use MFA result authResult.user = mfaResult.user; authResult.session = mfaResult.session; authResult.accessToken = mfaResult.accessToken; } console.log(chalk.green(`\n✅ Welcome, ${authResult.user?.username}!\n`)); // Store session for subsequent requests if (authResult.accessToken) { await this.storeSessionToken(authResult.accessToken); } return { authenticated: true, authMethod: 'interactive', userId: authResult.user?.id, username: authResult.user?.username, permissions: authResult.session?.permissions, sessionId: authResult.session?.id, }; } catch (error) { logger.error('Interactive authentication failed', error); return { authenticated: false, authMethod: 'interactive', }; } } /** * Check if command is allowed without authentication */ isUnauthenticatedCommandAllowed(command) { return this.config.allowedUnauthenticatedCommands.includes(command.toLowerCase()); } /** * Store session token for subsequent requests */ async storeSessionToken(token) { try { // Store in a secure location (encrypted file or environment) const tokenPath = process.env.HOME || process.env.USERPROFILE; if (tokenPath) { const fs = await import('fs/promises'); const path = await import('path'); const tokenFile = path.join(tokenPath, '.codecrucible-session'); await fs.writeFile(tokenFile, token, { mode: 0o600 }); logger.debug('Session token stored'); } } catch (error) { logger.warn('Failed to store session token', { error: error.message }); } } /** * Load stored session token */ async loadStoredToken() { try { const tokenPath = process.env.HOME || process.env.USERPROFILE; if (!tokenPath) return null; const fs = await import('fs/promises'); const path = await import('path'); const tokenFile = path.join(tokenPath, '.codecrucible-session'); const token = await fs.readFile(tokenFile, 'utf8'); return token.trim(); } catch { return null; } } /** * Validate permission for specific operation */ async validatePermission(auth, operation, resource) { try { if (!this.config.enabled || !auth.authenticated) { return !this.config.requireAuth; } if (!auth.userId || !auth.permissions) { return false; } // Check RBAC permissions const permissionId = resource ? `${operation}:${resource}` : operation; return this.rbacSystem.hasPermission(auth.userId, permissionId); } catch (error) { logger.error('Permission validation error', error); return false; } } /** * Logout current session */ async logout(sessionId) { try { if (sessionId) { await this.authManager.logout(sessionId); } // Remove stored token const tokenPath = process.env.HOME || process.env.USERPROFILE; if (tokenPath) { const fs = await import('fs/promises'); const path = await import('path'); const tokenFile = path.join(tokenPath, '.codecrucible-session'); await fs.unlink(tokenFile).catch(() => { // Ignore if file doesn't exist }); } logger.info('User logged out'); } catch (error) { logger.error('Logout error', error); } } /** * Ensure default admin user exists */ async ensureDefaultAdminUser() { try { const users = await this.rbacSystem.getUsers(); const adminExists = users.some(user => user.roles.includes('admin') || user.username === 'admin'); if (!adminExists) { const defaultPassword = process.env.DEFAULT_ADMIN_PASSWORD || 'Admin123!@#'; const adminUser = await this.rbacSystem.createUser({ username: 'admin', email: 'admin@codecrucible.local', password: defaultPassword, }); // Assign admin roles separately await this.rbacSystem.assignRoleToUser(adminUser.id, 'admin'); await this.rbacSystem.assignRoleToUser(adminUser.id, 'developer'); logger.warn('Default admin user created', { username: 'admin', message: 'Please change the default password immediately', }); if (!process.env.DEFAULT_ADMIN_PASSWORD) { console.log(chalk.yellow('\n⚠️ Default admin user created with password: Admin123!@#')); console.log(chalk.yellow(' Please change this password immediately for security!\n')); } } } catch (error) { logger.error('Failed to ensure default admin user', error); } } /** * Get authentication statistics */ getAuthStats() { return this.authManager.getAuthStats(); } /** * Check if authentication is enabled */ isAuthEnabled() { return this.config.enabled; } /** * Check if authentication is required */ isAuthRequired() { return this.config.requireAuth; } } //# sourceMappingURL=auth-middleware.js.map