100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Node.js

Password Hashing with bcrypt

Understand why passwords must be hashed with bcrypt rather than stored in plain text or hashed with a fast general-purpose algorithm.

Authentication & SecurityIntermediate10 min readJul 8, 2026
Analogies

Introduction

Storing user passwords safely is one of the most important responsibilities of a backend application. Passwords must never be stored in plain text, and they should not be hashed with fast general-purpose algorithms like plain MD5 or a single round of SHA-256 either, because those algorithms are designed to be computed quickly, which makes them easy to brute-force with modern hardware (GPUs can compute billions of MD5/SHA-256 hashes per second). bcrypt is a purpose-built password-hashing algorithm that is intentionally slow and includes a random salt automatically, making both brute-force and rainbow-table attacks impractical.

🏏

Cricket analogy: A team never leaves its match strategy openly written in the dressing room for rivals to photograph; storing a password in plain text is like taping tomorrow's batting order to the pavilion window instead of locking it away.

Syntax

javascript
const bcrypt = require('bcrypt');

// Hashing a password before saving a user
const saltRounds = 12;
const passwordHash = await bcrypt.hash(plainPassword, saltRounds);

// Comparing a plaintext password against the stored hash during login
const isMatch = await bcrypt.compare(plainPassword, passwordHash);

Explanation

bcrypt.hash() takes the plaintext password and a 'cost factor' called salt rounds (commonly 10-12). Each increment of the salt rounds doubles the computation time, which is deliberate: it makes each individual hash attempt expensive, so an attacker trying millions of password guesses per second against a fast hash would only manage a tiny fraction of that against bcrypt. bcrypt also generates a unique random salt per password automatically and embeds it in the output hash string, so two users with the same password get completely different hashes, defeating precomputed rainbow-table attacks. Because the salt is stored alongside the hash, bcrypt.compare() can extract it and recompute the hash from the candidate password to check for a match — the original plaintext password is never stored or needed again.

🏏

Cricket analogy: Increasing bcrypt's salt rounds is like a bowler adding extra run-up steps each over: doubling the run-up time per delivery makes it impractical for a batting side to face thousands of deliveries (guesses) in a session, and each player's unique stance (salt) means no two batsmen face identical deliveries.

Example

javascript
const bcrypt = require('bcrypt');
const router = require('express').Router();

router.post('/signup', async (req, res) => {
  const { email, password } = req.body;

  const existing = await User.findOne({ email });
  if (existing) return res.status(409).json({ error: 'Email already registered' });

  const passwordHash = await bcrypt.hash(password, 12);
  const user = await User.create({ email, passwordHash });

  res.status(201).json({ id: user.id, email: user.email });
});

router.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findOne({ email });

  const isMatch = user && (await bcrypt.compare(password, user.passwordHash));
  if (!isMatch) return res.status(401).json({ error: 'Invalid email or password' });

  res.json({ message: 'Login successful' });
});

Output

A bcrypt hash looks like $2b$12$KIXQ4y2N8gk7z...Qe9J2vRZ7g4b9wq — the $2b$ identifies the algorithm version, 12 is the salt rounds used, and the rest encodes the salt plus the hash. Signup stores only this string, never the plaintext password. On login, bcrypt.compare('correctPassword', storedHash) resolves to true only if the plaintext hashes to the same value using the embedded salt; any incorrect password resolves to false, and the server responds with a generic 401 error that does not reveal whether the email or the password was wrong.

🏏

Cricket analogy: The $2b$12$ prefix is like a scorecard header noting match format and overs (algorithm version and rounds) before the ball-by-ball data; bcrypt.compare() replays the exact same conditions to check if today's innings matches the recorded one, without revealing which detail was wrong.

Key Takeaways

  • Never store plaintext passwords, and never rely on fast general-purpose hashes (MD5, SHA-256 alone) for passwords.
  • bcrypt is deliberately slow and automatically salts each password, defeating brute-force and rainbow-table attacks.
  • Use bcrypt.hash() at signup with 10-12 salt rounds and bcrypt.compare() at login — never decrypt or 'unhash' a bcrypt hash.
  • Return generic error messages on failed login so attackers cannot tell whether the email or password was incorrect.

Practice what you learned

Was this page helpful?

Topics covered

#NodeJs#NodeJsExpressStudyNotes#WebDevelopment#PasswordHashingWithBcrypt#Password#Hashing#Bcrypt#Syntax#StudyNotes#SkillVeris