# Patterns — Implementing Authentication ## Contents - Password hashing (argon2id) - Session cookie setup - JWT issuance and verification - OAuth2 authorization code + PKCE - Password reset flow - TOTP MFA - Gotchas ## Password hashing (argon2id) ```python from argon2 import PasswordHasher from argon2.exceptions import VerifyMismatchError ph = PasswordHasher() # library defaults follow current OWASP guidance def hash_password(password: str) -> str: return ph.hash(password) def verify_password(stored: str, candidate: str) -> bool: try: ph.verify(stored, candidate) except VerifyMismatchError: return False # Transparent upgrade if parameters changed since hashing if ph.check_needs_rehash(stored): return True # caller should rehash and store return True ``` Fallback: `bcrypt.hashpw(pw, bcrypt.gensalt(rounds=12))` — bcrypt truncates at 72 bytes; reject longer passwords explicitly. ## Session cookie setup ```python # Flask example — equivalents exist in every framework app.config.update( SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SECURE=True, SESSION_COOKIE_SAMESITE="Lax", PERMANENT_SESSION_LIFETIME=timedelta(hours=12), ) # On every privilege change (login, MFA pass): session.regenerate() # or: logout_user(); new session id — blocks fixation ``` Store sessions server-side (Redis/DB) so logout and admin revocation are real. ## JWT issuance and verification ```python import jwt, datetime as dt ACCESS_TTL = dt.timedelta(minutes=15) # short: leaked tokens age out fast LEEWAY = 60 # seconds of clock-skew tolerance, max def issue(sub: str, secret: str) -> str: now = dt.datetime.now(dt.timezone.utc) return jwt.encode( {"sub": sub, "iat": now, "exp": now + ACCESS_TTL, "iss": "api"}, secret, algorithm="HS256") def verify(token: str, secret: str) -> dict: # Pin the algorithm list — never accept the header's alg claim blindly return jwt.decode(token, secret, algorithms=["HS256"], issuer="api", leeway=LEEWAY) ``` Refresh tokens: opaque random strings, stored hashed, rotated on every use; reuse of a rotated token revokes the whole family (theft signal). ## OAuth2 authorization code + PKCE ```python # authlib example (FastAPI/Starlette) oauth.register( name="google", server_metadata_url="https://accounts.google.com/.well-known/openid-configuration", client_id=..., client_secret=..., client_kwargs={"scope": "openid email profile", "code_challenge_method": "S256"}, ) # Callback: verify state, exchange code, then check id_token claims: # - aud == your client_id # - email_verified is True before trusting the email ``` ## Password reset flow ```python def start_reset(email: str): user = find_user(email) if user: raw = secrets.token_urlsafe(32) store_reset(user.id, sha256(raw), expires=now() + timedelta(hours=1)) send_email(email, link_with(raw)) return "If that address exists, we sent a link." # identical either way def finish_reset(raw: str, new_password: str): row = pop_reset(sha256(raw)) # single-use: delete on read if row is None or row.expired(): raise ResetInvalid # generic error, no detail set_password(row.user_id, hash_password(new_password)) revoke_all_sessions(row.user_id) # kill attacker's live sessions ``` ## TOTP MFA ```python import pyotp secret = pyotp.random_base32() # store encrypted, show QR once totp = pyotp.TOTP(secret) ok = totp.verify(code, valid_window=1) # ±30 s window, no more # Recovery codes: 8-10 random codes, stored hashed, single-use. ``` ## Gotchas - **`alg: none` / algorithm confusion** — always pin `algorithms=[...]` when decoding JWTs; never trust the token header. - **bcrypt 72-byte truncation** — `password[:72]` collisions; validate length or pre-hash with SHA-256+base64 before bcrypt. - **SameSite=Lax still sends cookies on top-level GET navigation** — state-changing endpoints must be POST with CSRF protection. - **Timing side channel on login** — run the hash verify even when the user doesn't exist (hash a dummy) so response time doesn't reveal account existence. - **JWT logout is not logout** — without a revocation list or short TTL, "logged out" tokens keep working until expiry. - **OAuth `state` skipped** — omitting the state check re-opens CSRF on the callback; PKCE does not replace it for web apps. - **Storing TOTP secrets in plaintext** — encrypt at rest; a DB dump otherwise defeats MFA entirely.