streamflow/backend/utils/passwordPolicy.js

166 lines
5.2 KiB
JavaScript
Raw Permalink Normal View History

/**
* Password Policy Configuration
* Enforces strong password requirements
*/
const PASSWORD_POLICY = {
minLength: 12,
maxLength: 128,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true,
specialChars: '!@#$%^&*()_+-=[]{}|;:,.<>?',
preventCommonPasswords: true,
preventUserInfo: true, // Don't allow username/email in password
maxRepeatingChars: 3,
historyCount: 5 // Remember last 5 passwords
};
const ACCOUNT_LOCKOUT = {
maxFailedAttempts: 5,
lockoutDuration: 30 * 60 * 1000, // 30 minutes
resetAfterSuccess: true,
notifyOnLockout: true
};
const PASSWORD_EXPIRY = {
enabled: true,
expiryDays: 90,
warningDays: 14,
gracePeriodDays: 7
};
const SESSION_POLICY = {
maxConcurrentSessions: 3,
absoluteTimeout: 24 * 60 * 60 * 1000, // 24 hours
idleTimeout: 2 * 60 * 60 * 1000, // 2 hours
refreshTokenRotation: true
};
// Common passwords to block (top 100 most common)
const COMMON_PASSWORDS = [
'123456', 'password', '12345678', 'qwerty', '123456789', '12345', '1234', '111111',
'1234567', 'dragon', '123123', 'baseball', 'iloveyou', 'trustno1', '1234567890',
'sunshine', 'master', '123321', '666666', 'photoshop', '1111111', 'princess', 'azerty',
'000000', 'access', '696969', 'batman', '121212', 'letmein', 'qwertyuiop', 'admin',
'welcome', 'monkey', 'login', 'abc123', 'starwars', 'shadow', 'ashley', 'football',
'superman', 'michael', 'ninja', 'mustang', 'password1', 'passw0rd', 'password123'
];
/**
* Validates password against policy
* @param {string} password - Password to validate
* @param {object} userData - User data (username, email) to prevent personal info
* @returns {object} - {valid: boolean, errors: string[]}
*/
function validatePassword(password, userData = {}) {
const errors = [];
// Length check
if (password.length < PASSWORD_POLICY.minLength) {
errors.push(`Password must be at least ${PASSWORD_POLICY.minLength} characters long`);
}
if (password.length > PASSWORD_POLICY.maxLength) {
errors.push(`Password must not exceed ${PASSWORD_POLICY.maxLength} characters`);
}
// Character requirements
if (PASSWORD_POLICY.requireUppercase && !/[A-Z]/.test(password)) {
errors.push('Password must contain at least one uppercase letter');
}
if (PASSWORD_POLICY.requireLowercase && !/[a-z]/.test(password)) {
errors.push('Password must contain at least one lowercase letter');
}
if (PASSWORD_POLICY.requireNumbers && !/\d/.test(password)) {
errors.push('Password must contain at least one number');
}
if (PASSWORD_POLICY.requireSpecialChars) {
const specialCharsRegex = new RegExp(`[${PASSWORD_POLICY.specialChars.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}]`);
if (!specialCharsRegex.test(password)) {
errors.push('Password must contain at least one special character (!@#$%^&*...)');
}
}
// Repeating characters
const repeatingRegex = new RegExp(`(.)\\1{${PASSWORD_POLICY.maxRepeatingChars},}`);
if (repeatingRegex.test(password)) {
errors.push(`Password cannot contain more than ${PASSWORD_POLICY.maxRepeatingChars} repeating characters`);
}
// Common passwords
if (PASSWORD_POLICY.preventCommonPasswords) {
const lowerPassword = password.toLowerCase();
if (COMMON_PASSWORDS.some(common => lowerPassword.includes(common))) {
errors.push('Password is too common or easily guessable');
}
}
// User info in password
if (PASSWORD_POLICY.preventUserInfo && userData) {
const lowerPassword = password.toLowerCase();
if (userData.username && lowerPassword.includes(userData.username.toLowerCase())) {
errors.push('Password cannot contain your username');
}
if (userData.email) {
const emailParts = userData.email.split('@')[0].toLowerCase();
if (lowerPassword.includes(emailParts)) {
errors.push('Password cannot contain your email address');
}
}
}
return {
valid: errors.length === 0,
errors,
strength: calculatePasswordStrength(password)
};
}
/**
* Calculate password strength score (0-100)
*/
function calculatePasswordStrength(password) {
let score = 0;
// Length score (0-30 points)
score += Math.min(30, password.length * 2);
// Character variety (0-40 points)
if (/[a-z]/.test(password)) score += 10;
if (/[A-Z]/.test(password)) score += 10;
if (/\d/.test(password)) score += 10;
if (/[^a-zA-Z0-9]/.test(password)) score += 10;
// Patterns (0-30 points)
const hasNoRepeats = !/(.)\\1{2,}/.test(password);
const hasNoSequence = !/(?:abc|bcd|cde|123|234|345)/i.test(password);
const hasMixedCase = /[a-z]/.test(password) && /[A-Z]/.test(password);
if (hasNoRepeats) score += 10;
if (hasNoSequence) score += 10;
if (hasMixedCase) score += 10;
return Math.min(100, score);
}
/**
* Get password strength label
*/
function getStrengthLabel(score) {
if (score >= 80) return { label: 'Strong', color: 'success' };
if (score >= 60) return { label: 'Good', color: 'info' };
if (score >= 40) return { label: 'Fair', color: 'warning' };
return { label: 'Weak', color: 'error' };
}
module.exports = {
PASSWORD_POLICY,
ACCOUNT_LOCKOUT,
PASSWORD_EXPIRY,
SESSION_POLICY,
validatePassword,
calculatePasswordStrength,
getStrengthLabel
};