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:
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Complete bcrypt guide for password security. Learn hashing, salting, verification, cost factors, and implementing secure…
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- C++ Constant Initialization |
constexpr·constinit - Node.js JWT Authentication Complete Guide | bcrypt
- C++ Profiling
Keywords Covered in This Article (Related Search Terms)
This article covers bcrypt, Security, Authentication, Node.js, Passwords, Cryptography.