2026-02-23•3 min read•by DevUtilz
Bcrypt Password Hashing Guide
SecurityBcryptPasswordTutorial
Bcrypt Password Hashing Guide
Bcrypt is the gold standard for password hashing. Learn how to implement it correctly in your applications.
Why Bcrypt?
Bcrypt is designed to be slow and memory-intensive, making it resistant to brute-force attacks. Key features:
- Adaptive: Work factor can be increased over time
- Salt included: Each password gets unique salt
- Proven: Used by millions of applications worldwide
Basic Implementation
Installation
npm install bcrypt
Hashing Passwords
const bcrypt = require('bcrypt');
async function hashPassword(password) {
const saltRounds = 12; // Higher = slower but more secure
const hash = await bcrypt.hash(password, saltRounds);
return hash;
}
Verifying Passwords
async function verifyPassword(password, hash) {
const match = await bcrypt.compare(password, hash);
return match;
}
Understanding the Hash
A bcrypt hash looks like this:
$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYqKx7pIqGi
Parts:
$2b$- Algorithm version12$- Cost factor (2^12 = 4096 iterations)LQv3c1yqBWVHxkd0LHAkCO- Salt (22 characters)Yz6TtxMQJqhN8/LewY5GyYqKx7pIqGi- Hash (31 characters)
Choosing the Right Cost Factor
| Cost | Time (approx) | Recommendation | |------|---------------|----------------| | 10 | ~100ms | Minimum for new apps | | 12 | ~400ms | Recommended default | | 14 | ~1.6s | High security | | 15+ | >3s | Too slow for web |
Start with 12 and adjust based on your performance requirements.
Best Practices
1. Never Store Plain Text Passwords
// Wrong - Never do this!
user.password = password;
// Correct - Always hash
user.password = await bcrypt.hash(password, 12);
2. Use Consistent Error Handling
async function login(email, password) {
const user = await findUserByEmail(email);
if (!user) {
return { error: 'Invalid credentials' };
}
const valid = await bcrypt.compare(password, user.password);
if (!valid) {
return { error: 'Invalid credentials' };
}
return { user };
}
3. Handle Timing Attacks
// Use bcrypt's compare to prevent timing attacks
const valid = await bcrypt.compare(password, hash);
// Don't use: password === storedHash
4. Implement Rate Limiting
const loginAttempts = new Map();
async function checkRateLimit(ip) {
const attempts = loginAttempts.get(ip) || 0;
if (attempts >= 5) {
throw new Error('Too many attempts. Try again later.');
}
loginAttempts.set(ip, attempts + 1);
// Reset after 15 minutes
setTimeout(() => loginAttempts.delete(ip), 900000);
}
Common Mistakes to Avoid
- Low cost factor - Don't use cost < 10
- No salt - Bcrypt handles this automatically
- Storing the password - Store only the hash
- Not handling errors - Always wrap in try/catch
Advanced: Custom Raw Hash
const salt = await bcrypt.genSalt(12);
const hash = await bcrypt.hash(password, salt);
// Or use the promise API
const hash = await bcrypt.hash(password, '$2b$12$customsaltstring');
Migration Strategy
When upgrading cost factor:
async function migratePassword(user, password) {
// Verify with old hash
const valid = await bcrypt.compare(password, user.password);
if (valid) {
// Rehash with new cost factor
user.password = await bcrypt.hash(password, 12);
await user.save();
}
return valid;
}
Conclusion
Bcrypt is the right choice for password hashing in most applications. Use cost factor 12, always hash passwords, and never store plain text. Your users' security depends on it.