UNPKG

bigbasealpha

Version:

Enterprise-Grade NoSQL Database System with Modular Logger & Offline HSM Security - Complete database platform with professional text-based logging, encryption, caching, indexing, JWT authentication, auto-generated REST API, real-time dashboard, and maste

847 lines (722 loc) โ€ข 28.5 kB
import { EventEmitter } from 'events'; import crypto from 'crypto'; import bcrypt from 'bcrypt'; /** * BigBaseAlpha JWT Authentication & User Management System * Enterprise-grade authentication with role-based access control * * @copyright 2025 ByAlphas. All rights reserved. */ export class AuthenticationManager extends EventEmitter { constructor(database, options = {}) { super(); this.database = database; // Logger support for v1.5.1 this.logger = database.logger || { success: (msg) => console.log(`[SUCCESS] [AUTH] ${msg}`), info: (msg) => console.log(`[INFO] [AUTH] ${msg}`), warn: (msg) => console.log(`[WARN] [AUTH] ${msg}`), error: (msg) => console.log(`[ERROR] [AUTH] ${msg}`), process: (msg) => console.log(`[PROCESS] [AUTH] ${msg}`) }; this.options = { jwtSecret: options.jwtSecret || this._generateSecret(), tokenExpiry: options.tokenExpiry || '24h', refreshTokenExpiry: options.refreshTokenExpiry || '7d', saltRounds: options.saltRounds || 12, maxLoginAttempts: options.maxLoginAttempts || 5, lockoutTime: options.lockoutTime || 15 * 60 * 1000, // 15 minutes requireEmailVerification: options.requireEmailVerification || false, ...options }; // Session storage this.activeSessions = new Map(); this.refreshTokens = new Map(); this.loginAttempts = new Map(); // Built-in roles this.defaultRoles = { admin: { name: 'admin', permissions: ['*'], // All permissions description: 'Full system access' }, user: { name: 'user', permissions: ['read', 'write:own'], description: 'Standard user access' }, readonly: { name: 'readonly', permissions: ['read'], description: 'Read-only access' }, api: { name: 'api', permissions: ['api:read', 'api:write'], description: 'API access only' } }; this._initializeCollections(); } /** * Initialize authentication collections */ async _initializeCollections() { try { // Create users collection this.usersCollection = this.database.collection('_auth_users'); // Create indexes with error handling try { await this.usersCollection.createIndex({ username: 1 }, { unique: true }); } catch (error) { if (!error.message.includes('already exists')) throw error; } try { await this.usersCollection.createIndex({ email: 1 }, { unique: true }); } catch (error) { if (!error.message.includes('already exists')) throw error; } // Create roles collection this.rolesCollection = this.database.collection('_auth_roles'); try { await this.rolesCollection.createIndex({ name: 1 }, { unique: true }); } catch (error) { if (!error.message.includes('already exists')) throw error; } // Create API keys collection this.apiKeysCollection = this.database.collection('_auth_api_keys'); try { await this.apiKeysCollection.createIndex({ key: 1 }, { unique: true }); } catch (error) { if (!error.message.includes('already exists')) throw error; } // Create audit log collection this.auditCollection = this.database.collection('_auth_audit'); try { await this.auditCollection.createIndex({ timestamp: -1 }); } catch (error) { if (!error.message.includes('already exists')) throw error; } try { await this.auditCollection.createIndex({ userId: 1 }); } catch (error) { if (!error.message.includes('already exists')) throw error; } // Initialize default roles await this._initializeDefaultRoles(); this.logger.success('Authentication system initialized'); this.emit('authInitialized'); } catch (error) { console.error('Failed to initialize authentication:', error); throw error; } } /** * Initialize default roles */ async _initializeDefaultRoles() { for (const [roleKey, roleData] of Object.entries(this.defaultRoles)) { const existingRole = await this.rolesCollection.findOne({ name: roleData.name }); if (!existingRole) { await this.rolesCollection.insertOne({ ...roleData, createdAt: new Date(), system: true }); } } } /** * Create a new user */ async createUser(userData) { const { username, password, email, role = 'user', profile = {}, permissions = [] } = userData; // Validation if (!username || !password) { throw new Error('Username and password are required'); } if (username.length < 3) { throw new Error('Username must be at least 3 characters'); } if (password.length < 6) { throw new Error('Password must be at least 6 characters'); } // Check if user exists const existingUser = await this.usersCollection.findOne({ $or: [{ username }, { email }] }); if (existingUser) { throw new Error('User with this username or email already exists'); } // Validate role const roleData = await this.rolesCollection.findOne({ name: role }); if (!roleData) { throw new Error(`Role '${role}' does not exist`); } // Hash password const hashedPassword = await bcrypt.hash(password, this.options.saltRounds); // Create user const user = { _id: this._generateUserId(), username, email, password: hashedPassword, role, permissions: [...roleData.permissions, ...permissions], profile: { firstName: profile.firstName || '', lastName: profile.lastName || '', avatar: profile.avatar || '', ...profile }, status: 'active', emailVerified: !this.options.requireEmailVerification, createdAt: new Date(), updatedAt: new Date(), lastLogin: null, loginCount: 0, twoFactorEnabled: false, metadata: { ipAddresses: [], userAgents: [], devices: [] } }; const result = await this.usersCollection.insertOne(user); // Log creation await this._logAuditEvent('user_created', null, { targetUserId: user._id, username: user.username, role: user.role }); // Remove password from response const { password: _, ...userResponse } = user; this.emit('userCreated', { user: userResponse }); console.log(`๐Ÿ‘ค User created: ${username} (${role})`); return { success: true, user: userResponse }; } /** * Authenticate user and generate tokens */ async login(credentials, metadata = {}) { const { username, password, rememberMe = false } = credentials; const { ipAddress, userAgent, deviceInfo } = metadata; // Check login attempts const attemptKey = `${username}:${ipAddress || 'unknown'}`; const attempts = this.loginAttempts.get(attemptKey) || { count: 0, lockedUntil: null }; if (attempts.lockedUntil && attempts.lockedUntil > Date.now()) { const remainingTime = Math.ceil((attempts.lockedUntil - Date.now()) / 1000 / 60); throw new Error(`Account locked. Try again in ${remainingTime} minutes`); } // Find user const user = await this.usersCollection.findOne({ $or: [{ username }, { email: username }] }); if (!user) { await this._incrementLoginAttempts(attemptKey); throw new Error('Invalid credentials'); } if (user.status !== 'active') { throw new Error('Account is disabled'); } // Verify password const isValidPassword = await bcrypt.compare(password, user.password); if (!isValidPassword) { await this._incrementLoginAttempts(attemptKey); await this._logAuditEvent('login_failed', user._id, { reason: 'invalid_password', ipAddress, userAgent }); throw new Error('Invalid credentials'); } // Clear login attempts on successful login this.loginAttempts.delete(attemptKey); // Generate tokens const sessionId = this._generateSessionId(); const accessToken = this._generateJWT(user, sessionId, false); const refreshToken = rememberMe ? this._generateJWT(user, sessionId, true) : null; // Create session const session = { sessionId, userId: user._id, username: user.username, role: user.role, permissions: user.permissions, accessToken, refreshToken, createdAt: new Date(), expiresAt: new Date(Date.now() + this._parseTimeToMs(this.options.tokenExpiry)), ipAddress, userAgent, deviceInfo, active: true }; this.activeSessions.set(sessionId, session); if (refreshToken) { this.refreshTokens.set(refreshToken, sessionId); } // Update user login info await this.usersCollection.updateOne( { _id: user._id }, { $set: { lastLogin: new Date(), updatedAt: new Date() }, $inc: { loginCount: 1 }, $push: { 'metadata.ipAddresses': { $each: [ipAddress], $slice: -10 }, 'metadata.userAgents': { $each: [userAgent], $slice: -5 } } } ); // Log successful login await this._logAuditEvent('login_success', user._id, { sessionId, ipAddress, userAgent, deviceInfo }); // Remove sensitive data const { password: _, ...userResponse } = user; this.emit('userLoggedIn', { user: userResponse, session }); this.logger.success(`User logged in: ${user.username}`); return { success: true, user: userResponse, tokens: { accessToken, refreshToken, expiresAt: session.expiresAt }, session: { sessionId, createdAt: session.createdAt, expiresAt: session.expiresAt } }; } /** * Logout user and invalidate session */ async logout(token) { try { const payload = this._verifyJWT(token); const session = this.activeSessions.get(payload.sessionId); if (session) { // Remove session this.activeSessions.delete(payload.sessionId); // Remove refresh token if exists if (session.refreshToken) { this.refreshTokens.delete(session.refreshToken); } // Log logout await this._logAuditEvent('logout', payload.userId, { sessionId: payload.sessionId }); this.emit('userLoggedOut', { userId: payload.userId, sessionId: payload.sessionId }); console.log(`๐Ÿšช User logged out: ${session.username}`); return { success: true, message: 'Logged out successfully' }; } return { success: false, message: 'Session not found' }; } catch (error) { throw new Error('Invalid token'); } } /** * Refresh access token */ async refreshToken(refreshToken) { try { const sessionId = this.refreshTokens.get(refreshToken); if (!sessionId) { throw new Error('Invalid refresh token'); } const session = this.activeSessions.get(sessionId); if (!session || !session.active) { throw new Error('Session expired or invalid'); } // Get current user data const user = await this.usersCollection.findOne({ _id: session.userId }); if (!user || user.status !== 'active') { throw new Error('User account is disabled'); } // Generate new access token const newAccessToken = this._generateJWT(user, sessionId, false); // Update session session.accessToken = newAccessToken; session.expiresAt = new Date(Date.now() + this._parseTimeToMs(this.options.tokenExpiry)); this.activeSessions.set(sessionId, session); return { success: true, tokens: { accessToken: newAccessToken, refreshToken, expiresAt: session.expiresAt } }; } catch (error) { throw new Error('Token refresh failed: ' + error.message); } } /** * Verify JWT token and return user info */ async verifyToken(token) { try { const payload = this._verifyJWT(token); const session = this.activeSessions.get(payload.sessionId); if (!session || !session.active) { throw new Error('Session expired or invalid'); } if (session.expiresAt < new Date()) { this.activeSessions.delete(payload.sessionId); throw new Error('Token expired'); } // Get current user data const user = await this.usersCollection.findOne({ _id: payload.userId }); if (!user || user.status !== 'active') { throw new Error('User account is disabled'); } return { valid: true, user: { _id: user._id, username: user.username, email: user.email, role: user.role, permissions: user.permissions, profile: user.profile }, session: { sessionId: session.sessionId, createdAt: session.createdAt, expiresAt: session.expiresAt } }; } catch (error) { return { valid: false, error: error.message }; } } /** * Generate API key for user */ async generateAPIKey(userId, options = {}) { const { name = 'Default API Key', permissions = [], expiresIn = null, rateLimitRpm = 1000 } = options; const user = await this.usersCollection.findOne({ _id: userId }); if (!user) { throw new Error('User not found'); } const apiKey = { _id: this._generateAPIKeyId(), key: this._generateAPIKeyToken(), userId, name, permissions: permissions.length > 0 ? permissions : user.permissions, rateLimitRpm, createdAt: new Date(), expiresAt: expiresIn ? new Date(Date.now() + this._parseTimeToMs(expiresIn)) : null, lastUsed: null, usageCount: 0, active: true }; await this.apiKeysCollection.insertOne(apiKey); await this._logAuditEvent('api_key_created', userId, { apiKeyId: apiKey._id, name }); this.logger.success(`API key generated: ${name} for ${user.username}`); return { success: true, apiKey: { id: apiKey._id, key: apiKey.key, name: apiKey.name, permissions: apiKey.permissions, createdAt: apiKey.createdAt, expiresAt: apiKey.expiresAt } }; } /** * Verify API key */ async verifyAPIKey(keyString) { const apiKey = await this.apiKeysCollection.findOne({ key: keyString, active: true }); if (!apiKey) { return { valid: false, error: 'Invalid API key' }; } if (apiKey.expiresAt && apiKey.expiresAt < new Date()) { return { valid: false, error: 'API key expired' }; } // Update usage await this.apiKeysCollection.updateOne( { _id: apiKey._id }, { $set: { lastUsed: new Date() }, $inc: { usageCount: 1 } } ); const user = await this.usersCollection.findOne({ _id: apiKey.userId }); return { valid: true, user: { _id: user._id, username: user.username, role: user.role, permissions: apiKey.permissions }, apiKey: { id: apiKey._id, name: apiKey.name, permissions: apiKey.permissions } }; } /** * Check if user has permission */ hasPermission(userPermissions, requiredPermission) { if (userPermissions.includes('*')) return true; if (userPermissions.includes(requiredPermission)) return true; // Check wildcard permissions for (const permission of userPermissions) { if (permission.endsWith('*')) { const prefix = permission.slice(0, -1); if (requiredPermission.startsWith(prefix)) return true; } } return false; } /** * Middleware for Express.js authentication */ middleware() { return async (req, res, next) => { try { const authHeader = req.headers.authorization; const apiKey = req.headers['x-api-key']; let authResult = null; if (apiKey) { // API Key authentication authResult = await this.verifyAPIKey(apiKey); if (authResult.valid) { req.user = authResult.user; req.authType = 'api_key'; req.apiKey = authResult.apiKey; } } else if (authHeader && authHeader.startsWith('Bearer ')) { // JWT authentication const token = authHeader.substring(7); authResult = await this.verifyToken(token); if (authResult.valid) { req.user = authResult.user; req.authType = 'jwt'; req.session = authResult.session; } } if (!authResult || !authResult.valid) { return res.status(401).json({ error: 'Authentication required', message: authResult?.error || 'No valid authentication provided' }); } next(); } catch (error) { res.status(401).json({ error: 'Authentication failed', message: error.message }); } }; } /** * Permission check middleware */ requirePermission(permission) { return (req, res, next) => { if (!req.user) { return res.status(401).json({ error: 'Authentication required' }); } if (!this.hasPermission(req.user.permissions, permission)) { return res.status(403).json({ error: 'Insufficient permissions', required: permission, userPermissions: req.user.permissions }); } next(); }; } // ===== PRIVATE METHODS ===== _generateSecret() { return crypto.randomBytes(64).toString('hex'); } _generateUserId() { return 'user_' + crypto.randomBytes(16).toString('hex'); } _generateSessionId() { return 'sess_' + crypto.randomBytes(24).toString('hex'); } _generateAPIKeyId() { return 'key_' + crypto.randomBytes(16).toString('hex'); } _generateAPIKeyToken() { return 'bb_' + crypto.randomBytes(32).toString('hex'); } _generateJWT(user, sessionId, isRefresh = false) { const payload = { userId: user._id, username: user.username, role: user.role, permissions: user.permissions, sessionId, type: isRefresh ? 'refresh' : 'access', iat: Math.floor(Date.now() / 1000), exp: Math.floor((Date.now() + this._parseTimeToMs( isRefresh ? this.options.refreshTokenExpiry : this.options.tokenExpiry )) / 1000) }; const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url'); const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url'); const signature = crypto .createHmac('sha256', this.options.jwtSecret) .update(`${header}.${payloadB64}`) .digest('base64url'); return `${header}.${payloadB64}.${signature}`; } _verifyJWT(token) { const parts = token.split('.'); if (parts.length !== 3) { throw new Error('Invalid token format'); } const [header, payload, signature] = parts; const expectedSignature = crypto .createHmac('sha256', this.options.jwtSecret) .update(`${header}.${payload}`) .digest('base64url'); if (signature !== expectedSignature) { throw new Error('Invalid token signature'); } const decodedPayload = JSON.parse(Buffer.from(payload, 'base64url')); if (decodedPayload.exp < Math.floor(Date.now() / 1000)) { throw new Error('Token expired'); } return decodedPayload; } _parseTimeToMs(timeString) { const units = { 's': 1000, 'm': 60 * 1000, 'h': 60 * 60 * 1000, 'd': 24 * 60 * 60 * 1000 }; const match = timeString.match(/^(\d+)([smhd])$/); if (!match) { throw new Error('Invalid time format'); } const [, amount, unit] = match; return parseInt(amount) * units[unit]; } async _incrementLoginAttempts(attemptKey) { const attempts = this.loginAttempts.get(attemptKey) || { count: 0, lockedUntil: null }; attempts.count++; if (attempts.count >= this.options.maxLoginAttempts) { attempts.lockedUntil = Date.now() + this.options.lockoutTime; } this.loginAttempts.set(attemptKey, attempts); } async _logAuditEvent(action, userId, details = {}) { try { await this.auditCollection.insertOne({ action, userId, details, timestamp: new Date(), ipAddress: details.ipAddress || null, userAgent: details.userAgent || null }); } catch (error) { console.error('Failed to log audit event:', error); } } /** * Get user management statistics */ async getAuthStats() { const [ totalUsers, activeUsers, totalSessions, totalApiKeys, recentLogins ] = await Promise.all([ this.usersCollection.countDocuments(), this.usersCollection.countDocuments({ status: 'active' }), this.activeSessions.size, this.apiKeysCollection.countDocuments({ active: true }), this.auditCollection.countDocuments({ action: 'login_success', timestamp: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) } }) ]); return { users: { total: totalUsers, active: activeUsers, inactive: totalUsers - activeUsers }, sessions: { active: totalSessions, refreshTokens: this.refreshTokens.size }, apiKeys: { total: totalApiKeys }, activity: { recentLogins } }; } /** * Clean up expired sessions and tokens */ async cleanup() { const now = new Date(); let cleaned = 0; // Clean expired sessions for (const [sessionId, session] of this.activeSessions) { if (session.expiresAt < now) { this.activeSessions.delete(sessionId); if (session.refreshToken) { this.refreshTokens.delete(session.refreshToken); } cleaned++; } } // Clean expired API keys await this.apiKeysCollection.updateMany( { expiresAt: { $lt: now } }, { $set: { active: false } } ); console.log(`๐Ÿงน Cleaned ${cleaned} expired sessions`); return cleaned; } } export default AuthenticationManager;