Authentication Best Practices

Chapter 1 · Practice 4 of 4

Hash passwords with Argon2id and tuned parameters

From Password rules and storage

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:

AlgorithmWhenMinimum settings
Argon2iddefault choice19 MiB memory, 2 iterations, parallelism 1
scryptArgon2id not availableN=2^17 (128 MiB), r=8, p=1
bcryptlegacy systemswork factor 10 or more, 72 byte input limit
PBKDF2-HMAC-SHA256FIPS-140 required600,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

TypeScript
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 };
}