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.
669 lines (667 loc) • 26.6 kB
JavaScript
/**
* Authentication Service
*
* Comprehensive authentication service with JWT tokens, refresh tokens,
* session management, and security features.
*/ import * as bcrypt from 'bcrypt';
import * as jwt from 'jsonwebtoken';
import { createLogger } from '../lib/logging.js';
import { StandardError, ErrorCode } from '../lib/errors.js';
import { AuthMiddleware } from '../middleware/auth-middleware.js';
const logger = createLogger('authentication-service');
export class AuthenticationService {
redis;
database;
authMiddleware;
config;
constructor(config){
this.redis = config.redis;
this.database = config.database;
this.authMiddleware = new AuthMiddleware(config.jwtSecret);
this.config = {
jwtExpiration: config.jwtExpiration || '15m',
refreshExpiration: config.refreshExpiration || '7d',
maxSessionsPerUser: config.maxSessionsPerUser || 3,
maxLoginAttempts: config.maxLoginAttempts || 5,
lockoutDuration: config.lockoutDuration || 15 * 60 * 1000
};
this.initializeDatabase();
}
/**
* Initialize database tables for user management
*/ initializeDatabase() {
try {
this.database.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'developer',
is_active BOOLEAN DEFAULT true,
failed_login_attempts INTEGER DEFAULT 0,
locked_until TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_active ON users(is_active);
`);
logger.info('Database tables initialized for authentication service');
} catch (error) {
logger.error('Failed to initialize database tables:', error);
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'Failed to initialize authentication service', {}, error);
}
}
/**
* Register a new user
*/ async registerUser(userData) {
try {
// Validate input
this.validateRegistrationData(userData);
// Check if user already exists
const existingUser = this.database.prepare(`
SELECT id FROM users WHERE email = ? OR username = ?
`).get(userData.email, userData.username);
if (existingUser) {
throw new StandardError(ErrorCode.CONFLICT, 'User with this email or username already exists', {
field: existingUser.id ? 'email' : 'username'
});
}
// Hash password
const passwordHash = await bcrypt.hash(userData.password, 12);
// Create user
const userId = this.generateId();
const now = new Date().toISOString();
this.database.prepare(`
INSERT INTO users (id, username, email, password_hash, role, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(userId, userData.username, userData.email, passwordHash, userData.role || 'developer', now, now);
const user = {
id: userId,
username: userData.username,
email: userData.email,
role: userData.role || 'developer',
createdAt: new Date(now)
};
// Generate tokens
const tokens = await this.generateTokenPair(user);
logger.info('User registered successfully', {
userId,
email: userData.email
});
return {
user,
tokens
};
} catch (error) {
if (error instanceof StandardError) {
throw error;
}
logger.error('User registration failed:', error);
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'Registration failed', {}, error);
}
}
/**
* Authenticate user and generate tokens
*/ async loginUser(loginData, ipAddress, userAgent) {
try {
// Get user by email
const user = this.database.prepare(`
SELECT id, username, email, password_hash, role, is_active, failed_login_attempts, locked_until
FROM users WHERE email = ?
`).get(loginData.email);
if (!user) {
await this.recordFailedLogin(loginData.email, ipAddress);
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Invalid email or password', {
code: 'INVALID_CREDENTIALS'
});
}
// Check if account is locked
if (user.locked_until && new Date(user.locked_until) > new Date()) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Account temporarily locked due to too many failed login attempts', {
code: 'ACCOUNT_LOCKED',
lockedUntil: user.locked_until
});
}
// Check if account is active
if (!user.is_active) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Account is deactivated', {
code: 'ACCOUNT_DEACTIVATED'
});
}
// Verify password
const isPasswordValid = await bcrypt.compare(loginData.password, user.password_hash);
if (!isPasswordValid) {
await this.recordFailedLogin(loginData.email, ipAddress);
await this.incrementFailedAttempts(user.id);
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Invalid email or password', {
code: 'INVALID_CREDENTIALS'
});
}
// Reset failed attempts on successful login
await this.resetFailedAttempts(user.id);
// Update last login
this.database.prepare(`
UPDATE users SET last_login = ?, updated_at = ? WHERE id = ?
`).run(new Date().toISOString(), new Date().toISOString(), user.id);
const userProfile = {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
createdAt: new Date(user.created_at),
lastLogin: new Date()
};
// Manage concurrent sessions
await this.manageConcurrentSessions(user.id);
// Generate tokens
const tokens = await this.generateTokenPair(userProfile, ipAddress, userAgent);
logger.info('User logged in successfully', {
userId: user.id,
email: user.email,
ipAddress
});
return {
user: userProfile,
tokens
};
} catch (error) {
if (error instanceof StandardError) {
throw error;
}
logger.error('Login failed:', error);
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'Login failed', {}, error);
}
}
/**
* Refresh access token using refresh token
*/ async refreshToken(refreshToken) {
try {
// Validate refresh token
const tokenData = await this.validateRefreshToken(refreshToken);
// Get current user data
const user = this.database.prepare(`
SELECT id, username, email, role, is_active
FROM users WHERE id = ? AND is_active = true
`).get(tokenData.userId);
if (!user) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'User not found or inactive', {
code: 'USER_INACTIVE'
});
}
const userProfile = {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
createdAt: new Date()
};
// Generate new token pair (refresh token rotation)
const tokens = await this.generateTokenPair(userProfile);
// Invalidate old refresh token (rotation)
await this.invalidateRefreshToken(refreshToken);
logger.info('Token refreshed successfully', {
userId: user.id
});
return tokens;
} catch (error) {
if (error instanceof StandardError) {
throw error;
}
logger.error('Token refresh failed:', error);
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'Token refresh failed', {}, error);
}
}
/**
* Logout user and invalidate tokens
*/ async logout(userId, accessToken, refreshToken) {
try {
const tokenPayload = this.authMiddleware.validateToken(accessToken);
// Invalidate access token (add to blacklist)
await this.addToBlacklist(accessToken, tokenPayload.exp);
// Invalidate refresh token if provided
if (refreshToken) {
await this.invalidateRefreshToken(refreshToken);
}
// Remove session
await this.removeSession(userId, tokenPayload.jti);
logger.info('User logged out successfully', {
userId
});
} catch (error) {
logger.error('Logout failed:', error);
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'Logout failed', {}, error);
}
}
/**
* Logout from all sessions
*/ async logoutAllSessions(userId, accessToken) {
try {
// Get all user sessions
const sessionKey = `sessions:${userId}`;
const sessionIds = await this.redis.smembers(sessionKey);
// Remove all sessions
for (const sessionId of sessionIds){
await this.redis.del(`session:${sessionId}`);
}
// Clear session set
await this.redis.del(sessionKey);
// Blacklist current access token
const tokenPayload = this.authMiddleware.validateToken(accessToken);
await this.addToBlacklist(accessToken, tokenPayload.exp);
logger.info('All sessions terminated', {
userId,
sessionCount: sessionIds.length
});
} catch (error) {
logger.error('Logout all sessions failed:', error);
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'Failed to terminate all sessions', {}, error);
}
}
/**
* Get user profile
*/ async getUserProfile(userId) {
try {
const user = this.database.prepare(`
SELECT id, username, email, role, created_at, last_login
FROM users WHERE id = ? AND is_active = true
`).get(userId);
if (!user) {
throw new StandardError(ErrorCode.NOT_FOUND, 'User not found', {
code: 'USER_NOT_FOUND'
});
}
return {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
createdAt: new Date(user.created_at),
lastLogin: user.last_login ? new Date(user.last_login) : undefined
};
} catch (error) {
if (error instanceof StandardError) {
throw error;
}
logger.error('Get user profile failed:', error);
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'Failed to get user profile', {}, error);
}
}
/**
* Update user profile
*/ async updateUserProfile(userId, updates) {
try {
// Validate updates
if (updates.username) {
this.validateUsername(updates.username);
// Check if username is already taken
const existingUser = this.database.prepare(`
SELECT id FROM users WHERE username = ? AND id != ?
`).get(updates.username, userId);
if (existingUser) {
throw new StandardError(ErrorCode.CONFLICT, 'Username already taken', {
field: 'username'
});
}
}
if (updates.email) {
this.validateEmail(updates.email);
// Check if email is already taken
const existingUser = this.database.prepare(`
SELECT id FROM users WHERE email = ? AND id != ?
`).get(updates.email, userId);
if (existingUser) {
throw new StandardError(ErrorCode.CONFLICT, 'Email already taken', {
field: 'email'
});
}
}
// Build update query
const fields = [];
const values = [];
if (updates.username) {
fields.push('username = ?');
values.push(updates.username);
}
if (updates.email) {
fields.push('email = ?');
values.push(updates.email);
}
fields.push('updated_at = ?');
values.push(new Date().toISOString());
values.push(userId);
// Update user
if (fields.length > 1) {
this.database.prepare(`
UPDATE users SET ${fields.join(', ')} WHERE id = ?
`).run(...values);
}
// Return updated profile
return await this.getUserProfile(userId);
} catch (error) {
if (error instanceof StandardError) {
throw error;
}
logger.error('Update user profile failed:', error);
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'Failed to update profile', {}, error);
}
}
/**
* Change user password
*/ async changePassword(userId, currentPassword, newPassword) {
try {
// Validate new password
this.validatePasswordStrength(newPassword);
// Get current user
const user = this.database.prepare(`
SELECT id, password_hash
FROM users WHERE id = ? AND is_active = true
`).get(userId);
if (!user) {
throw new StandardError(ErrorCode.NOT_FOUND, 'User not found', {
code: 'USER_NOT_FOUND'
});
}
// Verify current password
const isCurrentPasswordValid = await bcrypt.compare(currentPassword, user.password_hash);
if (!isCurrentPasswordValid) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Current password is incorrect', {
code: 'INVALID_CURRENT_PASSWORD'
});
}
// Hash new password
const newPasswordHash = await bcrypt.hash(newPassword, 12);
// Update password
this.database.prepare(`
UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?
`).run(newPasswordHash, new Date().toISOString(), userId);
// Invalidate all sessions (force re-login)
await this.logoutAllSessions(userId, '');
logger.info('Password changed successfully', {
userId
});
} catch (error) {
if (error instanceof StandardError) {
throw error;
}
logger.error('Change password failed:', error);
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'Failed to change password', {}, error);
}
}
/**
* Generate token pair for user
*/ async generateTokenPair(user, ipAddress, userAgent) {
try {
// Generate access token
const accessToken = this.authMiddleware.generateToken(user.id, user.username, user.role, user.email);
// Generate refresh token
const refreshToken = jwt.sign({
userId: user.id,
type: 'refresh',
sessionId: this.generateId()
}, process.env.JWT_SECRET || 'default-secret', {
expiresIn: this.config.refreshExpiration
});
// Store refresh token in Redis
const refreshKey = `refresh:${refreshToken}`;
const refreshData = {
userId: user.id,
type: 'refresh',
ipAddress,
userAgent,
createdAt: new Date().toISOString()
};
await this.redis.setex(refreshKey, this.parseExpiration(this.config.refreshExpiration), JSON.stringify(refreshData));
// Store session
const sessionId = this.generateId();
const sessionKey = `session:${sessionId}`;
const sessionData = {
userId: user.id,
username: user.username,
email: user.email,
role: user.role,
ipAddress,
userAgent,
createdAt: new Date().toISOString()
};
await this.redis.setex(sessionKey, this.parseExpiration(this.config.jwtExpiration), JSON.stringify(sessionData));
// Add session to user's session set
await this.redis.sadd(`sessions:${user.id}`, sessionId);
await this.redis.expire(`sessions:${user.id}`, this.parseExpiration(this.config.refreshExpiration));
return {
accessToken,
refreshToken,
expiresIn: this.parseExpiration(this.config.jwtExpiration),
tokenType: 'Bearer'
};
} catch (error) {
logger.error('Token generation failed:', error);
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'Failed to generate tokens', {}, error);
}
}
/**
* Validate refresh token
*/ async validateRefreshToken(refreshToken) {
try {
// Check if refresh token exists in Redis
const refreshKey = `refresh:${refreshToken}`;
const tokenData = await this.redis.get(refreshKey);
if (!tokenData) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Invalid or expired refresh token', {
code: 'INVALID_REFRESH_TOKEN'
});
}
// Verify JWT token
const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET || 'default-secret');
if (decoded.type !== 'refresh') {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Invalid token type', {
code: 'INVALID_TOKEN_TYPE'
});
}
return decoded;
} catch (error) {
if (error instanceof StandardError) {
throw error;
}
if (error instanceof jwt.TokenExpiredError) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Refresh token has expired', {
code: 'REFRESH_TOKEN_EXPIRED'
});
}
if (error instanceof jwt.JsonWebTokenError) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Invalid refresh token', {
code: 'INVALID_REFRESH_TOKEN'
});
}
throw error;
}
}
/**
* Invalidate refresh token
*/ async invalidateRefreshToken(refreshToken) {
try {
const refreshKey = `refresh:${refreshToken}`;
await this.redis.del(refreshKey);
} catch (error) {
logger.error('Failed to invalidate refresh token:', error);
}
}
/**
* Add token to blacklist
*/ async addToBlacklist(token, expirationTime) {
try {
const blacklistKey = `blacklist:${this.hashToken(token)}`;
const ttl = expirationTime - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await this.redis.setex(blacklistKey, ttl, '1');
}
} catch (error) {
logger.error('Failed to add token to blacklist:', error);
}
}
/**
* Remove session
*/ async removeSession(userId, sessionId) {
try {
await this.redis.del(`session:${sessionId}`);
await this.redis.srem(`sessions:${userId}`, sessionId);
} catch (error) {
logger.error('Failed to remove session:', error);
}
}
/**
* Manage concurrent sessions
*/ async manageConcurrentSessions(userId) {
try {
const sessionKey = `sessions:${userId}`;
const sessionIds = await this.redis.smembers(sessionKey);
if (sessionIds.length >= this.config.maxSessionsPerUser) {
// Remove oldest session
const oldestSessionId = sessionIds[0];
await this.removeSession(userId, oldestSessionId);
}
} catch (error) {
logger.error('Failed to manage concurrent sessions:', error);
}
}
/**
* Record failed login attempt
*/ async recordFailedLogin(email, ipAddress) {
try {
const key = `failed_login:${email}:${ipAddress || 'unknown'}`;
await this.redis.incr(key);
await this.redis.expire(key, this.config.lockoutDuration / 1000);
} catch (error) {
logger.error('Failed to record failed login:', error);
}
}
/**
* Increment failed login attempts for user
*/ async incrementFailedAttempts(userId) {
try {
const user = this.database.prepare(`
SELECT failed_login_attempts FROM users WHERE id = ?
`).get(userId);
if (user) {
const newAttempts = user.failed_login_attempts + 1;
const lockedUntil = newAttempts >= this.config.maxLoginAttempts ? new Date(Date.now() + this.config.lockoutDuration).toISOString() : null;
this.database.prepare(`
UPDATE users SET failed_login_attempts = ?, locked_until = ?, updated_at = ?
WHERE id = ?
`).run(newAttempts, lockedUntil, new Date().toISOString(), userId);
}
} catch (error) {
logger.error('Failed to increment failed attempts:', error);
}
}
/**
* Reset failed login attempts for user
*/ async resetFailedAttempts(userId) {
try {
this.database.prepare(`
UPDATE users SET failed_login_attempts = 0, locked_until = null, updated_at = ?
WHERE id = ?
`).run(new Date().toISOString(), userId);
} catch (error) {
logger.error('Failed to reset failed attempts:', error);
}
}
/**
* Validation methods
*/ validateRegistrationData(data) {
if (!data.username || data.username.trim().length < 3) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Username must be at least 3 characters long', {
field: 'username'
});
}
this.validateUsername(data.username);
this.validateEmail(data.email);
this.validatePasswordStrength(data.password);
if (data.role && ![
'admin',
'developer',
'readonly'
].includes(data.role)) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Invalid role specified', {
field: 'role'
});
}
}
validateUsername(username) {
if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Username can only contain letters, numbers, underscores, and hyphens', {
field: 'username'
});
}
}
validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Invalid email format', {
field: 'email'
});
}
}
validatePasswordStrength(password) {
if (password.length < 8) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Password must be at least 8 characters long', {
field: 'password'
});
}
if (!/[A-Z]/.test(password)) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Password must contain at least one uppercase letter', {
field: 'password'
});
}
if (!/[a-z]/.test(password)) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Password must contain at least one lowercase letter', {
field: 'password'
});
}
if (!/\d/.test(password)) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Password must contain at least one number', {
field: 'password'
});
}
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Password must contain at least one special character', {
field: 'password'
});
}
}
/**
* Utility methods
*/ generateId() {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}
parseExpiration(expiration) {
const match = expiration.match(/^(\d+)([smhd])$/);
if (!match) {
return 3600; // Default to 1 hour
}
const value = parseInt(match[1], 10);
const unit = match[2];
switch(unit){
case 's':
return value;
case 'm':
return value * 60;
case 'h':
return value * 3600;
case 'd':
return value * 86400;
default:
return 3600;
}
}
hashToken(token) {
return token.split('').reduce((acc, char)=>{
acc = (acc << 5) - acc + char.charCodeAt(0);
return acc & acc;
}, 0).toString(36);
}
}
//# sourceMappingURL=authentication.js.map