본문으로 건너뛰기 bcrypt Complete Guide | Secure Password Hashing in Node.js

bcrypt Complete Guide | Secure Password Hashing in Node.js

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:

  1. Never store plain passwords
  2. Use cost factor 10-12
  3. Always use async methods
  4. Implement rate limiting
  5. Enforce strong passwords

Next Steps:

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.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers bcrypt, Security, Authentication, Node.js, Passwords, Cryptography.