"Logged in" can mean wildly different things depending on what is in the cookie. Three patterns dominate, and each has a distinct failure mode.
| Pattern | What is in the cookie | Server stores | Revocation |
|---|---|---|---|
| Session cookie | Random opaque token (e.g. 32 hex chars) | Mapping token → user_id, expires_at, ip in DB | Delete the row — instant |
| JWT (signed, stateless) | Self-describing JSON + signature | Nothing | Cannot truly revoke until expiry; needs a denylist |
| Bearer token (API key) | Random string in Authorization header | DB row + scopes | Delete the row |
Why sessions win for solo apps
The PIN layer in this quest uses session cookies in a SQLite table. Reasons:
- Instant revocation. "Revoke All" is one SQL
DELETE. JWT requires a denylist that defeats the whole stateless point. - Tied to IP. Each session row stores the IP it was issued to; mismatched IP = invalid. Free anti-replay.
- Visible in admin. You can list active sessions, see when they were issued, where from, when they expire.
- Tiny. No external dependency; SQLite ships with Python.
Cookie flags that matter
| Flag | Effect | Default stance |
|---|---|---|
HttpOnly | JavaScript cannot read the cookie | Always on — blocks XSS theft |
Secure | Only sent over HTTPS | On in production; off only for localhost |
SameSite=Strict | Cookie not sent on cross-site requests | On for solo apps — you don't need cross-site flow |
Max-Age | Lifetime in seconds | Bounded — e.g. 30 days, never "infinite" |