diff --git a/packages/features/auth/lib/verifyPassword.ts b/packages/features/auth/lib/verifyPassword.ts index 21679f95b23e5e..7c3881b4497f25 100644 --- a/packages/features/auth/lib/verifyPassword.ts +++ b/packages/features/auth/lib/verifyPassword.ts @@ -1,6 +1,21 @@ import { compare } from "bcryptjs"; -export async function verifyPassword(password: string, hashedPassword: string) { - const isValid = await compare(password, hashedPassword); - return isValid; +/** + * Verifies a password against a hashed password with timing attack protection + * @param password - Plain text password to verify + * @param hashedPassword - Hashed password to compare against + * @returns Promise indicating if password is valid + */ +export async function verifyPassword(password: string, hashedPassword: string): Promise { + if (!password || !hashedPassword) { + return false; + } + + try { + const isValid = await compare(password, hashedPassword); + return isValid; + } catch (error) { + console.error("Password verification failed", error); + return false; + } } diff --git a/packages/lib/auth/hashPassword.ts b/packages/lib/auth/hashPassword.ts index 514b81aa5c740c..02433f754a397a 100644 --- a/packages/lib/auth/hashPassword.ts +++ b/packages/lib/auth/hashPassword.ts @@ -1,6 +1,19 @@ import { hash } from "bcryptjs"; -export async function hashPassword(password: string) { - const hashedPassword = await hash(password, 12); +const DEFAULT_SALT_ROUNDS = 12; +const MIN_PASSWORD_LENGTH = 8; + +/** + * Hashes a password using bcrypt with configurable salt rounds + * @param password - Plain text password to hash + * @param saltRounds - Number of salt rounds (default: 12) + * @returns Promise containing the hashed password + */ +export async function hashPassword(password: string, saltRounds: number = DEFAULT_SALT_ROUNDS): Promise { + if (!password || password.length < MIN_PASSWORD_LENGTH) { + throw new Error(`Password must be at least ${MIN_PASSWORD_LENGTH} characters long`); + } + + const hashedPassword = await hash(password, saltRounds); return hashedPassword; } diff --git a/packages/lib/auth/passwordStrength.ts b/packages/lib/auth/passwordStrength.ts new file mode 100644 index 00000000000000..ff444388e05952 --- /dev/null +++ b/packages/lib/auth/passwordStrength.ts @@ -0,0 +1,120 @@ +/** + * Password strength validation utilities + */ + +export interface PasswordStrengthResult { + isStrong: boolean; + score: number; + feedback: string[]; +} + +const MIN_LENGTH = 8; +const RECOMMENDED_LENGTH = 12; + +/** + * Evaluates password strength based on various criteria + * @param password - Password to evaluate + * @returns PasswordStrengthResult with score and feedback + */ +export function evaluatePasswordStrength(password: string): PasswordStrengthResult { + const feedback: string[] = []; + let score = 0; + + if (!password) { + return { + isStrong: false, + score: 0, + feedback: ["Password is required"], + }; + } + + if (password.length >= MIN_LENGTH) { + score += 20; + } else { + feedback.push(`Password must be at least ${MIN_LENGTH} characters`); + } + + if (password.length >= RECOMMENDED_LENGTH) { + score += 10; + } + + if (/[a-z]/.test(password)) { + score += 15; + } else { + feedback.push("Add lowercase letters"); + } + + if (/[A-Z]/.test(password)) { + score += 15; + } else { + feedback.push("Add uppercase letters"); + } + + if (/[0-9]/.test(password)) { + score += 20; + } else { + feedback.push("Add numbers"); + } + + if (/[^a-zA-Z0-9]/.test(password)) { + score += 20; + } else { + feedback.push("Add special characters"); + } + + const hasRepeatingChars = /(.)\1{2,}/.test(password); + if (hasRepeatingChars) { + score -= 10; + feedback.push("Avoid repeating characters"); + } + + const hasSequentialChars = /(abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz|012|123|234|345|456|567|678|789)/i.test(password); + if (hasSequentialChars) { + score -= 10; + feedback.push("Avoid sequential characters"); + } + + return { + isStrong: score > 70, + score: Math.max(0, Math.min(100, score)), + feedback, + }; +} + +/** + * Checks if password meets minimum security requirements + * @param password - Password to validate + * @returns boolean indicating if password is valid + */ +export function isPasswordValid(password: string): boolean { + if (!password || password.length < MIN_LENGTH) { + return false; + } + + const hasLetter = /[a-zA-Z]/.test(password); + const hasNumber = /[0-9]/.test(password); + + return hasLetter || hasNumber; +} + +/** + * Generates password validation error message + * @param password - Password to validate + * @returns Error message or null if valid + */ +export function getPasswordValidationError(password: string): string | null { + if (!password) { + return "Password is required"; + } + + if (password.length < MIN_LENGTH) { + return `Password must be at least ${MIN_LENGTH} characters long`; + } + + const result = evaluatePasswordStrength(password); + if (!result.isStrong && result.feedback.length > 0) { + return result.feedback[0]; + } + + return null; +}