Here's a scary thought: every week, major companies announce data breaches exposing millions of passwords. If you've ever reused a password, you're at risk. But here's the thing, well-designed systems should never be able to leak your actual password, even if they're completely compromised.
This is the magic of password hashingWhat is hashing?A one-way mathematical transformation that turns data (like a password) into a fixed-length string that can't be reversed. Used to store passwords securely..
The cardinal sin: storing plain text passwords
// ⚠️ NEVER DO THIS
const user = {
email: '[email protected]',
password: 'MySecretPassword123' // Plain text!
};If an attacker gets your database, they now have everyone's passwords. And since people reuse passwords across sites, you've just compromised their email, bank accounts, and social media.
Real-world impact of password leaks:
- Direct account takeover: Attacker logs in with stolen credentials
- Credential stuffingWhat is credential stuffing?An automated attack that tries username/password pairs leaked from one breach against many other services.: Automated attacks trying the same email/password on other sites
- Reputation damage: Users lose trust in your service
- Legal consequences: GDPRWhat is gdpr?A European regulation that gives users control over their personal data, including the right to access, delete, and export it. violations, potential fines, mandatory disclosure
HashingWhat is hashing?A one-way mathematical transformation that turns data (like a password) into a fixed-length string that can't be reversed. Used to store passwords securely. vs encryptionWhat is encryption?Scrambling data so only someone with the right key can read it, protecting information from being intercepted or stolen.
These terms are often confused, but they're fundamentally different:
| Feature | Hashing | Encryption |
|---|---|---|
| Reversible | No | Yes (with key) |
| Use case | Passwords | Data protection |
| Examples | bcrypt, Argon2 | AES, RSA |
| Output length | Fixed | Same as input |
Encryption is reversible, you can decrypt the data with the right key. This is great for protecting credit card numbers or personal information that you need to read later.
Hashing is one-way. You can verify that "password123" produces hash XYZ, but you can't reverse XYZ back to "password123". This is perfect for passwords because you never need to know the original password, you only need to verify that the user typed it correctly.
bcryptWhat is bcrypt?A widely used password hashing algorithm that is intentionally slow to make brute-force cracking impractical. It automatically generates and embeds a salt in the hash output.: the industry standard
bcrypt is the most widely recommended password hashingWhat is hashing?A one-way mathematical transformation that turns data (like a password) into a fixed-length string that can't be reversed. Used to store passwords securely. algorithm. It automatically handles salting and is intentionally slow to make brute force attacks impractical.
npm install bcryptimport bcrypt from 'bcrypt';
const SALT_ROUNDS = 12; // 10-12 is recommended
// Hash a password
async function hashPassword(password) {
const salt = await bcrypt.genSalt(SALT_ROUNDS);
const hash = await bcrypt.hash(password, salt);
return hash;
// Returns: $2bCODE_BLOCK2$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYA.qGZvKG6G
// Format: $2b$ = bcrypt, 12$ = cost factor, rest = salt + hash
}
// Verify a password
async function verifyPassword(password, hash) {
return bcrypt.compare(password, hash);
// Returns true or false
}What's happening here?
genSalt(12)creates a random 22-character saltWhat is salt?A random string added to a password before hashing so that two users with the same password produce different hash values.hash()combines the password with the salt and runs the bcrypt algorithm- The result includes the algorithm version, cost factor, salt, and hash
compare()extracts the salt from the stored hash, hashes the input password with it, and compares
The "12" in genSalt(12) is the cost factor. Higher numbers mean slower hashing, which makes brute force attacks harder. But don't go too high, 12 rounds takes about 250ms on modern hardware. That's the sweet spot.
Complete registration and login
// Registration endpoint
app.post('/auth/register', async (req, res) => {
const { email, password } = req.body;
// Validate password strength
if (password.length < 8) {
return res.status(400).json({
error: 'Password must be at least 8 characters'
});
}
// Hash the password
const passwordHash = await hashPassword(password);
// Store in database (NEVER store the plain password)
const user = await db.createUser({
email,
passwordHash // Store hash, not password!
});
// Return user data (without the hash!)
res.status(201).json({
id: user.id,
email: user.email
});
});
// Login endpoint
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
// Find user by email
const user = await db.findUserByEmail(email);
// Check if user exists
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Verify password
const valid = await verifyPassword(password, user.passwordHash);
if (!valid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate token (JWT or session)
const token = generateToken(user.id);
res.json({ token });
});Argon2What is argon2?A memory-hard password hashing algorithm recommended for new projects over bcrypt - intentionally expensive to resist brute-force attacks.: the modern choice
Argon2 won the Password HashingWhat is hashing?A one-way mathematical transformation that turns data (like a password) into a fixed-length string that can't be reversed. Used to store passwords securely. Competition in 2015 and is now the recommended algorithm for new projects.
npm install argon2import argon2 from 'argon2';
// Hash password
const hash = await argon2.hash(password);
// Verify password
const valid = await argon2.verify(hash, password);Why Argon2 is better than bcryptWhat is bcrypt?A widely used password hashing algorithm that is intentionally slow to make brute-force cracking impractical. It automatically generates and embeds a salt in the hash output.:
- Memory-hard: Resistant to GPU and ASIC attacks (specialized cracking hardware)
- Configurable: You can tune memory usage, time cost, and parallelism
- Winner of official competition: Designed by cryptography experts
For new projects, prefer Argon2. For existing bcrypt implementations, they're still secure, don't rush to migrate unless you have specific threat models.
Password security best practices
Validate password strength
Don't let users choose "password" or "123456":
import { z } from 'zod';
const passwordSchema = z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Must contain at least one uppercase letter')
.regex(/[a-z]/, 'Must contain at least one lowercase letter')
.regex(/[0-9]/, 'Must contain at least one number')
.regex(/[^A-Za-z0-9]/, 'Must contain at least one special character');Never log passwords
// ❌ WRONG
console.log('Login attempt:', { email, password });
// ✅ RIGHT
console.log('Login attempt:', { email }); // Never log passwordsEven in development, never log passwords. They end up in log files, which might be shipped to external services or accidentally exposed.
Use generic error messages
// ❌ WRONG (information leak)
if (!user) {
return res.status(401).json({ error: 'Email not found' });
}
if (!valid) {
return res.status(401).json({ error: 'Wrong password' });
}
// ✅ RIGHT (same message for both)
if (!user || !valid) {
return res.status(401).json({ error: 'Invalid credentials' });
}With specific messages, attackers can enumerate registered emails by trying different addresses and seeing which error they get.
Rate limit authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token.
import rateLimit from 'express-rate-limit';
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per window
message: 'Too many login attempts. Please try again later.',
standardHeaders: true,
legacyHeaders: false
});
app.use('/auth/', authLimiter);Without rate limitingWhat is rate limiting?Restricting how many requests a client can make within a time window. Prevents brute-force attacks and protects your API from being overwhelmed., attackers can brute force passwords by trying thousands of combinations.
Quick reference: password security
| Do | Don't |
|---|---|
| Hash with bcrypt or Argon2 | Store plain text passwords |
| Use unique salts | Reuse salts across users |
| Validate password strength | Allow weak passwords like "123456" |
| Return generic error messages | Reveal if email exists |
| Rate limit login attempts | Allow unlimited brute force |
| Log attempts without passwords | Log actual passwords |
Password hashingWhat is hashing?A one-way mathematical transformation that turns data (like a password) into a fixed-length string that can't be reversed. Used to store passwords securely. isn't optional, it's fundamental. Get this wrong, and you're one data breach away from catastrophic consequences. Get it right, and even a complete database compromise won't expose your users' passwords.