State, Sessions, and Authentication
Handle user state securely and implement authentication and authorization flows.
Content
Cookies and storage options
Versions:
Watch & Learn
AI-discovered learning video
Cookies and Storage Options — Where Web Apps Remember Stuff (Without Becoming Creepy)
"Databases remember the universe; cookies remember your coffee order." — Probably a very optimistic browser
You've already learned how to model, migrate, and validate persistent data with SQL and ORMs, and how caching and fixtures help during development. Great — now let's talk about the smaller, sneakier place where web apps keep state: the browser and session layer. This is where sessions, authentication, and quick preferences live — and where developers either become security heroes or accidentally invent new classes of bugs.
Why this matters (quick, practical):
- Your database holds canonical data (users, posts, orders). Your cookies and storage hold state that's fast, personal to a browser/session, or used to authenticate a user.
- Choosing the wrong place to store something (like putting a long secret token in localStorage) is an invite for Cross-Site Scripting (XSS) disasters.
- Server-side sessions, client-side tokens, and browser storage each trade off security, persistence, and performance.
Overview: the main options
1) Cookies (HTTP cookies)
- What: small key/value pairs the browser sends automatically on matching requests.
- Where stored: browser-managed cookie jar.
- Common uses: session identifiers, CSRF tokens, preferences.
- Size: ~4 KB per cookie (varies). Many browsers cap total cookies per domain.
Important cookie attributes:
- HttpOnly — JavaScript can't read this. Use for session IDs or sensitive tokens to protect from XSS.
- Secure — sent only over HTTPS. Don't send auth cookies over plain HTTP.
- SameSite (Lax/Strict/None) — controls cross-site sending (CSRF mitigation).
- Expires/Max-Age — controls persistence (session cookie vs persistent cookie).
- Domain/Path — scoping.
Example (Flask):
from flask import make_response
resp = make_response('logged in')
# set HttpOnly, Secure, SameSite
resp.set_cookie('session_id', value=session_id, httponly=True, secure=True, samesite='Lax')
return resp
Example (JS - read/write, but can't access HttpOnly):
// Write a non-HttpOnly cookie (not recommended for secrets)
document.cookie = 'theme=dark; Max-Age=31536000; Path=/';
// Read (naive):
console.log(document.cookie);
2) localStorage / sessionStorage (Web Storage API)
- What: simple synchronous key/value store accessible via JavaScript.
- localStorage persists across browser sessions until explicitly cleared.
- sessionStorage lasts for the browser tab lifetime.
- Size: ~5–10 MB depending on browser.
- Usage: UI preferences, caching non-sensitive state, small offline-friendly data.
Pro: easy to use, larger size. Con: accessible to JS, so vulnerable to XSS. Do not store secrets or auth tokens here unless you accept major risk.
Example:
// Save
localStorage.setItem('draft', JSON.stringify({body: 'hello'}));
// Read
const draft = JSON.parse(localStorage.getItem('draft'));
3) IndexedDB
- What: client-side NoSQL DB for large structured data (files, blobs).
- Usage: complex offline apps (PWA), large caches.
- Security: still accessible by JS — vulnerable to XSS.
4) Server-side sessions (session store + session cookie)
- Pattern: cookie holds a session ID. Server stores session data (in DB, Redis, or memory) keyed by that ID.
- Pros: sensitive data never exposed to client; easy invalidation and rotation server-side.
- Cons: server state to manage; scaling needs (Redis) for many users.
This links back to your ORM and caching work: sessions are often persisted in a DB or in-memory cache (Redis). Use the caching strategies you learned to keep session lookups fast and avoid unnecessary DB hits.
5) JSON Web Tokens (JWTs)
- What: self-contained signed tokens carrying claims (user id, exp, etc.).
- Where to store: either in HttpOnly cookies or localStorage (or elsewhere).
- Tradeoffs: storing JWTs in localStorage makes them vulnerable to XSS; storing them in cookies exposes them to CSRF unless SameSite or CSRF tokens are used.
Quick comparison table
| Option | Persistence | Accessible by JS | Good for | Security concerns |
|---|---|---|---|---|
| HttpOnly Cookie (session id) | session or persistent | no | Auth session id | CSRF if SameSite lax/none + no CSRF token |
| localStorage | persistent | yes | UI state, large non-sensitive data | XSS theft |
| sessionStorage | tab-lifetime | yes | tab-specific state | XSS theft |
| IndexedDB | persistent | yes | large offline data | XSS theft |
| JWT in cookie | persistent | optional (if HttpOnly) | stateless auth | CSRF if not SameSite/CSRF token |
| JWT in localStorage | persistent | yes | SPA auth | XSS theft |
Practical rules (the stuff you actually use in projects)
- Never store raw secrets or long-lived tokens in localStorage or sessionStorage. If an attacker can run JS in your page, they can steal them.
- Prefer HttpOnly, Secure cookies for session IDs or auth tokens, and enable SameSite=Lax or Strict where practical. Use CSRF tokens for state-changing requests if you must support cross-site flows.
- Store minimal data in cookies — only an opaque session id or short metadata. Let the server (DB/Redis) hold the rest. This keeps requests small and safe.
- Use IndexedDB for large offline caches, but still avoid sensitive stuff there.
- Validate everything server-side — even if you trust a cookie or token, always validate and check permissions. This is where your validation and constraints knowledge from the ORM section really helps.
- Log out/invalidation: with server-side sessions you can invalidate a session id immediately; with stateless JWTs you need revocation lists (DB), which brings you back to the ORM + caching patterns you already know.
Example flow: secure login (recommended simple pattern)
- Client submits credentials via HTTPS.
- Server verifies credentials (ORM query + validation).
- Server creates a session object in Redis/DB and returns a HttpOnly, Secure, SameSite=Lax cookie containing the session id.
- Client relies on the cookie for authenticated requests (browser sends it automatically).
- To log out, server deletes the session in Redis/DB.
This gives you immediate server control (useful for logout, forced logout, and audit logging). Use caching techniques to keep session lookups fast.
Common pitfalls & debugging tips
- "My cookie isn’t set": check domain, path, Secure vs HTTP, and whether the response is cross-site (SameSite rules apply).
- Double-encoded tokens and size-overflow errors: cookies are small — if you find yourself stuffing user profiles into cookies, stop. Use server-side sessions or cache.
- You get a CSRF exploit: enable SameSite and implement CSRF tokens for forms/APIs.
- You get an XSS exploit: sanitize inputs, use CSP, and move secrets to HttpOnly cookies.
Key takeaways — what to remember after you close this tab
- Cookies = browser-managed, great for auth via session ids when combined with HttpOnly/Secure/SameSite.
- localStorage/sessionStorage/IndexedDB = JS-accessible, bigger capacity, not for secrets.
- Server-side sessions + cookie session id = safe, controllable, scalable if you use Redis and caching strategies.
- JWTs are convenient, but storage choice changes the security model. No free lunch.
"Treat browser storage like a whiteboard: great for scribbles (UI state), terrible for secret blueprints (auth tokens)."
If you're building the next feature in your CS50 project: store user profile and permissions server-side (ORM), set a short-lived HttpOnly session cookie, and cache session lookups for speed. Use localStorage only for harmless UI flourishes. Your future self (and your users) will thank you.
Want a short checklist to paste into your README? Say so and I'll write one you can copy-paste into every new project setup.
Comments (0)
Please sign in to leave a comment.
No comments yet. Be the first to comment!