# Auth Redirect Loop - Visual Diagram ## Current Behavior (BROKEN) ``` ┌──────────────────────────────────────────────────────────────────┐ │ User clicks "Dev Login" │ └──────────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ GET /dev/login │ │ │ │ 1. create_dev_session(me) → returns "abc123xyz" │ │ 2. response.set_cookie("session", "abc123xyz") │ │ 3. flash("DEV MODE: Logged in") ← This triggers Flask session! │ │ Flask writes: session = {_flashes: ["message"]} │ │ 4. return redirect("/admin/") │ │ │ │ ⚠️ Cookie "session" is now Flask session data, NOT auth token! │ └──────────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ Browser → GET /admin/ │ │ Cookie: session={_flashes: ["message"]} ← WRONG DATA! │ └──────────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ @require_auth decorator │ │ │ │ 1. session_token = request.cookies.get("session") │ │ → Gets: {_flashes: ["message"]} ← Not a token! │ │ 2. verify_session("{_flashes: ...}") │ │ → hash("{_flashes: ...}") doesn't match any DB session │ │ → Returns None │ │ 3. if not session_info: │ │ session["next"] = request.url ← More Flask session! │ │ return redirect("/admin/login") │ └──────────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ Browser → GET /admin/login │ │ User sees: Login page (NOT dashboard) │ │ Result: REDIRECT LOOP ❌ │ └──────────────────────────────────────────────────────────────────┘ ``` ## Fixed Behavior (CORRECT) ``` ┌──────────────────────────────────────────────────────────────────┐ │ User clicks "Dev Login" │ └──────────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ GET /dev/login │ │ │ │ 1. create_dev_session(me) → returns "abc123xyz" │ │ 2. response.set_cookie("starpunk_session", "abc123xyz") │ │ 3. flash("DEV MODE: Logged in") │ │ Flask writes: session = {_flashes: ["message"]} │ │ 4. return redirect("/admin/") │ │ │ │ ✅ Two separate cookies: │ │ - starpunk_session = "abc123xyz" (auth token) │ │ - session = {_flashes: ["message"]} (Flask session) │ └──────────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ Browser → GET /admin/ │ │ Cookie: starpunk_session=abc123xyz │ │ Cookie: session={_flashes: ["message"]} │ └──────────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ @require_auth decorator │ │ │ │ 1. session_token = request.cookies.get("starpunk_session") │ │ → Gets: "abc123xyz" ✅ Correct auth token! │ │ 2. verify_session("abc123xyz") │ │ → hash("abc123xyz") matches DB session │ │ → Returns: {me: "https://dev.example.com", ...} │ │ 3. if session_info: ✅ Valid session! │ │ g.user = session_info │ │ g.me = session_info["me"] │ │ return dashboard() ← Continues to dashboard! │ └──────────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ Browser → Renders /admin/ dashboard │ │ User sees: Dashboard with notes ✅ │ │ Flash message: "DEV MODE: Logged in" ✅ │ │ Result: SUCCESS! No redirect loop! ✅ │ └──────────────────────────────────────────────────────────────────┘ ``` ## Cookie Collision Visualization ### BEFORE (Broken) ``` ┌─────────────────────────────────────────────────┐ │ BROWSER │ │ │ │ Cookies for localhost:5000: │ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ Name: session │ │ │ │ Value: {_flashes: ["message"]} │ │ │ │ │ │ │ │ ❌ CONFLICT: This should be auth token!│ │ │ │ Flask overwrote our auth token! │ │ │ └─────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────┘ ``` ### AFTER (Fixed) ``` ┌─────────────────────────────────────────────────┐ │ BROWSER │ │ │ │ Cookies for localhost:5000: │ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ Name: starpunk_session │ │ │ │ Value: abc123xyz... │ │ │ │ Purpose: Auth token ✅ │ │ │ └─────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ Name: session │ │ │ │ Value: {_flashes: ["message"]} │ │ │ │ Purpose: Flask session ✅ │ │ │ └─────────────────────────────────────────┘ │ │ │ │ ✅ Two separate cookies, no conflict! │ └─────────────────────────────────────────────────┘ ``` ## Timeline of Events ### Broken Flow ``` Time Request Cookie State Auth State ──────────────────────────────────────────────────────────────────────── T0 GET /dev/login (none) Not authed T1 ↓ set_cookie session = "abc123xyz" Token set ✅ T2 ↓ flash() session = {_flashes: [...]} OVERWRITTEN ❌ T3 302 → /admin/ session = {_flashes: [...]} Token LOST ❌ T4 GET /admin/ session = {_flashes: [...]} Not authed ❌ T5 ↓ @require_auth verify("{_flashes...}") = None FAIL ❌ T6 302 → /admin/login session = {_flashes: [...]} Not authed ❌ T7 GET /admin/login session = {_flashes: [...]} Not authed ❌ → User sees login page (LOOP!) ❌ ``` ### Fixed Flow ``` Time Request Cookie State Auth State ───────────────────────────────────────────────────────────────────────────── T0 GET /dev/login (none) Not authed T1 ↓ set_cookie starpunk_session = "abc123xyz" Token set ✅ T2 ↓ flash() session = {_flashes: [...]} Flask data ✅ starpunk_session = "abc123xyz" Token preserved ✅ T3 302 → /admin/ starpunk_session = "abc123xyz" Authed ✅ session = {_flashes: [...]} T4 GET /admin/ starpunk_session = "abc123xyz" Authed ✅ T5 ↓ @require_auth verify("abc123xyz") = {me: ...} SUCCESS ✅ T6 Render dashboard starpunk_session = "abc123xyz" Authed ✅ → User sees dashboard ✅ ``` ## Request/Response Detail ### Broken Request/Response Cycle ``` REQUEST 1: GET /dev/login ═══════════════════════════════════════════════════════════════════ RESPONSE 1: HTTP/1.1 302 Found Location: /admin/ Set-Cookie: session={_flashes: [...]}; HttpOnly; SameSite=Lax ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ❌ Flask overwrote our auth token! ─────────────────────────────────────────────────────────────────── REQUEST 2: GET /admin/ Cookie: session={_flashes: [...]} ^^^^^^^^^^^^^^^^^^^^^^^^ ❌ Sending Flask session data, not auth token! ═══════════════════════════════════════════════════════════════════ RESPONSE 2: HTTP/1.1 302 Found Location: /admin/login ❌ @require_auth rejected (no valid token) ``` ### Fixed Request/Response Cycle ``` REQUEST 1: GET /dev/login ═══════════════════════════════════════════════════════════════════ RESPONSE 1: HTTP/1.1 302 Found Location: /admin/ Set-Cookie: starpunk_session=abc123xyz; HttpOnly; SameSite=Lax ✅ Auth token in separate cookie Set-Cookie: session={_flashes: [...]}; HttpOnly; SameSite=Lax ✅ Flask session in separate cookie ─────────────────────────────────────────────────────────────────── REQUEST 2: GET /admin/ Cookie: starpunk_session=abc123xyz ✅ Sending correct auth token! Cookie: session={_flashes: [...]} ✅ Flask session data also sent (for flash messages) ═══════════════════════════════════════════════════════════════════ RESPONSE 2: HTTP/1.1 200 OK Content-Type: text/html ``` ## Code Comparison ### Setting the Cookie ```python # BEFORE (Broken) response.set_cookie( "session", # ❌ Conflicts with Flask session_token, httponly=True, secure=False, samesite="Lax", ) # AFTER (Fixed) response.set_cookie( "starpunk_session", # ✅ No conflict! session_token, httponly=True, secure=False, samesite="Lax", ) ``` ### Reading the Cookie ```python # BEFORE (Broken) session_token = request.cookies.get("session") # Gets Flask session data, not our token! ❌ # AFTER (Fixed) session_token = request.cookies.get("starpunk_session") # Gets our auth token correctly! ✅ ``` ## Why Flash Triggers the Problem Flask's `flash()` function writes to the session: ```python # When you call: flash("DEV MODE: Logged in", "warning") # Flask internally does: session['_flashes'] = [("warning", "DEV MODE: Logged in")] # Which triggers: response.set_cookie("session", serialize(session), ...) # This OVERWRITES any cookie named "session"! ``` ## The Key Insight **Flask owns the `session` cookie name. We cannot use it.** Flask reserves this cookie for its own session management (flash messages, session["key"] storage, etc.). When we try to use the same cookie name for our auth token, Flask overwrites it whenever session data is modified. **Solution**: Use our own namespace → `starpunk_session` ## Architectural Principle Established **Cookie Naming Convention**: All application cookies MUST use an application-specific prefix to avoid conflicts with framework-reserved names. - Framework cookies: `session`, `csrf_token`, etc. (owned by Flask) - Application cookies: `starpunk_session`, `starpunk_*` (owned by StarPunk) This separation ensures: 1. No name collisions 2. Clear ownership 3. Easier debugging (you know which cookie is which) 4. Standards compliance