Password Hashing (argon2/bcrypt)
Never store plaintext passwords; never hash them with a fast hash (SHA-256, MD5). Use a memory-hard, slow KDF: argon2id, scrypt, or bcrypt. Tune cost to ~250ms per hash on your hardware.
argon2id (Node, Python), upgrade path
EXAMPLE
// 1) Node — argon2 (npm i argon2)
import argon2 from 'argon2';
// Hash on signup
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19 * 1024, // 19 MB — OWASP 2024 minimum
timeCost: 2,
parallelism: 1,
});
await db.users.update({ id }, { password_hash: hash });
// Verify on login — constant-time comparison built in
const ok = await argon2.verify(stored.password_hash, attempt);
// Re-hash if params changed (e.g. you raised memoryCost)
if (ok && argon2.needsRehash(stored.password_hash, { memoryCost: 64 * 1024 })) {
const fresh = await argon2.hash(attempt, { memoryCost: 64 * 1024 });
await db.users.update({ id }, { password_hash: fresh });
}
// 2) Python — argon2-cffi
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(memory_cost=19 * 1024, time_cost=2, parallelism=1)
hashed = ph.hash(password)
try:
ph.verify(hashed, attempt)
if ph.check_needs_rehash(hashed):
new_hash = ph.hash(attempt)
except VerifyMismatchError:
raise InvalidLogin()
// 3) Why NOT a plain hash
// SHA-256(pw) is ~10ns. A laptop GPU brute-forces 10^10 candidates per second.
// argon2id at memoryCost=19MB takes ~250ms. Same GPU: 4 attempts/sec.
// 4) Length requirements (NIST SP 800-63B)
// Minimum 8 chars, allow up to 64+, allow all printable Unicode,
// prohibit common passwords (top 100k), DO NOT require complex composition rules.
// 5) Check against the leaks corpus — HaveIBeenPwned range API
import crypto from 'crypto';
const sha1 = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();
const prefix = sha1.slice(0, 5);
const suffix = sha1.slice(5);
const r = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
const found = (await r.text()).split('\n').some(line => line.startsWith(suffix));
if (found) throw new Error('Password is in a known breach corpus');
// 6) Account-takeover defences AROUND the password
// • Rate limit /login per IP + per account
// • Lock account / require captcha after N failures
// • Email on new device / IP / failed attempts
// • MFA (TOTP, WebAuthn) for sensitive accounts
// • Never log passwords (even hashed) in request logs
// 7) When migrating from an old hash
// Verify with the old hash; if successful, re-hash with argon2id and update.
// Run for 6-12 months; force a password reset for users who haven't logged in.
Why it matters
Argon2id with OWASP’s 2024 parameters is the right default. Pair it with a HaveIBeenPwned check at signup, rate-limited login endpoints, and optional MFA — that combination locks down 99% of account-takeover attempts.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Use a slow, memory-hard KDF, not a hash.
import argon2 from 'argon2';
const hash = await argon2.hash(password, { type: argon2.argon2id });
await argon2.verify(hash, password);
// bcrypt cost 12+ is acceptable on legacy stacks.
Try it Yourself »
Exercise
Preferred password-hashing variant of Argon2.
argon2.hash(pw, { type: argon2.
});
Lowercase, ends in id.
Discussion
Loading…