# Security — threat model of the auth flows

Scope: the authentication surface (registration, activation, login, refresh,
logout, password reset) and the token/session model. Framing is STRIDE, focused on
the concrete threats these flows face and how the implementation mitigates them.

## Assets & trust boundaries

- **Assets**: user credentials (Argon2id hashes), access tokens (JWT), refresh
  tokens (opaque, hashed), one-time tokens (activation/reset), the JWT signing
  secret, the TLS private key.
- **Boundaries**: client ↔ app (TLS on 4455), app ↔ Postgres (compose network),
  app ↔ SMTP. Secrets live only in the environment / a mode-600 `.env`, never in
  the image, repo, compose files, or logs.

## Threats & mitigations

### Account enumeration (Information disclosure)
`register`, `password/forgot`, and `activation/resend` return an **identical**
response regardless of whether the email exists, and never send a "you already have
an account" signal. `login` returns the same `401 INVALID_CREDENTIALS` for an
unknown email and a wrong password, and runs a **dummy Argon2 verify** on the
unknown-email path to equalize timing. Email uniqueness is enforced
case-insensitively.

### Credential theft / offline cracking (Information disclosure)
Passwords are stored only as **Argon2id** hashes (19 MiB, t=2, p=1) behind a
`DelegatingPasswordEncoder` (self-describing, migratable). A password policy
(≥12 chars + common-password deny list) raises the floor. The `password` column is
documented as a hash; `/users/me` and every DTO exclude it (asserted in tests).

### Brute force / credential stuffing (Spoofing, DoS)
Per-account **lockout** after 5 failed logins for 15 minutes, plus an in-memory
**token-bucket rate limiter** on `/auth/**` (per client IP). The lockout counter is
persisted even on the failing request (`noRollbackFor`). Residual risk: the rate
limiter is per-instance (single-instance deployment); a distributed limit needs a
shared store.

### Token forgery / tampering (Spoofing, Tampering)
Access tokens are **HS256 JWTs**; the resource server validates signature, `exp`,
issuer, and audience — a tampered, expired, or wrong-issuer/audience token is
rejected (401), covered by the authorization-matrix test. The signing secret is
≥32 bytes, env-only, and validated at startup.

### Refresh-token theft & replay (Spoofing, Elevation)
Refresh tokens are **opaque, 32-byte random, stored only as SHA-256 hashes**, and
**single-use with rotation**. Presenting an already-rotated token is treated as
theft: the whole chain for that user is revoked (`TOKEN_REUSE_DETECTED`). Password
reset and logout revoke tokens (all / one). A stolen refresh token is therefore
usable at most until the legitimate client's next refresh, which trips detection.

### One-time-token abuse (Tampering, Replay)
Activation/reset tokens are random, delivered only by email, **stored as hashes**
(hash-and-lookup, no plaintext, no per-char compare), **single-use**, and expiring
(activation 24h, reset 1h). Issuing a new one invalidates prior active tokens.
Reset additionally revokes all refresh tokens, killing existing sessions.

### Transport & session (Tampering, Information disclosure)
TLS terminates at the app (leaf + intermediate served; ADR-0005). Sessions are
**stateless** (no cookies), CSRF disabled accordingly, CORS is deny-by-default and
env-configurable, security headers set (frame-deny, referrer policy), and **HSTS is
enabled in prod**. Errors are uniform RFC 7807 problems that don't leak internals;
unexpected exceptions return an opaque 500 (logged server-side with the requestId).

### Repudiation
Auth events (login success/failure, lockout, reset requested) are logged with the
user id / hashed email and a per-request `X-Request-Id` — no plaintext passwords or
token material is ever logged.

## Residual risks / future work

- **Single-instance rate limiting** — move to a shared store (Redis) for a global
  limit behind multiple replicas.
- **No live TLS-bundle reload** — rotation requires a restart (ADR-0005).
- **Common-password list is illustrative** — back it with a breached-password
  service (e.g. HIBP k-anonymity) in production.
- **No MFA** and no device/session management UI — out of scope for this service.
- **Email is a trusted channel** — activation/reset security assumes the user's
  mailbox is not compromised.
