Learning Goals
3 minBy the end of this lesson you can:
- Explain how session-cookie auth and token auth each keep a user logged in.
- Compare their trade-offs (revocation, scaling, storage, XSS/CSRF exposure).
- Set secure cookie flags: HttpOnly, Secure, SameSite.
- Avoid A07 auth failures: weak/guessable sessions, no rate limit, no logout.
Warm-Up · HTTP Has Amnesia
5 minEach HTTP request is independent — the server doesn't inherently know that this request comes from the same person who logged in a moment ago. So after you authenticate, the server gives you something to present on every later request that proves "I'm the one who logged in." That something is a session ID or a token.
Authentication has two phases: login (prove identity once, with a password — hashed per Lessons 12-13) and session management (carry that proof across stateless requests). The two main designs are server sessions (the server stores state, the client holds an opaque ID) and tokens (the proof itself is sent each time, often a signed JWT). Each has distinct security properties — and most A07 "auth failures" are about getting the session part wrong.
New Concept · Two Designs, Their Trade-offs
14 minDesign A: server-side sessions
1. login → server creates a session, stores {session_id: user} server-side
2. server sends a COOKIE containing only the opaque session_id
3. browser sends that cookie automatically on every request
4. server looks up the id → knows who you are
Logout = delete the server-side session. Revocation is instant.import secrets SESSIONS = {} # server-side store (in real apps: Redis/DB) def create_session(user_id: int) -> str: sid = secrets.token_urlsafe(32) # ✓ cryptographically random (L8-29) SESSIONS[sid] = {"user_id": user_id} return sid # → set as an HttpOnly cookie def whoami(sid: str): return SESSIONS.get(sid) # None if logged out / invalid
Design B: tokens (e.g. JWT)
1. login → server creates a SIGNED token containing the user's identity 2. client stores it and sends it (usually 'Authorization: Bearer <token>') 3. server VERIFIES the signature (no DB lookup) → trusts the contents The server stores NOTHING → scales easily, but revocation is HARD.
Tokens carry the identity inside themselves (signed so they can't be forged — Lesson 38). The server doesn't store session state, which scales well across many servers — but that's also the catch: you can't easily "delete" a token a user already holds.
The trade-offs
SESSIONS (cookie) TOKENS (JWT) server state yes (store per session) none (stateless) revocation instant (delete it) hard (valid until expiry) scaling needs shared store easy (any server verifies) where stored cookie (auto-sent) cookie OR localStorage main risk CSRF (auto-sent cookie) XSS (if in localStorage) + no revoke best for classic web apps APIs, microservices, mobile
Secure cookie flags (for either, when using cookies)
resp.set_cookie("session", sid, httponly=True, # JS can't read it → XSS can't steal it (L8-33) secure=True, # only sent over HTTPS → no plaintext leak samesite="Lax") # not sent on cross-site requests → CSRF defence
A common mistake: storing a JWT in localStorage — which JavaScript can read, so any XSS steals it. Storing the token in an HttpOnly cookie instead means XSS can't read it (but then you must handle CSRF with SameSite/CSRF tokens). There's no free lunch: cookies trade XSS-resistance for CSRF exposure; localStorage trades CSRF-immunity for XSS exposure. Prefer HttpOnly cookies + SameSite for browser apps.
The A07 auth failures to avoid
- Guessable session IDs — must be cryptographically random (
secrets), long, and unpredictable. - No rate limiting on login → brute force / credential stuffing (Lesson 25). Add lockout/backoff/CAPTCHA.
- No real logout / no expiry — sessions/tokens that live forever can't be revoked.
- Session fixation — regenerate the session ID on login so a pre-set ID can't be reused.
- Sessions over HTTP — sniffable (Lesson 24); always HTTPS + Secure cookies.
Worked Example · A Secure Session Layer
12 minGoal: a small Flask login with proper session management — random IDs, secure cookies, rate-limited login, session regeneration, and logout. The right way, end to end (passwords via bcrypt from Lesson 13).
import time, secrets, bcrypt from collections import defaultdict from flask import Flask, request, make_response app = Flask(__name__) SESSIONS = {} # sid -> {user_id, created} USERS = {"aisha": bcrypt.hashpw(b"demo-pw", bcrypt.gensalt())} # demo login_attempts = defaultdict(list) # ip -> [timestamps] def rate_limited(ip: str, limit=5, window=300) -> bool: now = time.time() login_attempts[ip] = [t for t in login_attempts[ip] if t > now - window] return len(login_attempts[ip]) >= limit # too many recent attempts? @app.post("/login") def login(): ip = request.remote_addr if rate_limited(ip): # A07: rate limit login return "too many attempts, try later", 429 login_attempts[ip].append(time.time()) user, pw = request.form["user"], request.form["pw"].encode() stored = USERS.get(user) # constant-work check whether or not the user exists (no enumeration) if not stored or not bcrypt.checkpw(pw, stored): return "invalid credentials", 401 sid = secrets.token_urlsafe(32) # ✓ random, unguessable SESSIONS[sid] = {"user": user, "created": time.time()} # regenerated on login resp = make_response("ok") resp.set_cookie("session", sid, httponly=True, secure=True, samesite="Lax") return resp @app.post("/logout") def logout(): sid = request.cookies.get("session") SESSIONS.pop(sid, None) # ✓ real server-side revocation resp = make_response("logged out") resp.delete_cookie("session") return resp def current_user(): sess = SESSIONS.get(request.cookies.get("session", "")) # optional: expire old sessions if sess and time.time() - sess["created"] > 86400: return None return sess["user"] if sess else None
POST /login (good creds) → 200, Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax POST /login x6 fast → 429 too many attempts (rate limited) POST /logout → session deleted server-side; cookie cleared GET protected after logout → current_user() is None
Read the code
Every A07 pitfall is addressed: session IDs are secrets.token_urlsafe (unguessable), the cookie is HttpOnly + Secure + SameSite (XSS can't read it, only HTTPS, CSRF-resistant), login is rate-limited (brute force throttled), a fresh ID is generated on each login (no fixation), logout truly revokes server-side, and old sessions expire. Passwords use bcrypt (Lesson 13), and the check does constant work whether the user exists or not (no enumeration). This is the secure baseline the Flask auth project (Lesson 41) builds on.
Try It Yourself
13 minImplement login → cookie → protected route → logout. Confirm the protected route works only with a valid session cookie and fails after logout.
Add the login rate limiter and confirm the 6th rapid attempt from one IP is blocked (429). Explain how this defeats credential stuffing (Lesson 25).
Hint
Track timestamps per IP in a sliding window; block when count ≥ limit. Real apps also add per-account lockout and CAPTCHA after repeated failures.
Implement the same auth two ways: token in an HttpOnly cookie vs. token in localStorage (front-end). Write up which is exposed to XSS, which to CSRF, and why HttpOnly cookies + SameSite are the safer default for browser apps.
Hint
localStorage token → readable by JS → XSS steals it (no CSRF risk: not auto-sent)
HttpOnly cookie → JS can't read it → XSS can't steal it (but auto-sent → CSRF;
mitigate with SameSite + CSRF tokens)
Browser apps: prefer HttpOnly cookie + SameSite=Lax/Strict.Mini-Challenge · An Auth-Hardening Audit
8 minWrite a checklist auditor for an auth implementation that verifies: session IDs use secrets (not random/predictable), cookies set HttpOnly+Secure+SameSite, login is rate-limited, logout revokes server-side, and sessions expire. Report pass/fail per item — the auth section of a security review.
Show a sample solution
import re from pathlib import Path def audit_auth(path: str) -> None: src = Path(path).read_text(encoding="utf-8") checks = { "random session IDs (secrets)": "secrets.token" in src and "random.randint" not in src, "HttpOnly cookie": "httponly=True" in src.lower(), "Secure cookie": "secure=True" in src.lower(), "SameSite cookie": "samesite" in src.lower(), "rate-limited login": "429" in src or "rate" in src.lower(), "server-side logout": ".pop(" in src or "del " in src, "session expiry": "created" in src or "expire" in src.lower(), } fails = [name for name, ok in checks.items() if not ok] for name, ok in checks.items(): print(f" [{'✓' if ok else '✗'}] {name}") print("\nauth hardening OK" if not fails else f"\n{len(fails)} gap(s) to fix") audit_auth("auth.py")
Non-negotiables: checks random IDs, cookie flags, rate limiting, real logout, and expiry — pass/fail report.
Recap
3 minHTTP is stateless, so after login the server hands the client something to present on every request. Sessions keep state server-side (the client holds an opaque, random ID in a cookie) — easy revocation, but needs a shared store and is CSRF-exposed. Tokens (JWT) carry signed identity in the request — stateless and scalable, but hard to revoke, and XSS-exposed if stored in localStorage. For browser apps, prefer HttpOnly + Secure + SameSite cookies. Avoid the A07 failures: guessable IDs (use secrets), no rate limiting (brute force), no real logout/expiry, session fixation, and auth over HTTP. Next: JWTs in depth.
Vocabulary Card
- session
- Server-stored login state; the client holds an opaque session ID.
- token (JWT)
- A self-contained, signed proof of identity sent on each request.
- cookie flags
- HttpOnly (no JS access), Secure (HTTPS only), SameSite (CSRF defence).
- session fixation
- Reusing a pre-set session ID; prevented by regenerating it on login.
Homework
4 minBuild a secure session-based login (random IDs, secure cookies, rate limiting, real logout, expiry) on top of your bcrypt store from Lesson 13. Run the auth-hardening audit to zero gaps. Write a short note comparing sessions vs tokens for two scenarios — a classic server-rendered web app and a mobile-app API — and which you'd choose for each and why.
Sample · sessions vs tokens by scenario
Classic server-rendered web app (e.g. my blog): → SESSIONS (HttpOnly cookie). The browser sends the cookie automatically, revocation is instant (logout deletes it), and HttpOnly keeps XSS from stealing it. I add SameSite=Lax for CSRF. Server state is fine for one app + a small session store. Mobile-app / multi-service API: → TOKENS (JWT). No browser cookies; the app sends a Bearer token, any backend instance can verify the signature without a shared session store (scales horizontally). I use SHORT expiry + refresh tokens to limit the no-easy-revocation downside, and store the token in secure device storage, never logging it. Either way: random IDs/secrets, rate-limited login, real logout/ expiry, HTTPS only. My audit reports 7/7.
Non-negotiables: a hardened session login passing the audit, and a reasoned sessions-vs-tokens choice for both scenarios.