Difficulty: Intermediate
How do you securely store and verify passwords in Node.js? Explain hashing, salting, and bcrypt.
Passwords must never be stored in plain text. Instead, they are hashed using a one-way function that produces a fixed-length digest. Even if the database is compromised, the original passwords cannot be recovered from the hashes.
Bcrypt is the industry standard for password hashing. It automatically generates a random salt (preventing rainbow table attacks), includes the salt in the output, and uses a configurable work factor (salt rounds) that makes brute-force attacks computationally expensive.
The salt rounds parameter (typically 10-12) controls how many iterations the algorithm performs. Each increment doubles the computation time. This means bcrypt automatically becomes harder to crack as hardware gets faster - you just increase the rounds.
Bcrypt's compare function is designed to be constant-time, preventing timing attacks where an attacker measures response times to guess password characters. Always use bcrypt.compare() instead of comparing hashes with === directly.
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12; // ~300ms on modern hardware
// Hash a password
async function hashPassword(plainPassword) {
const hash = await bcrypt.hash(plainPassword, SALT_ROUNDS);
return hash;
// Output: $2b$12$LJ3m4ys3Lg... (60 chars)
// Format: $algorithm$rounds$salt+hash
}
// Verify a password
async function verifyPassword(plainPassword, storedHash) {
const isMatch = await bcrypt.compare(plainPassword, storedHash);
return isMatch;
}
// Usage
const hash = await hashPassword('MySecurePass123');
console.log('Hash:', hash);
console.log('Match:', await verifyPassword('MySecurePass123', hash));
console.log('No match:', await verifyPassword('wrong', hash));
bcrypt.hash() generates a unique salt each time, so the same password produces different hashes. bcrypt.compare() extracts the salt from the stored hash to verify.
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12;
// Registration
app.post('/api/auth/register', async (req, res) => {
const { email, password, name } = req.body;
// Check if user exists
const existing = await prisma.user.findUnique({ where: { email } });
if (existing) {
return res.status(409).json({ error: 'Email already registered' });
}
// Hash password before storing
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
const user = await prisma.user.create({
data: { email, password: hashedPassword, name },
});
res.status(201).json({ id: user.id, email: user.email });
});
// Login
app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await prisma.user.findUnique({ where: { email } });
// Same error for both cases (prevents email enumeration)
if (!user || !await bcrypt.compare(password, user.password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = generateToken(user);
res.json({ token, user: { id: user.id, email: user.email } });
});
Return the same error for missing user and wrong password to prevent email enumeration attacks. Hash on register, compare on login.
const crypto = require('crypto');
const bcrypt = require('bcrypt');
// BAD: Simple hash (fast = easy to brute force)
const md5 = crypto.createHash('md5').update('password').digest('hex');
// 5f4dcc3b5aa765d61d8327deb882cf99
// Can be cracked in milliseconds with rainbow tables
// BAD: SHA256 (still too fast for passwords)
const sha = crypto.createHash('sha256').update('password').digest('hex');
// Billions of hashes per second on GPU
// GOOD: bcrypt (intentionally slow)
const hash = await bcrypt.hash('password', 12);
// ~300ms per hash - makes brute force impractical
// Salt comparison
const hash1 = await bcrypt.hash('password', 12);
const hash2 = await bcrypt.hash('password', 12);
console.log(hash1 === hash2); // false! Different salts
console.log(await bcrypt.compare('password', hash1)); // true
console.log(await bcrypt.compare('password', hash2)); // true
MD5/SHA are designed to be fast - bad for passwords. bcrypt is intentionally slow and uses unique salts, so identical passwords produce different hashes.
bcrypt, Salt, Hashing, Password Security, Timing Attacks