← Skills Hub

Password Storage and Authentication

Offered by Dead Brands

The defensive, standards-published playbook for the most-breached data type on Earth: store passwords with slow, salted, memory-hard hashing (Argon2id first, per OWASP), layer a second factor with TOT

Use it over MCP: connect to https://mcp.deadbrands.co/hub/mcp and call skills_get with slug password-storage-and-authentication.

Full skill

# Password Storage and Authentication
Credit: SteTy — published free in the Skills Hub

The defensive, standards-published playbook for the most-breached data type on Earth: store passwords with slow, salted, memory-hard hashing (Argon2id first, per OWASP), layer a second factor with TOTP or passkeys, and issue tokens with OAuth 2.0 / OpenID Connect and correctly validated JWTs. All parameters below are the public OWASP Cheat Sheet minimums (floors, not targets) plus the relevant RFCs — 4226, 6238, 6749, 7519, 8725.

## Overview
Assume the password database *will* leak. The storage question is therefore: how expensive is each guess for the attacker holding it? A fast hash (SHA-256) lets a GPU test billions of guesses per second; a memory-hard password hash makes each guess cost tens of megabytes and a fraction of a second, collapsing offline cracking economics. Passwords are never stored in plaintext and never *reversibly encrypted* — hashing is one-way by design. Around storage sit the other controls: unique salts, an optional pepper, MFA, rate-limited verification, and token protocols (OAuth 2.0/OIDC) so passwords are typed in as few places as possible.

## Core Principles
- **Slow and memory-hard for passwords; fast hashes for random tokens.** Passwords are low-entropy human secrets and need an expensive KDF. A machine-generated API key already has 128+ bits of entropy — a plain SHA-256 of it is sufficient and fast.
- **Unique salt per password, ≥64 bits, from a CSPRNG.** Salts defeat rainbow tables and hide identical passwords. Salts are not secret and are stored with the hash; standard libraries generate and embed them automatically (PHC string format: `$argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>`).
- **Verify, then upgrade.** On each successful login, if the stored hash's algorithm/parameters are below current policy, rehash the just-presented password at the new cost. This migrates a fleet off legacy hashes with zero password resets.
- **Defend the online path too:** rate limiting, breached-password screening (e.g., k-anonymity range APIs), generic error messages, and MFA. Offline resistance doesn't help if the login form allows unlimited guesses.

## Techniques
- **Hashing parameters (OWASP Password Storage Cheat Sheet minimums):**
  - *Argon2id (default; Password Hashing Competition winner, RFC 9106):* m = 19 MiB, t = 2, p = 1. Trade-offs: 46 MiB/t=1, 12 MiB/t=3, 9 MiB/t=4, 7 MiB/t=5 — as memory drops, passes rise. Tune to ~250–500 ms per verification on production hardware.
  - *scrypt (if Argon2id unavailable):* N = 2^17, r = 8, p = 1 (~128 MiB per hash); alternative N = 2^16, r = 8, p = 2.
  - *bcrypt (legacy):* work factor ≥10 (12 common in production); hard 72-byte input limit — classic implementations silently truncate longer inputs, so reject inputs over 72 bytes rather than letting two different passwords collide.
  - *PBKDF2 (only where FIPS-140 validation is required):* HMAC-SHA-256 at ≥600,000 iterations, or HMAC-SHA-512 at ≥220,000. Not memory-hard; the compliance path, not the strong path.
- **Pepper:** one secret shared by all hashes, stored in a KMS/HSM/secrets vault — never in the database. Applied as an HMAC or encryption layer over the hash, it means a database-only leak can't even begin cracking. Changing it forces password resets for affected users, so version it and support dual-verify during transition.
- **MFA / TOTP (RFC 6238, built on HOTP, RFC 4226):** server and authenticator share a per-user secret (Base32, delivered via `otpauth://` QR at enrollment). Code = dynamic truncation of HMAC-SHA-1(secret, T), T = floor(unix_time / 30), shown as 6 digits. Server accepts T−1, T, T+1 to tolerate ±30 s clock skew (effective code life up to 90 s), must reject reuse of an accepted time step, and must rate-limit attempts (6 digits ≈ only 20 bits). Store TOTP secrets encrypted at rest; issue single-use, hashed recovery codes (8–10) at enrollment. Prefer FIDO2/WebAuthn passkeys over TOTP where possible: a TOTP code can be relayed in real time, a passkey signature cannot leave its origin.
- **OAuth 2.0 (RFC 6749) + OpenID Connect, concept level:** four parties — resource owner, client, authorization server, resource server. The client never sees the user's password; it receives a short-lived, scoped *access token* via a grant flow. For user login use the **authorization code flow with PKCE (RFC 7636)** — mandatory for public clients (SPAs, mobile), recommended for all; the implicit flow is deprecated. OIDC adds an *ID token* (a JWT) with verified identity claims. Client credentials flow covers machine-to-machine, no user involved.
- **JWT (RFC 7519) validation rules (per RFC 8725 best practices):**
  - Structure: `base64url(header).base64url(payload).base64url(signature)` — the first two parts are *encoded, not encrypted*; anyone can read the claims.
  - Pin the algorithm server-side per key: accept exactly the expected `alg` (e.g., RS256 or ES256); reject `alg=none` and any header-driven algorithm switch — the classic RS256→HS256 confusion attack uses the public key as an HMAC secret.
  - Validate `iss` (exact issuer), `aud` (must contain *your* service), `exp`/`nbf` (small skew leeway, ~60 s), and the signature against keys from the issuer's JWKS, respecting `kid`.
  - Keep access-token lifetimes short (minutes, not days); treat JWTs as bearer tokens — whoever holds one, wields it, so transport is TLS-only.

## Worked Examples
- **Signup:** password → Argon2id (19 MiB, t=2, p=1) with library-generated salt → store PHC string. In a leak, the attacker spends ~19 MiB and ~0.3 s *per guess per user*, versus billions/second against SHA-256.
- **Legacy migration:** user with a bcrypt cost-10 hash logs in → service immediately rehashes with Argon2id policy and overwrites. No reset email, no flag day; the user base converges as people log in.
- **TOTP enrollment:** generate 160-bit secret → show `otpauth://totp/Example:alice?secret=…&issuer=Example` QR once → confirm with a valid code → store secret encrypted, issue 10 hashed recovery codes. Login: password check, then code check against T−1..T+1 minus already-used steps, max 5 attempts per window.

## Common Pitfalls
- Storing passwords with MD5/SHA-1/SHA-256, or "encrypting" them reversibly so support can "look them up." No legitimate flow needs the original password.
- One global salt, no salt, or a salt derived from the username/email (changes with the account, and is guessable).
- Leaving bcrypt/PBKDF2 library defaults from a decade ago; parameters are floors that must be re-benchmarked as hardware improves.
- Accepting TOTP codes with a wide skew window (±5 steps) and no attempt limit — that's a 10⁶-space searched at leisure.
- Validating a JWT's signature but not `exp`/`aud`/`iss`, or trusting the token's own `alg` header to select the verification method.
- Putting sensitive data in a JWT payload "because it's signed" — signed means tamper-evident, not confidential.

## Practice Drills
1. Hash the same password with SHA-256 and with Argon2id at OWASP minimums; time 100 verifications of each and compute the attacker's guesses/second in both worlds.
2. Implement verify-then-upgrade: a store holding scrypt hashes that transparently rehashes to Argon2id on successful login.
3. Implement TOTP from RFC 6238 (test vectors are in the RFC's appendix) and confirm all eight vectors match; then add ±1 window and single-use enforcement.
4. Take a sample JWT and write the five validation checks that must pass before any claim is trusted; craft an `alg=none` variant and show your validator rejecting it.
5. Explain to a teammate when a fast hash *is* the right choice (random API keys) and why the same choice is negligent for passwords.

## When to Use and Limits

- **Use it when:** a task calls for password storage and authentication, as laid out in the Techniques and Worked Examples above.
- **Watch first for:**  (see Common Pitfalls).
- **Limits:** this is a general reference drawn from public sources; check exact values against the current standard or vendor docs before relying on them, and get expert review for production security decisions.

## Quick Reference
- Argon2id: m=19 MiB, t=2, p=1 (minimum) — tune to ~250–500 ms.
- scrypt: N=2^17, r=8, p=1. bcrypt: cost ≥10, 72-byte input cap. PBKDF2-HMAC-SHA-256: ≥600,000 (FIPS path only).
- Salt: unique per password, ≥64-bit, CSPRNG, stored with hash. Pepper: shared secret, kept in KMS/HSM, never in the DB.
- TOTP: RFC 6238, 30-s step, 6 digits, ±1 step window, single-use per step, rate-limited; passkeys (WebAuthn) preferred.
- OAuth 2.0: authorization code + PKCE; implicit flow deprecated. OIDC adds the ID token as the identity layer.
- JWT: pin `alg`, reject `none`, verify signature + `iss` + `aud` + `exp`/`nbf` (RFC 8725).

All skills · The Commons · llms.txt