e2d0a83d51
Test and Lint / backend-test (push) Successful in 1m9s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m13s
Version and Release / version-bump (push) Successful in 35s
Version and Release / trigger-drone (push) Successful in 3s
- Reduce minimum password length from 12 to 8 characters - Make special characters optional for gallery passwords - Lower strength requirement from score 3 to 1 for galleries - Add eye icon toggle for password visibility on each field - Remove redundant 'Show passwords' checkbox - Add translation for password security requirements error - Update both English and German translations This allows users to use simpler passwords like 'Sommer2025\!' for events while maintaining security through other measures like expiration dates. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
240 lines
7.3 KiB
JavaScript
240 lines
7.3 KiB
JavaScript
/**
|
|
* Password Validation and Security Utilities
|
|
* Implements strong password requirements and security checks
|
|
*/
|
|
|
|
const zxcvbn = require('zxcvbn');
|
|
const logger = require('./logger');
|
|
|
|
// Configuration
|
|
const PASSWORD_CONFIG = {
|
|
minLength: 8, // Reduced from 12 to 8 for better usability
|
|
requireUppercase: true,
|
|
requireLowercase: true,
|
|
requireNumbers: true,
|
|
requireSpecialChars: false, // Made optional for gallery passwords
|
|
preventCommonPasswords: true,
|
|
minStrengthScore: 2, // Reduced from 3 to 2 (moderate strength)
|
|
bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS) || 12 // Configurable, default 12
|
|
};
|
|
|
|
// Common passwords to block (extend this list)
|
|
const COMMON_PASSWORDS = [
|
|
'password', 'password123', 'admin123', 'welcome123', 'test123',
|
|
'qwerty', 'abc123', '123456', 'password1', 'admin',
|
|
'letmein', 'welcome', 'monkey', 'dragon', 'baseball'
|
|
];
|
|
|
|
/**
|
|
* Validate password meets security requirements
|
|
* @param {string} password - Password to validate
|
|
* @param {Object} options - Optional configuration overrides
|
|
* @returns {Object} - { valid: boolean, errors: string[], score: number, feedback: Object }
|
|
*/
|
|
function validatePassword(password, options = {}) {
|
|
const config = { ...PASSWORD_CONFIG, ...options };
|
|
const errors = [];
|
|
|
|
// Check if password exists
|
|
if (!password || typeof password !== 'string') {
|
|
return {
|
|
valid: false,
|
|
errors: ['Password is required'],
|
|
score: 0,
|
|
feedback: {}
|
|
};
|
|
}
|
|
|
|
// Check minimum length
|
|
if (password.length < config.minLength) {
|
|
errors.push(`Password must be at least ${config.minLength} characters long`);
|
|
}
|
|
|
|
// Check uppercase requirement
|
|
if (config.requireUppercase && !/[A-Z]/.test(password)) {
|
|
errors.push('Password must contain at least one uppercase letter');
|
|
}
|
|
|
|
// Check lowercase requirement
|
|
if (config.requireLowercase && !/[a-z]/.test(password)) {
|
|
errors.push('Password must contain at least one lowercase letter');
|
|
}
|
|
|
|
// Check number requirement
|
|
if (config.requireNumbers && !/[0-9]/.test(password)) {
|
|
errors.push('Password must contain at least one number');
|
|
}
|
|
|
|
// Check special character requirement
|
|
if (config.requireSpecialChars && !/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
|
|
errors.push('Password must contain at least one special character');
|
|
}
|
|
|
|
// Check against common passwords
|
|
if (config.preventCommonPasswords) {
|
|
const lowerPassword = password.toLowerCase();
|
|
if (COMMON_PASSWORDS.includes(lowerPassword)) {
|
|
errors.push('This password is too common. Please choose a more unique password');
|
|
}
|
|
}
|
|
|
|
// Use zxcvbn for strength analysis
|
|
const strength = zxcvbn(password);
|
|
|
|
// Check minimum strength score
|
|
if (strength.score < config.minStrengthScore) {
|
|
errors.push('Password is too weak. Please choose a stronger password');
|
|
}
|
|
|
|
// Add zxcvbn suggestions
|
|
if (strength.feedback.suggestions.length > 0) {
|
|
errors.push(...strength.feedback.suggestions);
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors,
|
|
score: strength.score,
|
|
feedback: {
|
|
warning: strength.feedback.warning,
|
|
suggestions: strength.feedback.suggestions,
|
|
crackTime: strength.crack_times_display.offline_slow_hashing_1e4_per_second
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate password for specific contexts (admin, gallery)
|
|
* @param {string} password - Password to validate
|
|
* @param {string} context - Context ('admin' or 'gallery')
|
|
* @param {Object} userData - Additional user data for context-aware validation
|
|
* @returns {Object} - Validation result
|
|
*/
|
|
function validatePasswordInContext(password, context, userData = {}) {
|
|
// Base validation
|
|
const result = validatePassword(password);
|
|
|
|
// Context-specific validation
|
|
if (context === 'admin') {
|
|
// Admins need stronger passwords
|
|
if (result.score < 4) {
|
|
result.valid = false;
|
|
result.errors.push('Admin passwords must be very strong (score 4/4)');
|
|
}
|
|
|
|
// Check password doesn't contain username
|
|
if (userData.username && password.toLowerCase().includes(userData.username.toLowerCase())) {
|
|
result.valid = false;
|
|
result.errors.push('Password must not contain your username');
|
|
}
|
|
|
|
// Check password doesn't contain email
|
|
if (userData.email) {
|
|
const emailUser = userData.email.split('@')[0];
|
|
if (password.toLowerCase().includes(emailUser.toLowerCase())) {
|
|
result.valid = false;
|
|
result.errors.push('Password must not contain parts of your email');
|
|
}
|
|
}
|
|
} else if (context === 'gallery') {
|
|
// Gallery passwords can be more lenient for user convenience
|
|
// Allow passwords with score >= 1 (weak but acceptable)
|
|
if (result.score < 1) {
|
|
result.valid = false;
|
|
result.errors.push('Password is too simple. Please add more complexity');
|
|
}
|
|
|
|
// Don't check for event name in password - allow date-based passwords
|
|
// This allows passwords like "Sommer2025!" which users prefer
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Generate a secure random password
|
|
* @param {Object} options - Generation options
|
|
* @returns {string} - Generated password
|
|
*/
|
|
function generateSecurePassword(options = {}) {
|
|
const config = {
|
|
length: options.length || 16,
|
|
includeUppercase: options.includeUppercase !== false,
|
|
includeLowercase: options.includeLowercase !== false,
|
|
includeNumbers: options.includeNumbers !== false,
|
|
includeSpecialChars: options.includeSpecialChars !== false,
|
|
excludeAmbiguous: options.excludeAmbiguous !== false
|
|
};
|
|
|
|
let charset = '';
|
|
|
|
if (config.includeLowercase) {
|
|
charset += config.excludeAmbiguous ? 'abcdefghjkmnpqrstuvwxyz' : 'abcdefghijklmnopqrstuvwxyz';
|
|
}
|
|
|
|
if (config.includeUppercase) {
|
|
charset += config.excludeAmbiguous ? 'ABCDEFGHJKLMNPQRSTUVWXYZ' : 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
}
|
|
|
|
if (config.includeNumbers) {
|
|
charset += config.excludeAmbiguous ? '23456789' : '0123456789';
|
|
}
|
|
|
|
if (config.includeSpecialChars) {
|
|
charset += '!@#$%^&*()_+-=[]{}|;:,.<>?';
|
|
}
|
|
|
|
if (charset.length === 0) {
|
|
throw new Error('At least one character type must be included');
|
|
}
|
|
|
|
// Generate password
|
|
const crypto = require('crypto');
|
|
let password = '';
|
|
|
|
for (let i = 0; i < config.length; i++) {
|
|
const randomIndex = crypto.randomInt(charset.length);
|
|
password += charset[randomIndex];
|
|
}
|
|
|
|
// Ensure password meets requirements
|
|
const validation = validatePassword(password);
|
|
if (!validation.valid) {
|
|
// Recursively generate until we get a valid password
|
|
return generateSecurePassword(options);
|
|
}
|
|
|
|
return password;
|
|
}
|
|
|
|
/**
|
|
* Get bcrypt rounds configuration
|
|
* @returns {number} - Number of bcrypt rounds to use
|
|
*/
|
|
function getBcryptRounds() {
|
|
return PASSWORD_CONFIG.bcryptRounds;
|
|
}
|
|
|
|
/**
|
|
* Log password validation failures for security monitoring
|
|
* @param {string} context - Context of validation failure
|
|
* @param {Array} errors - Validation errors
|
|
* @param {Object} metadata - Additional metadata
|
|
*/
|
|
function logPasswordValidationFailure(context, errors, metadata = {}) {
|
|
logger.warn('Password validation failed', {
|
|
context,
|
|
errorCount: errors.length,
|
|
errors: errors.slice(0, 3), // Log first 3 errors only
|
|
...metadata
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
validatePassword,
|
|
validatePasswordInContext,
|
|
generateSecurePassword,
|
|
getBcryptRounds,
|
|
logPasswordValidationFailure,
|
|
PASSWORD_CONFIG
|
|
}; |