How Passwords Are Protected in Real Apps | Hashing, Salt & Pepper 🔐
Ever wondered what actually happens to your password the moment you hit "Sign Up"?
No serious application ever stores your password as plain text. Instead, real-world apps use
a combination of password hashing, salting, and sometimes peppering to make sure
that even if a database is leaked, attackers can't easily recover the original passwords.
In this tutorial, we will learn:
Why storing plain text passwords is dangerous
What password hashing is and how it works
Why hashing alone isn't enough — the problem of rainbow tables
What a salt is and how it protects against precomputed attacks
What a pepper is and how it differs from a salt
Implementing secure password hashing in Node.js using bcrypt
Verifying a login password against its stored hash
---
Why Not Just Store Passwords as Plain Text?
If a hacker ever gains access to a database that stores plain text passwords, every single
user account is instantly compromised — and since many people reuse passwords across sites,
the damage spreads far beyond just your app. This is why storing raw, readable passwords is
considered one of the worst security mistakes a developer can make. The solution is to never
store the actual password at all — only a scrambled, irreversible version of it.
---
What Is Password Hashing?
Hashing is a one-way mathematical function that converts a password like
MyPassword123 into a fixed-length string of random-looking characters, called a
hash. The key property of a good hashing algorithm is that it is irreversible —
you can't take the hash and work backward to get the original password. When a user logs in,
the app hashes the entered password again and compares the two hashes, instead of comparing
raw passwords.
// A simplified idea of hashing (not real code, just for understanding)
password = "MyPassword123";
hash = hashFunction(password);
// hash => "e3f1c2...9a7b" (stored in database, not the real password)
---
The Problem With Plain Hashing: Rainbow Tables
Hashing alone has a weakness. Attackers can precompute the hashes of millions of common
passwords (like 123456 or password) and store them in giant lookup
tables called rainbow tables. If two users pick the same password, their hashes will
be identical — so an attacker only needs to crack it once to unlock every matching account.
This is exactly the gap that salting is designed to close.
---
What Is a Salt?
A salt is a random string of characters generated uniquely for every single user, which
gets combined with their password before hashing. Because the salt is different for
every user, even two identical passwords produce completely different hashes. The salt is
stored alongside the hash in the database — it doesn't need to be secret, it just needs to be
unique and random.
// Conceptual example
salt = generateRandomString(); // unique per user, e.g. "x7Ak2p"
saltedPassword = password + salt; // "MyPassword123x7Ak2p"
hash = hashFunction(saltedPassword); // stored along with the salt
---
What Is a Pepper — and How Is It Different From a Salt?
A pepper is also a random value added to the password before hashing, but with one key
difference: it is not stored in the database. Instead, it's kept somewhere separate —
typically as an environment variable or in a secrets manager. The same pepper is usually
shared across all users of the app. This way, even if the entire database is stolen,
the attacker still doesn't have the pepper needed to crack the hashes.
| Concept |
Unique Per User? |
Stored In |
Purpose |
| Hash |
N/A |
Database |
One-way scrambled version of the password |
| Salt |
Yes |
Database (next to the hash) |
Prevents identical passwords from producing identical hashes |
| Pepper |
No (shared across app) |
Environment variable / secrets manager |
Adds a secret layer that isn't exposed even if the database leaks |
---
Step 1: Install bcrypt in Your Node.js Project
In real applications, we don't write our own hashing logic — we use a well-tested library
like bcrypt, which automatically handles salting for us internally.
---
Step 2: Hashing a Password on Signup
When a user signs up, we hash their password (combined with our secret pepper) before saving
it to the database. bcrypt automatically generates and stores a unique salt as
part of the resulting hash string.
const bcrypt = require("bcrypt");
const PEPPER = process.env.PASSWORD_PEPPER; // stored securely, not in the database
const SALT_ROUNDS = 10;
async function registerUser(email, plainPassword) {
const pepperedPassword = plainPassword + PEPPER;
const hashedPassword = await bcrypt.hash(pepperedPassword, SALT_ROUNDS);
// Save `email` and `hashedPassword` to the database
return { email, hashedPassword };
}
---
Step 3: Verifying a Password on Login
When the user logs in, we re-apply the same pepper to the entered password and let
bcrypt.compare handle extracting the salt and checking it against the stored hash.
async function loginUser(enteredPassword, storedHash) {
const pepperedInput = enteredPassword + PEPPER;
const isMatch = await bcrypt.compare(pepperedInput, storedHash);
if (isMatch) {
console.log("Login successful ✅");
} else {
console.log("Invalid credentials ❌");
}
return isMatch;
}
---
Why bcrypt Instead of a Simple Hash Like MD5 or SHA-256?
General-purpose hash functions like MD5 or SHA-256 are built for speed, which is
exactly the wrong property for password storage — a fast hash means an attacker can try
billions of guesses per second on stolen hashes. bcrypt (along with alternatives
like scrypt and Argon2) is intentionally slow and configurable via a
cost factor (SALT_ROUNDS), making brute-force attacks far more expensive
and impractical, even with modern hardware.
---
Features and Learnings:-
Understood why plain text password storage is a critical security risk.
Learned what hashing is and why it's a one-way, irreversible process.
Understood the rainbow table attack and why plain hashing isn't enough on its own.
Learned what a salt is and how it makes every user's hash unique.
Learned what a pepper is and how it differs from a salt by staying outside the database.
Installed and used bcrypt to hash passwords securely in Node.js.
Implemented a real signup flow that hashes passwords with a salt and pepper.
Implemented a real login flow that verifies passwords using bcrypt.compare.
Understood why slow, purpose-built algorithms like bcrypt are safer than fast hashes like MD5 or SHA-256.