Chapter 1 · Practice 4 of 4
Hash passwords with Argon2id and tuned parameters
Why it matters
Store passwords with a slow, salted, memory-hard hash so that a leaked table takes enormous effort to crack. OWASP's Password Storage Cheat Sheet gives these minimums:
| Algorithm | When | Minimum settings |
|---|---|---|
| Argon2id | default choice | 19 MiB memory, 2 iterations, parallelism 1 |
| scrypt | Argon2id not available | N=2^17 (128 MiB), r=8, p=1 |
| bcrypt | legacy systems | work factor 10 or more, 72 byte input limit |
| PBKDF2-HMAC-SHA256 | FIPS-140 required | 600,000 iterations |
Libraries generate a unique salt per password and store it inside the hash string, so you do not manage salts yourself.
A pepper (a secret key kept outside the database, in a vault or HSM) adds defense in depth: a stolen database alone is not enough to start cracking. OWASP notes a pepper cannot be rotated without users resetting their passwords, so plan for that before you add one.
When you raise parameters or move from bcrypt to Argon2id, rehash on the next successful login: verify with the old hash, then store a new one.
Avoid
- MD5, SHA-1 or SHA-256 on their own for passwords
- Encrypting passwords so they can be decrypted
- One shared salt for every user
Example
import argon2 from 'argon2';
const OPTIONS = { type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1 };
export const hashPassword = (pw: string) => argon2.hash(pw, OPTIONS);
export async function verifyPassword(stored: string, pw: string) {
const ok = await argon2.verify(stored, pw);
// upgrade old or weaker hashes after a successful login
const rehash = ok && argon2.needsRehash(stored, OPTIONS);
return { ok, rehash };
}