Secure Password Storage: Modern Hashing Techniques
Introduction
Password security is fundamental to application security, yet it’s commonly implemented incorrectly. Storing passwords in plaintext or using weak hashing algorithms can lead to catastrophic breaches. This article explores modern password hashing techniques and best practices for secure password storage.
Why You Should Never Store Passwords in Plaintext
Storing passwords in plaintext means that if an attacker gains database access, they immediately have access to all user credentials. Additionally, since users often reuse passwords across multiple services, a single breach can compromise users across the internet.
Vulnerable Password Storage Examples
Plaintext Storage (Never Do This)
javascript// EXTREMELY VULNERABLE - Never store plaintext passwords! const express = require('express'); const db = require('./database'); app.post('/register', async (req, res) => { const { username, password } = req.body; // Storing password in plaintext - CRITICAL VULNERABILITY await db.query( 'INSERT INTO users (username, password) VALUES (?, ?)', [username, password] ); res.json({ success: true }); }); app.post('/login', async (req, res) => { const { username, password } = req.body; const user = await db.query( 'SELECT * FROM users WHERE username = ? AND password = ?', [username, password] ); if (user) { res.json({ success: true }); } else { res.status(401).json({ error: 'Invalid credentials' }); } });
Weak Hashing (Also Vulnerable)
javascript// VULNERABLE - MD5 and SHA1 are not suitable for passwords const crypto = require('crypto'); function hashPasswordWeak(password) { // MD5 is fast and easily brute-forced return crypto.createHash('md5').update(password).digest('hex'); } // Even SHA256 without salt is vulnerable to rainbow tables function hashPasswordBetter(password) { return crypto.createHash('sha256').update(password).digest('hex'); }
Secure Password Hashing with bcrypt
bcrypt is specifically designed for password hashing with built-in salting and configurable work factor.
javascriptconst bcrypt = require('bcrypt'); const express = require('express'); const db = require('./database'); const SALT_ROUNDS = 12; // Adjust based on your security requirements // Registration with bcrypt app.post('/register', async (req, res) => { const { username, password } = req.body; // Validate password strength first if (!isPasswordStrong(password)) { return res.status(400).json({ error: 'Password must be at least 12 characters with mixed case, numbers, and symbols' }); } try { // Hash password with bcrypt (includes automatic salt) const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); await db.query( 'INSERT INTO users (username, password_hash) VALUES (?, ?)', [username, passwordHash] ); res.json({ success: true }); } catch (error) { console.error('Registration error:', error); res.status(500).json({ error: 'Registration failed' }); } }); // Login with bcrypt verification app.post('/login', async (req, res) => { const { username, password } = req.body; try { const user = await db.query( 'SELECT id, username, password_hash FROM users WHERE username = ?', [username] ); if (!user) { // Use constant-time response to prevent user enumeration await bcrypt.hash(password, SALT_ROUNDS); return res.status(401).json({ error: 'Invalid credentials' }); } // Compare provided password with stored hash const isValid = await bcrypt.compare(password, user.password_hash); if (!isValid) { return res.status(401).json({ error: 'Invalid credentials' }); } // Create session req.session.userId = user.id; res.json({ success: true, userId: user.id }); } catch (error) { console.error('Login error:', error); res.status(500).json({ error: 'Login failed' }); } }); // Password strength validation function isPasswordStrong(password) { if (password.length < 12) return false; const hasUpperCase = /[A-Z]/.test(password); const hasLowerCase = /[a-z]/.test(password); const hasNumbers = /\d/.test(password); const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password); return hasUpperCase && hasLowerCase && hasNumbers && hasSpecialChar; }
Using Argon2 (Current Best Practice)
Argon2 won the Password Hashing Competition in 2015 and is considered the most secure option.
javascriptconst argon2 = require('argon2'); // Configuration options const argon2Config = { type: argon2.argon2id, // Use Argon2id (hybrid mode) memoryCost: 65536, // 64 MB timeCost: 3, // 3 iterations parallelism: 4 // 4 parallel threads }; // Registration with Argon2 app.post('/register', async (req, res) => { const { username, password } = req.body; if (!isPasswordStrong(password)) { return res.status(400).json({ error: 'Password does not meet security requirements' }); } try { // Hash password with Argon2 const passwordHash = await argon2.hash(password, argon2Config); await db.query( 'INSERT INTO users (username, password_hash) VALUES (?, ?)', [username, passwordHash] ); res.json({ success: true }); } catch (error) { console.error('Registration error:', error); res.status(500).json({ error: 'Registration failed' }); } }); // Login with Argon2 verification app.post('/login', async (req, res) => { const { username, password } = req.body; try { const user = await db.query( 'SELECT id, username, password_hash FROM users WHERE username = ?', [username] ); if (!user) { // Constant-time dummy hash to prevent timing attacks await argon2.hash(password, argon2Config); return res.status(401).json({ error: 'Invalid credentials' }); } // Verify password const isValid = await argon2.verify(user.password_hash, password); if (!isValid) { return res.status(401).json({ error: 'Invalid credentials' }); } // Check if hash needs to be updated (rehashing) if (argon2.needsRehash(user.password_hash, argon2Config)) { const newHash = await argon2.hash(password, argon2Config); await db.query( 'UPDATE users SET password_hash = ? WHERE id = ?', [newHash, user.id] ); } req.session.userId = user.id; res.json({ success: true, userId: user.id }); } catch (error) { console.error('Login error:', error); res.status(500).json({ error: 'Login failed' }); } });
Python Implementation with Argon2
pythonfrom argon2 import PasswordHasher from argon2.exceptions import VerifyMismatchError # Initialize password hasher with secure defaults ph = PasswordHasher( time_cost=3, # Number of iterations memory_cost=65536, # 64 MB parallelism=4, # Number of parallel threads hash_len=32, # Length of hash salt_len=16 # Length of salt ) def register_user(username, password): """Register a new user with hashed password""" # Validate password strength if not is_password_strong(password): raise ValueError("Password does not meet security requirements") # Hash password password_hash = ph.hash(password) # Store in database db.execute( "INSERT INTO users (username, password_hash) VALUES (?, ?)", (username, password_hash) ) def authenticate_user(username, password): """Authenticate user and rehash if needed""" # Get user from database user = db.query( "SELECT id, username, password_hash FROM users WHERE username = ?", (username,) ) if not user: # Dummy hash to prevent timing attacks try: ph.hash(password) except: pass return None try: # Verify password ph.verify(user['password_hash'], password) # Check if rehashing is needed if ph.check_needs_rehash(user['password_hash']): new_hash = ph.hash(password) db.execute( "UPDATE users SET password_hash = ? WHERE id = ?", (new_hash, user['id']) ) return user except VerifyMismatchError: return None def is_password_strong(password): """Validate password strength""" if len(password) < 12: return False has_upper = any(c.isupper() for c in password) has_lower = any(c.islower() for c in password) has_digit = any(c.isdigit() for c in password) has_special = any(c in "!@#$%^&*()_+-=[]{}|;':\",./<>?" for c in password) return has_upper and has_lower and has_digit and has_special
Password Reset Flow
javascriptconst crypto = require('crypto'); const argon2 = require('argon2'); const nodemailer = require('nodemailer'); // Generate secure reset token function generateResetToken() { return crypto.randomBytes(32).toString('hex'); } // Request password reset app.post('/forgot-password', async (req, res) => { const { email } = req.body; const user = await db.query( 'SELECT id, email FROM users WHERE email = ?', [email] ); if (!user) { // Don't reveal if email exists return res.json({ message: 'If that email exists, a reset link has been sent' }); } // Generate reset token const resetToken = generateResetToken(); const resetTokenHash = await argon2.hash(resetToken); const expiresAt = new Date(Date.now() + 3600000); // 1 hour // Store hashed token await db.query( 'UPDATE users SET reset_token_hash = ?, reset_token_expires = ? WHERE id = ?', [resetTokenHash, expiresAt, user.id] ); // Send reset email const resetUrl = `https://yourapp.com/reset-password?token=${resetToken}&email=${email}`; await sendResetEmail(user.email, resetUrl); res.json({ message: 'If that email exists, a reset link has been sent' }); }); // Reset password app.post('/reset-password', async (req, res) => { const { email, token, newPassword } = req.body; if (!isPasswordStrong(newPassword)) { return res.status(400).json({ error: 'Password does not meet security requirements' }); } const user = await db.query( 'SELECT id, reset_token_hash, reset_token_expires FROM users WHERE email = ?', [email] ); if (!user || !user.reset_token_hash || new Date() > user.reset_token_expires) { return res.status(400).json({ error: 'Invalid or expired reset token' }); } try { // Verify reset token await argon2.verify(user.reset_token_hash, token); // Hash new password const newPasswordHash = await argon2.hash(newPassword, argon2Config); // Update password and clear reset token await db.query( 'UPDATE users SET password_hash = ?, reset_token_hash = NULL, reset_token_expires = NULL WHERE id = ?', [newPasswordHash, user.id] ); res.json({ success: true }); } catch (error) { res.status(400).json({ error: 'Invalid or expired reset token' }); } });
Password Security Best Practices
- Use Modern Algorithms: Argon2id > bcrypt > PBKDF2 >> SHA-256 >> MD5/SHA-1 (never)
- Sufficient Work Factor: Make hashing slow enough to deter brute force (200-500ms)
- Unique Salts: Modern algorithms handle this automatically
- Password Requirements: Enforce minimum length (12+ chars) and complexity
- Prevent Timing Attacks: Use constant-time comparisons
- Rehashing Strategy: Update hashes when users log in with outdated algorithms
- Secure Reset Flow: Use cryptographically random tokens, set expiration
- Rate Limiting: Limit login and reset attempts
- Multi-Factor Authentication: Add second factor for enhanced security
- Never Log Passwords: Ensure passwords aren’t logged anywhere in the system
Comparison of Hashing Algorithms
| Algorithm | Status | Use Case | Notes |
|---|---|---|---|
| Argon2id | ✅ Recommended | Password hashing | Winner of Password Hashing Competition |
| bcrypt | ✅ Good | Password hashing | Well-tested, widely supported |
| scrypt | ✅ Acceptable | Password hashing | Memory-hard, less common |
| PBKDF2 | ⚠️ Acceptable | Legacy systems | Better than SHA-256, not ideal |
| SHA-256 | ❌ Unsuitable | NOT for passwords | Too fast, vulnerable to GPUs |
| MD5/SHA-1 | ❌ Broken | Never use | Cryptographically broken |
Conclusion
Secure password storage is non-negotiable in modern applications. Use Argon2 or bcrypt with appropriate configuration, enforce strong password policies, implement secure reset flows, and always stay updated with current best practices.
Password security is just one aspect of application security. For comprehensive security assessments and penetration testing, contact the Whitespots team to ensure your entire authentication system is secure.


