165 lines
5.2 KiB
JavaScript
165 lines
5.2 KiB
JavaScript
/**
|
|
* 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
|
|
};
|