bcrypt Complete Guide | Secure Password Hashing in Node.js
이 글의 핵심
bcrypt is a password hashing library designed to be slow and computationally expensive, making brute-force attacks impractical. It's the industry standard for secure password storage.
Introduction
bcrypt is a password-hashing function designed for secure password storage. Unlike fast hash functions (MD5, SHA256), bcrypt is intentionally slow, making brute-force attacks impractical.
Why bcrypt?
Insecure (never do this):
const password = 'mypassword123';
const hash = crypto.createHash('sha256').update(password).digest('hex');
Problems:
- ❌ Too fast (billions of hashes per second)
- ❌ No salt (vulnerable to rainbow tables)
- ❌ Not designed for passwords
Secure (with bcrypt):
const bcrypt = require('bcrypt');
const hash = await bcrypt.hash('mypassword123', 10);
Benefits:
- ✅ Intentionally slow
- ✅ Automatic salting
- ✅ Adaptive (increase cost over time)
1. Installation
npm install bcrypt
2. Basic Usage
Hash a Password
const bcrypt = require('bcrypt');
async function hashPassword(password) {
const saltRounds = 10;
const hash = await bcrypt.hash(password, saltRounds);
return hash;
}
// Usage
const hash = await hashPassword('mypassword123');
console.log(hash);
// $2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
Verify a Password
async function verifyPassword(password, hash) {
const match = await bcrypt.compare(password, hash);
return match;
}
// Usage
const isValid = await verifyPassword('mypassword123', hash);
console.log(isValid); // true
const isInvalid = await verifyPassword('wrongpassword', hash);
console.log(isInvalid); // false
3. Salt Rounds (Cost Factor)
The salt rounds parameter determines how slow the hash function is. Each increase by 1 doubles the time.
// Fast (less secure)
await bcrypt.hash(password, 8); // ~40ms
// Balanced (recommended)
await bcrypt.hash(password, 10); // ~150ms
// Secure (slower)
await bcrypt.hash(password, 12); // ~600ms
// Very secure (slow)
await bcrypt.hash(password, 14); // ~2400ms
Recommendation: Start with 10, increase as hardware improves.
4. Manual Salt Generation
async function hashWithCustomSalt(password) {
// Generate salt
const salt = await bcrypt.genSalt(10);
console.log(salt); // $2b$10$N9qo8uLOickgx2ZMRZoMye
// Hash with salt
const hash = await bcrypt.hash(password, salt);
return hash;
}
Usually not needed — bcrypt.hash() does this automatically.
5. Synchronous API
// Hash (blocks event loop - not recommended)
const hash = bcrypt.hashSync('password123', 10);
// Verify (blocks event loop - not recommended)
const isValid = bcrypt.compareSync('password123', hash);
Warning: Use async versions in production to avoid blocking.
6. User Registration
const express = require('express');
const bcrypt = require('bcrypt');
const db = require('./database');
const app = express();
app.use(express.json());
app.post('/register', async (req, res) => {
try {
const { email, password } = req.body;
// Validate password strength
if (password.length < 8) {
return res.status(400).json({ error: 'Password too short' });
}
// Check if user exists
const existingUser = await db.findUserByEmail(email);
if (existingUser) {
return res.status(409).json({ error: 'Email already exists' });
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Save user
const user = await db.createUser({
email,
password: hashedPassword,
});
res.status(201).json({ id: user.id, email: user.email });
} catch (error) {
res.status(500).json({ error: 'Registration failed' });
}
});
7. User Login
app.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Find user
const user = await db.findUserByEmail(email);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Verify password
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate token (JWT, session, etc.)
const token = generateAuthToken(user);
res.json({ token, user: { id: user.id, email: user.email } });
} catch (error) {
res.status(500).json({ error: 'Login failed' });
}
});
8. Password Reset
const crypto = require('crypto');
// Generate reset token
app.post('/forgot-password', async (req, res) => {
const { email } = req.body;
const user = await db.findUserByEmail(email);
if (!user) {
// Don't reveal if email exists
return res.json({ message: 'If email exists, reset link sent' });
}
// Generate random token
const resetToken = crypto.randomBytes(32).toString('hex');
const resetTokenHash = await bcrypt.hash(resetToken, 10);
const expiry = Date.now() + 3600000; // 1 hour
await db.saveResetToken(user.id, resetTokenHash, expiry);
// Send email with resetToken (not hash!)
await sendResetEmail(email, resetToken);
res.json({ message: 'If email exists, reset link sent' });
});
// Reset password
app.post('/reset-password', async (req, res) => {
const { token, newPassword } = req.body;
// Find user by token
const resetData = await db.findResetToken();
// Verify token
const isValidToken = await bcrypt.compare(token, resetData.hash);
if (!isValidToken || Date.now() > resetData.expiry) {
return res.status(400).json({ error: 'Invalid or expired token' });
}
// Hash new password
const hashedPassword = await bcrypt.hash(newPassword, 10);
// Update user
await db.updatePassword(resetData.userId, hashedPassword);
await db.deleteResetToken(resetData.userId);
res.json({ message: 'Password reset successful' });
});
9. Sequelize Integration
const { DataTypes } = require('sequelize');
const bcrypt = require('bcrypt');
const User = sequelize.define('User', {
email: {
type: DataTypes.STRING,
unique: true,
allowNull: false,
},
password: {
type: DataTypes.STRING,
allowNull: false,
},
}, {
hooks: {
// Hash password before save
beforeCreate: async (user) => {
if (user.password) {
user.password = await bcrypt.hash(user.password, 10);
}
},
beforeUpdate: async (user) => {
if (user.changed('password')) {
user.password = await bcrypt.hash(user.password, 10);
}
},
},
});
// Instance method to verify password
User.prototype.verifyPassword = async function(password) {
return await bcrypt.compare(password, this.password);
};
// Usage
const user = await User.create({
email: 'user@example.com',
password: 'password123', // Automatically hashed
});
const isValid = await user.verifyPassword('password123');
10. Mongoose Integration
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
});
// Hash password before save
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
// Instance method to verify password
userSchema.methods.verifyPassword = async function(password) {
return await bcrypt.compare(password, this.password);
};
const User = mongoose.model('User', userSchema);
// Usage
const user = new User({
email: 'user@example.com',
password: 'password123',
});
await user.save(); // Password automatically hashed
const isValid = await user.verifyPassword('password123');
11. Security Best Practices
1. Never Log Passwords
// Bad
console.log('Password:', password);
logger.info('User registered', { password });
// Good
console.log('User registered');
logger.info('User registered', { email: user.email });
2. Rate Limit Login Attempts
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 requests
message: 'Too many login attempts, try again later',
});
app.post('/login', loginLimiter, async (req, res) => {
// Login logic
});
3. Use Constant-Time Comparison
bcrypt.compare() already uses constant-time comparison to prevent timing attacks. Never use === to compare hashes.
// Bad - vulnerable to timing attacks
if (hash === storedHash) { }
// Good - constant time
await bcrypt.compare(password, hash);
4. Enforce Strong Passwords
function isStrongPassword(password) {
return (
password.length >= 8 &&
/[A-Z]/.test(password) &&
/[a-z]/.test(password) &&
/[0-9]/.test(password) &&
/[^A-Za-z0-9]/.test(password)
);
}
5. Increase Cost Factor Over Time
async function rehashIfNeeded(user, password) {
const currentCost = 10;
const hash = user.password;
// Extract cost from hash
const cost = parseInt(hash.split('$')[2], 10);
if (cost < currentCost) {
const newHash = await bcrypt.hash(password, currentCost);
await db.updatePassword(user.id, newHash);
}
}
12. Performance Optimization
1. Don’t Hash in Request Handler
// Bad - blocks all requests
app.post('/register', (req, res) => {
const hash = bcrypt.hashSync(password, 14); // Blocks!
});
// Good - non-blocking
app.post('/register', async (req, res) => {
const hash = await bcrypt.hash(password, 14);
});
2. Use Worker Threads for Heavy Load
const { Worker } = require('worker_threads');
function hashPasswordWorker(password, saltRounds) {
return new Promise((resolve, reject) => {
const worker = new Worker('./hash-worker.js', {
workerData: { password, saltRounds },
});
worker.on('message', resolve);
worker.on('error', reject);
});
}
13. Testing
const bcrypt = require('bcrypt');
describe('Password Hashing', () => {
it('should hash password', async () => {
const password = 'password123';
const hash = await bcrypt.hash(password, 10);
expect(hash).not.toBe(password);
expect(hash).toMatch(/^\$2[ab]\$/);
});
it('should verify correct password', async () => {
const password = 'password123';
const hash = await bcrypt.hash(password, 10);
const isValid = await bcrypt.compare(password, hash);
expect(isValid).toBe(true);
});
it('should reject incorrect password', async () => {
const password = 'password123';
const hash = await bcrypt.hash(password, 10);
const isValid = await bcrypt.compare('wrongpassword', hash);
expect(isValid).toBe(false);
});
it('should generate unique hashes', async () => {
const password = 'password123';
const hash1 = await bcrypt.hash(password, 10);
const hash2 = await bcrypt.hash(password, 10);
expect(hash1).not.toBe(hash2); // Different salts
});
});
Summary
bcrypt provides secure password storage:
- Slow by design - resistant to brute force
- Automatic salting - prevents rainbow tables
- Adaptive - increase cost over time
- Industry standard - proven and trusted
- Easy to use - simple API
Key Takeaways:
- Never store plain passwords
- Use cost factor 10-12
- Always use async methods
- Implement rate limiting
- Enforce strong passwords
Next Steps:
- Implement JWT Auth
- Secure with Helmet
- Build Express API
Resources: