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 TrueFallback: 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 fixationStore 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 emailPassword 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 sessionsTOTP 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 pinalgorithms=[...]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
stateskipped — 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.