Back to Cheat Sheets

🪪 Secure Digital Identities

OWASP Top 10 Proactive Controls (2024) - C7

CORE CONTROL

📋 What Is This Control?

Secure Digital Identities is the proactive control that says: implement identity, authentication, and session management deliberately and correctly, and match the strength of those mechanisms to the risk of the thing they protect. It covers authentication factors, credential storage, MFA, session lifecycle, token handling, and recovery. Authentication is the gate in front of everything else - if identity can be forged, guessed, replayed, or stolen, every downstream control is operating on a lie.

C7 Proactive Control
Authentication Focus Area
OWASP 2024 Proactive Controls

🎯 What It Prevents

  • Account takeover (ATO): attackers operating as legitimate users
  • Credential stuffing: reused breach passwords replayed at scale against login endpoints
  • Session hijacking & fixation: a stolen or non-rotated session token grants full access without the password
  • Privilege escalation: a session not regenerated on privilege change inherits high-value rights
  • Token forgery: broken JWT verification (alg:none, unchecked signature) mints arbitrary identities
  • SIM-swap / SMS OTP interception of weak second factors
  • User enumeration: distinct responses reveal which accounts exist
  • Federation abuse: misconfigured OAuth2/OIDC (open redirects, missing state) turns SSO into a bypass

🧭 How It Works

1. Match assurance to risk (NIST AAL1/2/3)
pick the level from the action's sensitivity
↓
2. Passwords: length-first policy, breach-screen,
store with Argon2id/bcrypt/scrypt (per-user salt)
↓
3. Enforce MFA server-side; prefer passkeys/WebAuthn
↓
4. Sessions: CSPRNG IDs, regenerate on login &
privilege change, HttpOnly+Secure+SameSite, timeouts
↓
5. Verify tokens fully; step up for sensitive actions

❌ Without This Control

# Storage: fast, unsalted hash - a stolen DB is a stolen password list hashed = hashlib.md5(password.encode()).hexdigest() ← MD5, no salt! # No breach screening, forced composition + rotation (complexity theatre) # drives users to Spring2024! -> Summer2024! // Session never regenerated on login - session fixation app.post('/login', async (req, res) => { const user = await authenticate(req.body); // password only, no MFA req.session.userId = user.id; ← same session id kept! res.json({ ok: true }); }); // Cookie with no protective flags - readable by any XSS, sent cross-site Set-Cookie: session=abc123 // JWT "verified" without pinning the algorithm - accepts alg:none jwt.verify(token, key); ← no algorithms/aud/iss/exp checks # Enumerable reset: "no such user" vs "wrong password" leaks who exists

✅ Applying the Control

# Storage: Argon2id (slow, per-user salt handled internally) from argon2 import PasswordHasher ph = PasswordHasher() hashed = ph.hash(password) # screen new passwords vs a breach corpus // Session: regenerate a NEW id on login - kills fixation app.post('/login', async (req, res) => { const user = await authenticate(req.body); // password + MFA verified req.session.regenerate(err => { req.session.userId = user.id; res.json({ ok: true }); }); }); app.post('/logout', (req, res) => req.session.destroy(() => res.clearCookie('sid').json({ ok: true }))); // server-side kill // Cookie: every protective flag + __Host- prefix Set-Cookie: __Host-session=VALUE; HttpOnly; Secure; SameSite=Lax; Path=/ // JWT: pin the algorithm allow-list, verify aud/iss/exp every request jwt.verify(token, PUBLIC_KEY, { algorithms: ['RS256'], // 'none' can never match audience: 'api.example.com', issuer: 'https://idp.example.com' }); # Anti-enumeration: identical response for valid/invalid accounts # "If an account exists, a reset link has been sent."

✓ Implementation Checklist

  • Each action mapped to a NIST AAL; sensitive actions require step-up
  • Password policy is length-first, breach-screened, no forced rotation
  • Passwords stored with Argon2id/bcrypt/scrypt (per-user salt)
  • MFA enforced server-side; phishing-resistant factors preferred
  • Session IDs are CSPRNG; regenerated on login and privilege change
  • Cookies set HttpOnly; Secure; SameSite; idle + absolute timeouts
  • Logout invalidates the session server-side
  • JWTs verify signature, algorithm allow-list, exp, and aud; short-lived + rotated
  • OAuth2/OIDC uses Auth Code + PKCE, validated state, exact redirect URIs
  • Rate limiting / lockout on auth endpoints; anomaly alerting in place
  • Reset tokens are single-use, hashed, time-limited; responses non-enumerable

🔍 Tools & Techniques

Standards & Factors:

NIST SP 800-63B Passkeys / WebAuthn FIDO2 TOTP (pyotp) OAuth2 + PKCE OIDC

Libraries & Storage:

Argon2id bcrypt Pwned Passwords API express-rate-limit Vault / KMS

How to Verify:

  • Confirm the session ID changes on login (check before/after)
  • Test that logout invalidates the session server-side, not just the cookie
  • Submit a JWT with alg:none - it must be rejected
  • Compare login/reset responses for valid vs invalid accounts (must match)
  • Verify cookies carry HttpOnly, Secure, and SameSite
  • Confirm rate limiting fires on repeated failed logins

🌍 Why It Matters

These are documented incident classes - recurring patterns rather than any single fabricated breach or CVE.

  • Credential stuffing at scale: attackers replay username/password pairs leaked from unrelated breaches, betting on reuse. MFA breaks "password alone is enough," breach-corpus screening rejects known-compromised passwords, and rate limiting makes high-volume replay expensive and visible.
  • SIM-swap & SMS OTP interception: an attacker ports a victim's number or relays codes via a phishing site. Origin-bound passkeys/WebAuthn remove the shared secret an attacker can relay.
  • Session fixation & non-rotated sessions: the app keeps the same session ID before and after login. Regenerating on login and truly invalidating on logout makes a captured pre-auth ID worthless.
  • JWT alg:none / signature confusion: a service accepts a token claiming no signature or verifies RS256 as HS256. Pinning an algorithm allow-list and checking signature, exp, and aud makes forged tokens fail.

📌 Quick Tips

  • DO NOT store passwords with MD5/SHA-1 or without a salt
  • DO NOT accept alg:none or skip token verification
  • DO NOT reveal whether an account exists
  • DO favor length and breach screening over complexity rules
  • DO prefer phishing-resistant MFA (passkeys > TOTP > SMS)
  • DO regenerate sessions and invalidate server-side on logout
  • DO step up (re-auth) for sensitive actions