codecrucible-synth
Version:
Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability
851 lines (741 loc) • 22.6 kB
text/typescript
/**
* Enterprise Authentication Manager
* Handles authentication, session management, and token-based access control
*/
import * as jwt from 'jsonwebtoken';
import crypto from 'crypto';
import { RBACSystem, Session, User } from './production-rbac-system.js';
import { SecretsManager } from './secrets-manager.js';
import { InputSanitizer } from '../security/input-sanitizer.js';
import { logger } from '../logger.js';
export interface AuthConfig {
jwtSecret: string;
jwtExpiresIn: string;
refreshTokenExpiresIn: string;
sessionTimeout: number;
maxConcurrentSessions: number;
enforceIpBinding: boolean;
requireMFA: boolean;
passwordPolicy: {
minLength: number;
requireUppercase: boolean;
requireLowercase: boolean;
requireNumbers: boolean;
requireSymbols: boolean;
preventReuse: number;
};
rateLimiting: {
maxAttempts: number;
windowMs: number;
blockDuration: number;
};
}
export interface AuthRequest {
username: string;
password: string;
mfaCode?: string;
ipAddress?: string;
userAgent?: string;
deviceFingerprint?: string;
}
export interface AuthResult {
success: boolean;
user?: User;
session?: Session;
accessToken?: string;
refreshToken?: string;
expiresAt?: Date;
error?: string;
requiresMFA?: boolean;
}
export interface TokenValidationResult {
valid: boolean;
user?: User;
session?: Session;
permissions?: string[];
error?: string;
}
export interface APIKeyConfig {
id: string;
name: string;
key: string;
permissions: string[];
expiresAt?: Date;
lastUsed?: Date;
usageCount: number;
rateLimit?: {
maxRequests: number;
windowMs: number;
};
}
export class EnterpriseAuthManager {
private config: AuthConfig;
private rbac: RBACSystem;
private secretsManager: SecretsManager;
private rateLimitTracker = new Map<string, { count: number; resetTime: number }>();
private apiKeys = new Map<string, APIKeyConfig>();
private activeSessions = new Map<string, Session>();
constructor(rbac: RBACSystem, secretsManager: SecretsManager, config: Partial<AuthConfig> = {}) {
this.rbac = rbac;
this.secretsManager = secretsManager;
this.config = {
jwtSecret: process.env.JWT_SECRET || crypto.randomBytes(64).toString('hex'),
jwtExpiresIn: '1h',
refreshTokenExpiresIn: '7d',
sessionTimeout: 30 * 60 * 1000, // 30 minutes
maxConcurrentSessions: 5,
enforceIpBinding: true,
requireMFA: false,
passwordPolicy: {
minLength: 12,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSymbols: true,
preventReuse: 5,
},
rateLimiting: {
maxAttempts: 5,
windowMs: 15 * 60 * 1000, // 15 minutes
blockDuration: 30 * 60 * 1000, // 30 minutes
},
...config,
};
}
/**
* Authenticate user with comprehensive security checks
*/
async authenticate(request: AuthRequest): Promise<AuthResult> {
try {
// Input validation
const usernameValidation = InputSanitizer.sanitizePrompt(request.username);
if (!usernameValidation.isValid) {
return {
success: false,
error: 'Invalid username format',
};
}
// Rate limiting check
const rateLimitKey = request.ipAddress || 'unknown';
if (this.isRateLimited(rateLimitKey)) {
logger.warn('Authentication rate limited', {
ipAddress: request.ipAddress,
username: request.username,
});
return {
success: false,
error: 'Too many authentication attempts. Please try again later.',
};
}
// Attempt authentication via RBAC
const authResult = await this.rbac.authenticate(request.username, request.password, {
ipAddress: request.ipAddress,
userAgent: request.userAgent,
});
if (!authResult) {
this.recordFailedAttempt(rateLimitKey);
return {
success: false,
error: 'Invalid credentials',
};
}
const { user } = authResult;
const session = this.createSession(user, request.ipAddress);
// MFA check if required
if (this.config.requireMFA && !request.mfaCode) {
return {
success: false,
requiresMFA: true,
error: 'Multi-factor authentication required',
};
}
if (this.config.requireMFA && request.mfaCode) {
const mfaValid = await this.validateMFA(user?.id!, request.mfaCode);
if (!mfaValid) {
this.recordFailedAttempt(rateLimitKey);
return {
success: false,
error: 'Invalid MFA code',
};
}
}
// Session management
await this.manageUserSessions(user?.id!, session.id);
// Generate JWT tokens
const accessToken = this.generateAccessToken(user!, session);
const refreshToken = this.generateRefreshToken(session);
// Store session
this.activeSessions.set(session.id, session);
// Reset rate limiting on successful auth
this.rateLimitTracker.delete(rateLimitKey);
logger.info('User authenticated successfully', {
userId: user?.id!,
username: user?.username!,
sessionId: session.id,
ipAddress: request.ipAddress,
});
return {
success: true,
user,
session,
accessToken,
refreshToken,
expiresAt: session.expiresAt,
};
} catch (error) {
logger.error('Authentication error', error as Error, {
username: request.username,
ipAddress: request.ipAddress,
});
return {
success: false,
error: 'Authentication system error',
};
}
}
/**
* Validate JWT access token
*/
async validateToken(token: string, ipAddress?: string): Promise<TokenValidationResult> {
try {
// Verify JWT signature and expiration
const decoded = jwt.verify(token, this.config.jwtSecret) as any;
// Get session info
const session = this.activeSessions.get(decoded.sessionId);
if (!session || session.expiresAt < new Date()) {
return {
valid: false,
error: 'Session expired or invalid',
};
}
// IP binding check
if (this.config.enforceIpBinding && ipAddress && session.ipAddress !== ipAddress) {
logger.warn('IP binding violation detected', {
sessionId: session.id,
expectedIp: session.ipAddress,
actualIp: ipAddress,
});
return {
valid: false,
error: 'IP address mismatch',
};
}
// Get user info
const users = this.rbac.getUsers();
const user = (await users).find(u => u.id === session.userId);
if (!user || user?.status !== 'active') {
return {
valid: false,
error: 'User not found or inactive',
};
}
// Update session activity
session.lastActivity = new Date();
return {
valid: true,
user: user!,
session,
permissions: session.permissions,
};
} catch (error) {
logger.debug('Token validation failed', { error: (error as Error).message });
return {
valid: false,
error: 'Invalid token',
};
}
}
/**
* Refresh access token using refresh token
*/
async refreshToken(refreshToken: string): Promise<AuthResult> {
try {
const decoded = jwt.verify(refreshToken, this.config.jwtSecret) as any;
const session = this.activeSessions.get(decoded.sessionId);
if (!session || session.refreshToken !== refreshToken) {
return {
success: false,
error: 'Invalid refresh token',
};
}
const users = this.rbac.getUsers();
const user = (await users).find(u => u.id === session.userId);
if (!user || user?.status !== 'active') {
return {
success: false,
error: 'User not found or inactive',
};
}
// Generate new access token
const newAccessToken = this.generateAccessToken(user!, session);
// Optionally rotate refresh token
const newRefreshToken = this.generateRefreshToken(session);
session.refreshToken = newRefreshToken;
return {
success: true,
user: user!,
session,
accessToken: newAccessToken,
refreshToken: newRefreshToken,
expiresAt: session.expiresAt,
};
} catch (error) {
logger.debug('Refresh token validation failed', { error: (error as Error).message });
return {
success: false,
error: 'Invalid refresh token',
};
}
}
/**
* Logout user session
*/
async logout(sessionId: string): Promise<boolean> {
try {
const session = this.activeSessions.get(sessionId);
if (!session) {
return false;
}
// Remove from active sessions
this.activeSessions.delete(sessionId);
// Revoke in RBAC system
await this.rbac.revokeSession(sessionId);
logger.info('User logged out', {
sessionId,
userId: session.userId,
});
return true;
} catch (error) {
logger.error('Logout error', error as Error, { sessionId });
return false;
}
}
/**
* Create API key for programmatic access
*/
async createAPIKey(
config: Omit<APIKeyConfig, 'id' | 'key' | 'lastUsed' | 'usageCount'>
): Promise<{
id: string;
key: string;
}> {
try {
const id = crypto.randomBytes(16).toString('hex');
const key = `cc_${crypto.randomBytes(32).toString('hex')}`;
const hashedKey = crypto.createHash('sha256').update(key).digest('hex');
const apiKeyConfig: APIKeyConfig = {
id,
key: hashedKey, // Store hashed version
usageCount: 0,
...config,
};
this.apiKeys.set(id, apiKeyConfig);
// Store in secrets manager
await this.secretsManager.storeSecret(`apikey_${id}`, JSON.stringify(apiKeyConfig), {
description: `API Key: ${config.name}`,
tags: ['apikey', 'auth'],
expiresAt: config.expiresAt,
});
logger.info('API key created', {
id,
name: config.name,
permissions: config.permissions.length,
});
return { id, key }; // Return unhashed key to user
} catch (error) {
logger.error('Failed to create API key', error as Error);
throw error;
}
}
/**
* Validate API key
*/
async validateAPIKey(providedKey: string): Promise<{
valid: boolean;
apiKey?: APIKeyConfig;
error?: string;
}> {
try {
const hashedProvidedKey = crypto.createHash('sha256').update(providedKey).digest('hex');
for (const [id, apiKey] of this.apiKeys.entries()) {
if (apiKey.key === hashedProvidedKey) {
// Check expiration
if (apiKey.expiresAt && apiKey.expiresAt < new Date()) {
return {
valid: false,
error: 'API key expired',
};
}
// Check rate limiting
if (apiKey.rateLimit) {
const rateLimitKey = `apikey_${id}`;
if (this.isAPIKeyRateLimited(rateLimitKey, apiKey.rateLimit)) {
return {
valid: false,
error: 'API key rate limit exceeded',
};
}
this.recordAPIKeyUsage(rateLimitKey);
}
// Update usage statistics
apiKey.lastUsed = new Date();
apiKey.usageCount++;
return {
valid: true,
apiKey,
};
}
}
return {
valid: false,
error: 'Invalid API key',
};
} catch (error) {
logger.error('API key validation error', error as Error);
return {
valid: false,
error: 'API key validation failed',
};
}
}
/**
* Revoke API key
*/
async revokeAPIKey(keyId: string): Promise<boolean> {
try {
const apiKey = this.apiKeys.get(keyId);
if (!apiKey) {
return false;
}
this.apiKeys.delete(keyId);
await this.secretsManager.deleteSecret(`apikey_${keyId}`);
logger.info('API key revoked', {
id: keyId,
name: apiKey.name,
});
return true;
} catch (error) {
logger.error('Failed to revoke API key', error as Error, { keyId });
return false;
}
}
/**
* Password validation against policy
*/
validatePassword(
password: string,
username?: string
): {
valid: boolean;
errors: string[];
} {
const errors: string[] = [];
const policy = this.config.passwordPolicy;
if (password.length < policy.minLength) {
errors.push(`Password must be at least ${policy.minLength} characters long`);
}
if (policy.requireUppercase && !/[A-Z]/.test(password)) {
errors.push('Password must contain at least one uppercase letter');
}
if (policy.requireLowercase && !/[a-z]/.test(password)) {
errors.push('Password must contain at least one lowercase letter');
}
if (policy.requireNumbers && !/\d/.test(password)) {
errors.push('Password must contain at least one number');
}
if (policy.requireSymbols && !/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password)) {
errors.push('Password must contain at least one symbol');
}
if (username && password.toLowerCase().includes(username.toLowerCase())) {
errors.push('Password cannot contain username');
}
// Check for common weak passwords
const commonPasswords = ['password', '123456', 'admin', 'root', 'letmein'];
if (commonPasswords.includes(password.toLowerCase())) {
errors.push('Password is too common');
}
return {
valid: errors.length === 0,
errors,
};
}
/**
* Generate access token
*/
private generateAccessToken(user: User, session: Session): string {
try {
const payload = {
userId: user?.id!,
username: user?.username!,
sessionId: session.id,
permissions: session.permissions,
roles: session.roles,
type: 'access',
};
const options = {
expiresIn: this.config.jwtExpiresIn as any,
issuer: 'codecrucible-auth',
audience: 'codecrucible-api',
};
return jwt.sign(payload, this.config.jwtSecret as string, options as jwt.SignOptions);
} catch (error) {
logger.error('Failed to generate access token', error as Error);
throw error;
}
}
/**
* Generate refresh token
*/
private generateRefreshToken(session: Session): string {
try {
const payload = {
sessionId: session.id,
type: 'refresh',
};
const options = {
expiresIn: this.config.refreshTokenExpiresIn as any,
issuer: 'codecrucible-auth',
audience: 'codecrucible-api',
};
return jwt.sign(payload, this.config.jwtSecret as string, options as jwt.SignOptions);
} catch (error) {
logger.error('Failed to generate refresh token', error as Error);
throw error;
}
}
/**
* Validate MFA code (placeholder implementation)
*/
private async validateMFA(userId: string, mfaCode: string): Promise<boolean> {
// This is a placeholder - in a real implementation, you would:
// 1. Get user's MFA secret from secure storage
// 2. Validate TOTP code using a library like 'otplib'
// 3. Check for replay attacks
// For now, accept any 6-digit code
return /^\d{6}$/.test(mfaCode);
}
/**
* Manage concurrent sessions per user
*/
private async manageUserSessions(userId: string, currentSessionId: string): Promise<void> {
const userSessions = Array.from(this.activeSessions.values())
.filter(session => session.userId === userId && session.id !== currentSessionId)
.sort((a, b) => b.lastActivity.getTime() - a.lastActivity.getTime());
// Remove oldest sessions if limit exceeded
if (userSessions.length >= this.config.maxConcurrentSessions) {
const sessionsToRemove = userSessions.slice(this.config.maxConcurrentSessions - 1);
for (const session of sessionsToRemove) {
this.activeSessions.delete(session.id);
await this.rbac.revokeSession(session.id);
}
logger.info('Concurrent session limit enforced', {
userId,
removedSessions: sessionsToRemove.length,
});
}
}
/**
* Rate limiting methods
*/
private isRateLimited(key: string): boolean {
const now = Date.now();
const tracker = this.rateLimitTracker.get(key);
if (!tracker) {
return false;
}
if (now > tracker.resetTime) {
this.rateLimitTracker.delete(key);
return false;
}
return tracker.count >= this.config.rateLimiting.maxAttempts;
}
private recordFailedAttempt(key: string): void {
const now = Date.now();
const tracker = this.rateLimitTracker.get(key);
if (!tracker || now > tracker.resetTime) {
this.rateLimitTracker.set(key, {
count: 1,
resetTime: now + this.config.rateLimiting.windowMs,
});
} else {
tracker.count++;
}
}
private isAPIKeyRateLimited(
key: string,
rateLimit: { maxRequests: number; windowMs: number }
): boolean {
const tracker = this.rateLimitTracker.get(key);
const now = Date.now();
if (!tracker) {
return false;
}
if (now > tracker.resetTime) {
this.rateLimitTracker.delete(key);
return false;
}
return tracker.count >= rateLimit.maxRequests;
}
private recordAPIKeyUsage(key: string): void {
const now = Date.now();
const tracker = this.rateLimitTracker.get(key);
if (!tracker || now > tracker.resetTime) {
this.rateLimitTracker.set(key, {
count: 1,
resetTime: now + 15 * 60 * 1000, // Default 15 minutes
});
} else {
tracker.count++;
}
}
/**
* Cleanup expired sessions
*/
async cleanupExpiredSessions(): Promise<number> {
let cleaned = 0;
const now = new Date();
for (const [sessionId, session] of this.activeSessions.entries()) {
if (session.expiresAt < now) {
this.activeSessions.delete(sessionId);
await this.rbac.revokeSession(sessionId);
cleaned++;
}
}
if (cleaned > 0) {
logger.info('Expired sessions cleaned up', { count: cleaned });
}
return cleaned;
}
/**
* Get authentication statistics
*/
async getAuthStats(): Promise<{
activeSessions: number;
activeAPIKeys: number;
rateLimitedIPs: number;
totalUsers: number;
}> {
return {
activeSessions: this.activeSessions.size,
activeAPIKeys: this.apiKeys.size,
rateLimitedIPs: this.rateLimitTracker.size,
totalUsers: (await this.rbac.getUsers()).length,
};
}
/**
* Create a new session for authenticated user
*/
private createSession(user: User | undefined, ipAddress?: string): Session {
const sessionId = crypto.randomBytes(32).toString('hex');
const now = new Date();
const expiresAt = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 24 hours
return {
id: sessionId,
userId: user?.id || '',
ipAddress: ipAddress || '',
createdAt: now,
lastActivity: now,
expiresAt,
isActive: true,
permissions: [],
roles: [],
refreshToken: crypto.randomBytes(32).toString('hex'),
};
}
/**
* Public method to check rate limit for testing
*/
checkRateLimit(ip: string): boolean {
const isLimited = this.isRateLimited(ip);
if (!isLimited) {
// Record this as a failed attempt to trigger rate limiting
this.recordFailedAttempt(ip);
}
return !isLimited;
}
/**
* Get rate limit retry after time in seconds
*/
getRateLimitRetryAfter(ip: string): number {
const tracker = this.rateLimitTracker.get(ip);
if (!tracker) return 0;
const now = Date.now();
return Math.max(0, Math.ceil((tracker.resetTime - now) / 1000));
}
/**
* Generate JWT token (public method for testing)
*/
generateJWT(userId: string, payload: any): string {
return jwt.sign(
{
userId,
...payload,
type: 'access',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600, // 1 hour
},
this.config.jwtSecret,
{
issuer: 'codecrucible-auth',
audience: 'codecrucible-api',
}
);
}
/**
* Validate JWT token (public method for testing)
*/
validateJWT(token: string): { success: boolean; payload?: any; error?: string } {
try {
const decoded = jwt.verify(token, this.config.jwtSecret);
return {
success: true,
payload: decoded,
};
} catch (error) {
return {
success: false,
error: (error as Error).message,
};
}
}
/**
* Hash password using bcrypt-like method
*/
async hashPassword(password: string): Promise<string> {
// Using Node.js crypto for consistent hashing (in production, use bcrypt)
const salt = crypto.randomBytes(16).toString('hex');
const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
return `${salt}:${hash}`;
}
/**
* Verify password against hash
*/
async verifyPassword(password: string, hash: string): Promise<boolean> {
const [salt, storedHash] = hash.split(':');
const computedHash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
// Use timing-safe comparison to prevent timing attacks
return this.timingSafeEqual(computedHash, storedHash);
}
/**
* Validate password against policy (throws on failure)
*/
validatePasswordPolicy(password: string, username?: string): void {
const validation = this.validatePassword(password, username);
if (!validation.valid) {
throw new Error(`Password policy violation: ${validation.errors.join(', ')}`);
}
}
/**
* Timing-safe string comparison to prevent timing attacks
*/
private timingSafeEqual(a: string, b: string): boolean {
if (a.length !== b.length) {
return false;
}
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}
}