Files
StarPunk/docs/design/auth-redirect-loop-diagnosis.md
Phil Skentelbery 0cca8169ce feat: Implement Phase 4 Web Interface with bugfixes (v0.5.2)
## Phase 4: Web Interface Implementation

Implemented complete web interface with public and admin routes,
templates, CSS, and development authentication.

### Core Features

**Public Routes**:
- Homepage with recent published notes
- Note permalinks with microformats2
- Server-side rendering (Jinja2)

**Admin Routes**:
- Login via IndieLogin
- Dashboard with note management
- Create, edit, delete notes
- Protected with @require_auth decorator

**Development Authentication**:
- Dev login bypass for local testing (DEV_MODE only)
- Security safeguards per ADR-011
- Returns 404 when disabled

**Templates & Frontend**:
- Base layouts (public + admin)
- 8 HTML templates with microformats2
- Custom responsive CSS (114 lines)
- Error pages (404, 500)

### Bugfixes (v0.5.1 → v0.5.2)

1. **Cookie collision fix (v0.5.1)**:
   - Renamed auth cookie from "session" to "starpunk_session"
   - Fixed redirect loop between dev login and admin dashboard
   - Flask's session cookie no longer conflicts with auth

2. **HTTP 404 error handling (v0.5.1)**:
   - Update route now returns 404 for nonexistent notes
   - Delete route now returns 404 for nonexistent notes
   - Follows ADR-012 HTTP Error Handling Policy
   - Pattern consistency across all admin routes

3. **Note model enhancement (v0.5.2)**:
   - Exposed deleted_at field from database schema
   - Enables soft deletion verification in tests
   - Follows ADR-013 transparency principle

### Architecture

**New ADRs**:
- ADR-011: Development Authentication Mechanism
- ADR-012: HTTP Error Handling Policy
- ADR-013: Expose deleted_at Field in Note Model

**Standards Compliance**:
- Uses uv for Python environment
- Black formatted, Flake8 clean
- Follows git branching strategy
- Version incremented per versioning strategy

### Test Results

- 405/406 tests passing (99.75%)
- 87% code coverage
- All security tests passing
- Manual testing confirmed working

### Documentation

- Complete implementation reports in docs/reports/
- Architecture reviews in docs/reviews/
- Design documents in docs/design/
- CHANGELOG updated for v0.5.2

### Files Changed

**New Modules**:
- starpunk/dev_auth.py
- starpunk/routes/ (public, admin, auth, dev_auth)

**Templates**: 10 files (base, pages, admin, errors)
**Static**: CSS and optional JavaScript
**Tests**: 4 test files for routes and templates
**Docs**: 20+ architectural and implementation documents

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 23:01:53 -07:00

9.8 KiB

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:

@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:

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

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:

response.set_cookie(
    "session",
    session_token,
    httponly=True,
    secure=False,
    samesite="Lax",
    max_age=30 * 24 * 60 * 60,
)

New Code:

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:

session_token = request.cookies.get("session")

New Code:

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"

Find the logout route and ensure it clears starpunk_session instead of session.

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:

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.

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.

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:

# 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