# Authentication Redirect Loop Diagnosis - Phase 4 **Date**: 2025-11-18 **Status**: ROOT CAUSE IDENTIFIED **Severity**: Critical - Blocking manual testing ## Executive Summary The Phase 4 development authentication is experiencing a redirect loop between `/dev/login` and `/admin/`. The session cookie is being set correctly, but Flask's server-side session storage is failing, preventing the `@require_auth` decorator from storing the redirect URL properly. **Root Cause**: Misuse of Flask's `session` object in the `require_auth` decorator without proper initialization. ## Problem Description ### User Experience 1. User clicks dev login at `/dev/login` 2. Browser redirects to `/admin/` (302) 3. Browser redirects back to `/admin/login` (302) 4. User lands on login page, unauthenticated ### Server Logs ``` [2025-11-18 21:55:03] WARNING in dev_auth: DEV MODE: Creating session for https://dev.example.com WITHOUT authentication. [2025-11-18 21:55:03] INFO in auth: Session created for https://dev.example.com 127.0.0.1 - - [18/Nov/2025 21:55:03] "GET /dev/login HTTP/1.1" 302 - 127.0.0.1 - - [18/Nov/2025 21:55:03] "GET /admin/ HTTP/1.1" 302 - 127.0.0.1 - - [18/Nov/2025 21:55:03] "GET /admin/login HTTP/1.1" 200 - ``` ## Root Cause Analysis ### The Critical Issue In `starpunk/auth.py`, line 397, the `require_auth` decorator attempts to use Flask's server-side session: ```python @wraps(f) def decorated_function(*args, **kwargs): # Get session token from cookie session_token = request.cookies.get("session") # Verify session session_info = verify_session(session_token) if not session_info: # Store intended destination session["next"] = request.url # ← THIS IS THE PROBLEM return redirect(url_for("auth.login_form")) ``` ### Why This Causes the Redirect Loop 1. **Session Cookie Name Collision**: - Flask's server-side session uses a cookie named `session` by default - StarPunk's authentication uses a cookie named `session` for the session token - These are TWO DIFFERENT things being stored under the same name 2. **What Actually Happens**: - `/dev/login` sets `session` cookie with the authentication token (e.g., `"xyz123abc456..."`) - Browser sends this cookie to `/admin/` - `@require_auth` reads `request.cookies.get("session")` → Gets the auth token (correct) - `verify_session()` validates the token → Returns valid session info (correct) - BUT: If there's ANY code path that triggers Flask session access elsewhere, Flask tries to deserialize the auth token as a Flask session object - When `require_auth` tries to write `session["next"] = request.url`, Flask overwrites the `session` cookie with its own signed session data - On the next request, the auth token is gone, replaced by Flask session data - `verify_session()` fails because the cookie now contains Flask session JSON, not an auth token - User is redirected back to login 3. **The Timing Issue**: - The redirect happens so fast that the browser sees: 1. Cookie set to auth token 2. Redirect to `/admin/` 3. Flask session middleware processes the request 4. Cookie gets overwritten with Flask session data 5. Auth check fails 6. Redirect to `/admin/login` ### Secondary Issue: Flash Messages The dev login route also uses `flash()` which relies on Flask's session: ```python flash("DEV MODE: Logged in without authentication", "warning") ``` When `flash()` is called, Flask writes to the server-side session, which triggers the cookie overwrite. ## Why This Wasn't Caught Earlier 1. **Production IndieAuth Flow**: The production flow doesn't use `flash()` or `session["next"]` in the same request cycle as setting the auth cookie 2. **Test Coverage Gap**: Tests likely mock the session or don't test the full HTTP request/response cycle 3. **Cookie Name Collision**: Using `session` for both Flask's session and StarPunk's auth token is architecturally unsound ## The Fix ### Option 1: Rename StarPunk Session Cookie (RECOMMENDED) **Rationale**: Flask owns the `session` cookie name. We should not conflict with framework conventions. **Changes Required**: #### 1. Update `starpunk/routes/dev_auth.py` (Line 74-81) **Old Code**: ```python response.set_cookie( "session", session_token, httponly=True, secure=False, samesite="Lax", max_age=30 * 24 * 60 * 60, ) ``` **New Code**: ```python response.set_cookie( "starpunk_session", # ← Changed from "session" session_token, httponly=True, secure=False, samesite="Lax", max_age=30 * 24 * 60 * 60, ) ``` #### 2. Update `starpunk/auth.py` (Line 390) **Old Code**: ```python session_token = request.cookies.get("session") ``` **New Code**: ```python session_token = request.cookies.get("starpunk_session") # ← Changed from "session" ``` #### 3. Update `starpunk/routes/auth.py` (IndieAuth callback) Find where the session cookie is set after IndieAuth callback (likely similar to dev_auth) and change the cookie name there as well. **Search for**: `response.set_cookie("session"` **Replace with**: `response.set_cookie("starpunk_session"` #### 4. Update logout route to clear correct cookie Find the logout route and ensure it clears `starpunk_session` instead of `session`. ### Option 2: Disable Flask Session (NOT RECOMMENDED) We could disable Flask's session entirely by not setting `SECRET_KEY`, but this would: - Break `flash()` messages - Break `session["next"]` redirect tracking - Require rewriting all flash message functionality This adds complexity without benefit. ### Option 3: Use Query Parameter for Redirect (PARTIAL FIX) Instead of `session["next"]`, use a query parameter: ```python return redirect(url_for("auth.login_form", next=request.url)) ``` This fixes the immediate issue but doesn't resolve the cookie name collision, which will cause problems elsewhere. ## Recommended Solution: Option 1 **Why**: - Minimal code changes (4 locations) - Follows Flask conventions (Flask owns `session`) - Preserves all existing functionality - Clear separation of concerns - No security implications **Implementation Steps**: 1. Search codebase for all instances of `"session"` cookie usage 2. Replace with `"starpunk_session"` 3. Update any logout functionality 4. Update any session validation code 5. Test full auth flow (dev and production) ## Files Requiring Changes 1. `/home/phil/Projects/starpunk/starpunk/routes/dev_auth.py` - Line 75 2. `/home/phil/Projects/starpunk/starpunk/auth.py` - Line 390 3. `/home/phil/Projects/starpunk/starpunk/routes/auth.py` - Find callback route cookie setting 4. `/home/phil/Projects/starpunk/starpunk/routes/auth.py` - Find logout route cookie clearing ## Testing Approach ### Manual Test Plan 1. **Dev Login Flow**: ``` 1. Visit http://localhost:5000/admin/ 2. Verify redirect to /admin/login 3. Click dev login link 4. Verify redirect to /admin/ 5. Verify dashboard loads (no redirect loop) 6. Verify flash message appears 7. Check browser DevTools → Application → Cookies 8. Verify "starpunk_session" cookie exists with token value 9. Verify "session" cookie exists with Flask session data (if flash used) ``` 2. **Session Persistence**: ``` 1. After successful login, visit /admin/new 2. Verify authentication persists 3. Refresh page 4. Verify still authenticated ``` 3. **Logout**: ``` 1. While authenticated, click logout 2. Verify redirect to login 3. Verify "starpunk_session" cookie is cleared 4. Try to visit /admin/ 5. Verify redirect to /admin/login ``` ### Automated Test Requirements Add tests for: - Cookie name verification - Session persistence across requests - Flash message functionality with auth - Redirect loop prevention ## Security Implications **None**: This change is purely architectural cleanup. Both cookie names are: - HttpOnly (prevents JavaScript access) - SameSite=Lax (CSRF protection) - Same security properties The separation actually improves security by: - Clear separation of concerns - Easier to audit (two distinct cookies) - Follows framework conventions ## Architecture Decision This issue reveals a broader architectural concern: **Cookie Naming Strategy**. ### New Standard: Cookie Naming Convention **Rule**: Never use generic names that conflict with framework conventions. **StarPunk Cookie Names**: - `starpunk_session` - Authentication session token - `session` - Reserved for Flask framework use - Future cookies should use `starpunk_*` prefix **Document in**: `/docs/standards/api-design.md` under "Cookie Standards" ## Prevention ### Code Review Checklist Addition Add to code review standards: - [ ] No custom cookies named `session`, `csrf_token`, or other framework-reserved names - [ ] All StarPunk cookies use `starpunk_` prefix - [ ] Cookie security attributes verified (HttpOnly, Secure, SameSite) ### Configuration Validation Consider adding startup validation: ```python # In config.py validate_config() if app.config.get("SESSION_COOKIE_NAME") == "session": app.logger.warning( "Using default Flask session cookie name. " "StarPunk auth uses 'starpunk_session' to avoid conflicts." ) ``` ## Timeline **Estimated Fix Time**: 30 minutes - 10 min: Search and replace cookie names - 10 min: Manual testing - 10 min: Update changelog and version **Priority**: CRITICAL - Blocking Phase 4 manual testing ## Next Steps for Developer 1. Read this document completely 2. Search codebase for all `"session"` cookie references 3. Implement Option 1 changes systematically 4. Run manual test plan 5. Update `/docs/standards/api-design.md` with cookie naming convention 6. Update changelog 7. Increment version to 0.5.1 (bugfix) 8. Create git commit with proper message ## References - Flask Documentation: https://flask.palletsprojects.com/en/3.0.x/api/#flask.session - Cookie Security: https://owasp.org/www-community/controls/SecureFlag - IndieWeb Session Spec: https://indieweb.org/session