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>
This commit is contained in:
307
docs/design/auth-redirect-loop-diagnosis.md
Normal file
307
docs/design/auth-redirect-loop-diagnosis.md
Normal file
@@ -0,0 +1,307 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user