CodingNic

Password Hashing & User Accounts

Hashing with bcrypt

Password Hashing & User Accounts 15 min read

Hashing with bcrypt

Objectives

By the end of this lesson, you should be able to:

  • Explain what hashing is, and why it’s one-way
  • Hash a password with bcrypt, and verify a password against a stored hash
  • Explain what salting is, and why two identical passwords produce different hashes

💡 Why this matters: Hashing is the actual fix to the problem the last lesson demonstrated, a stored password that’s useless to an attacker, even in a full database leak, while still being checkable at login.

⚠️ A note on verification: every snippet and every output in this lesson was actually run.

What Hashing Is

A hash function takes an input (a password) and produces a fixed-length, seemingly random string, the hash. It’s one-way: computing a hash from a password is fast, but there’s no way to reverse a hash back into the original password. Storing a password’s hash, instead of the password itself, means a leaked database no longer hands over anyone’s actual password.

Installing bcrypt

bash
npm install bcryptjs

bcryptjs is a pure JavaScript implementation of the bcrypt algorithm, no native compilation step required, functionally equivalent to the bcrypt package for everything covered in this course.

Hashing a Password

javascript
const bcrypt = require('bcryptjs');

async function main() {
  const password = 'correct horse battery staple';
  const hash = await bcrypt.hash(password, 10);
  console.log('Hash:', hash);
  console.log('Hash length:', hash.length);
}

main();
text
Hash: $2b$10$wBOqg8dkmJMNvqtl0Hr2ruUhkyFLlRC6MSNoUbEJtSbd4YraSsZjC
Hash length: 60

The second argument to bcrypt.hash(), 10, is the cost factor (also called “salt rounds”), how much computational work goes into the hash. Higher means slower to compute, and slower to brute-force, 10 is a solid, current default, fast enough for a real login, slow enough to meaningfully resist guessing.

Verifying a Password

Hashing is one-way, so login doesn’t hash the entered password and compare strings directly, it uses bcrypt.compare(), which knows how to check a plain password against a hash:

javascript
const matches = await bcrypt.compare('correct horse battery staple', hash);
console.log('Correct password matches:', matches);

const wrongMatches = await bcrypt.compare('wrong password', hash);
console.log('Wrong password matches:', wrongMatches);
text
Correct password matches: true
Wrong password matches: false

Salting: Why the Same Password Produces a Different Hash

javascript
const hash1 = await bcrypt.hash(password, 10);
const hash2 = await bcrypt.hash(password, 10);
console.log('Same password, different hash:', hash1 !== hash2);
text
Same password, different hash: true

Every call to bcrypt.hash() generates a random salt, a value mixed into the password before hashing, and stored as part of the resulting hash string itself (that’s why the full 60-character hash, not just a shorter digest, gets stored). Salting means two users with the identical password get completely different stored hashes, which defeats a rainbow table attack, a precomputed list of hashes for common passwords, since there’s no single hash for "password123" to look up anymore, every user’s is unique.

Never Hash on the Client

Hashing always happens on the server, sending a plain password over HTTPS to the server, which then hashes it, is correct and safe. Hashing in the browser before sending it doesn’t add security, and actually turns the hash itself into “the password” as far as the server’s concerned, defeating the purpose entirely.

Try It

  1. Hash a password with bcrypt.hash(), and confirm the resulting string is 60 characters long.
  2. Verify the correct password returns true from bcrypt.compare(), and an incorrect one returns false.
  3. Hash the same password twice, and confirm the two hashes are different.
  4. Explain, in your own words, why bcrypt.compare() is needed at all, instead of just hashing the login attempt and checking newHash === storedHash.

Recap

  • Hashing turns a password into a one-way, unrecoverable string, safe to store even in a leak.
  • bcrypt.hash(password, costFactor) hashes a password, bcrypt.compare(password, hash) verifies one against a stored hash.
  • A random salt, generated per hash and stored as part of it, means identical passwords never produce identical hashes, defeating rainbow table attacks.

Next lesson: wiring this into a real user model and a registration endpoint.