# ADR-005: IndieLogin Authentication Integration ## Status Accepted ## Context The user has explicitly required external IndieLogin authentication via indielogin.com for V1. This is different from implementing a full IndieAuth server (which CLAUDE.MD mentions). The distinction is important: - **IndieAuth Server**: Host your own authentication endpoint (complex) - **IndieLogin Service**: Use indielogin.com as an external authentication provider (simple) The user wants the simpler approach: delegate authentication to indielogin.com using their API (https://indielogin.com/api). IndieLogin.com is a service that: 1. Handles the OAuth 2.0 / IndieAuth flow 2. Verifies user identity via their website 3. Returns authenticated identity to our application 4. Supports multiple authentication methods (RelMeAuth, email, etc.) ## Decision ### Use IndieLogin.com as External Authentication Provider **Authentication Flow**: OAuth 2.0 Authorization Code flow via indielogin.com **API Endpoint**: https://indielogin.com/auth **Token Validation**: Server-side session tokens (not IndieAuth tokens) **User Identity**: URL (me parameter) verified by indielogin.com ### Architecture ``` User Browser → StarPunk → indielogin.com → User's Website ↑ ↓ └──────────────────────────────┘ (Authenticated session) ``` ## Authentication Flow ### 1. Login Initiation ``` User clicks "Login" ↓ StarPunk generates state token (CSRF protection) ↓ Redirect to: https://indielogin.com/auth? - me={user_website} - client_id={starpunk_url} - redirect_uri={starpunk_url}/auth/callback - state={random_token} ``` ### 2. IndieLogin Processing ``` indielogin.com verifies user identity: - Checks for rel="me" links on user's website - Or sends email verification - Or uses other IndieAuth methods ↓ User authenticates via their chosen method ↓ indielogin.com redirects back to StarPunk ``` ### 3. Callback Verification ``` indielogin.com → StarPunk callback with: - code={authorization_code} - state={original_state} ↓ StarPunk verifies state matches ↓ StarPunk exchanges code for verified identity: POST https://indielogin.com/auth - code={authorization_code} - client_id={starpunk_url} - redirect_uri={starpunk_url}/auth/callback ↓ indielogin.com responds with: { "me": "https://user-website.com" } ↓ StarPunk creates authenticated session ``` ### 4. Session Management ``` StarPunk stores session token in cookie ↓ Session token maps to authenticated user URL ↓ Admin routes check for valid session ``` ## Implementation Requirements ### Configuration Variables ``` SITE_URL=https://starpunk.example.com ADMIN_ME=https://your-website.com SESSION_SECRET=random_secret_key ``` ### Database Schema Addition ```sql -- Add to existing schema CREATE TABLE sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_token TEXT UNIQUE NOT NULL, me TEXT NOT NULL, -- Authenticated user URL created_at TIMESTAMP NOT NULL, expires_at TIMESTAMP NOT NULL, last_used_at TIMESTAMP ); CREATE INDEX idx_sessions_token ON sessions(session_token); CREATE INDEX idx_sessions_expires ON sessions(expires_at); CREATE TABLE auth_state ( state TEXT PRIMARY KEY, created_at TIMESTAMP NOT NULL, expires_at TIMESTAMP NOT NULL -- Short-lived (5 minutes) ); ``` ### HTTP Client for API Calls Use **httpx** (already selected in ADR-002) for: - POST to https://indielogin.com/auth to exchange code - Verify response contains valid "me" URL - Handle network errors gracefully ### Routes Required ``` GET /admin/login - Display login form POST /admin/login - Initiate IndieLogin flow GET /auth/callback - Handle IndieLogin redirect POST /admin/logout - Destroy session ``` ### Login Flow Implementation #### Step 1: Login Form ```python # /admin/login (GET) # Display simple form asking for user's website URL # Form submits to POST /admin/login with "me" parameter ``` #### Step 2: Initiate Authentication ```python # /admin/login (POST) def initiate_login(me_url): # Validate me_url format if not is_valid_url(me_url): return error("Invalid URL") # Generate and store state token state = generate_random_token() store_state(state, expires_in_minutes=5) # Build IndieLogin authorization URL params = { 'me': me_url, 'client_id': SITE_URL, 'redirect_uri': f"{SITE_URL}/auth/callback", 'state': state } auth_url = f"https://indielogin.com/auth?{urlencode(params)}" # Redirect user to IndieLogin return redirect(auth_url) ``` #### Step 3: Handle Callback ```python # /auth/callback (GET) def handle_callback(code, state): # Verify state token (CSRF protection) if not verify_state(state): return error("Invalid state") # Exchange code for verified identity response = httpx.post('https://indielogin.com/auth', data={ 'code': code, 'client_id': SITE_URL, 'redirect_uri': f"{SITE_URL}/auth/callback" }) if response.status_code != 200: return error("Authentication failed") data = response.json() me = data.get('me') # Verify this is the authorized admin if me != ADMIN_ME: return error("Unauthorized user") # Create session session_token = generate_random_token() create_session(session_token, me, expires_in_days=30) # Set session cookie set_cookie('session', session_token, httponly=True, secure=True) # Redirect to admin dashboard return redirect('/admin') ``` #### Step 4: Session Validation ```python # Decorator for protected routes def require_auth(f): def wrapper(*args, **kwargs): session_token = request.cookies.get('session') if not session_token: return redirect('/admin/login') session = get_session(session_token) if not session or session.expired: return redirect('/admin/login') # Update last_used_at update_session_activity(session_token) # Store user info in request context g.user_me = session.me return f(*args, **kwargs) return wrapper # Usage @app.route('/admin') @require_auth def admin_dashboard(): return render_template('admin/dashboard.html') ``` ## Rationale ### Why IndieLogin.com Instead of Self-Hosted IndieAuth? **Simplicity Score: 10/10 (IndieLogin) vs 4/10 (Self-hosted)** - IndieLogin.com handles all complexity of: - Discovering user's auth endpoints - Verifying user identity - Supporting multiple auth methods (RelMeAuth, email, etc.) - PKCE implementation - Self-hosted would require implementing full IndieAuth spec (complex) **Fitness Score: 10/10** - Perfect for single-user system - User controls their identity via their own website - No password management needed - Aligns with IndieWeb principles **Maintenance Score: 10/10** - indielogin.com is maintained by IndieWeb community - No auth code to maintain ourselves - Security updates handled externally - Well-tested service **Standards Compliance: Pass** - Uses OAuth 2.0 / IndieAuth standards - Compatible with IndieWeb ecosystem - User identity is their URL (IndieWeb principle) ### Why Session Cookies Instead of Access Tokens? For admin interface (not Micropub): - **Simpler**: Standard web session pattern - **Secure**: HttpOnly cookies prevent XSS - **Appropriate**: Admin is human using browser, not API client - **Note**: Micropub will still use access tokens (separate ADR needed) ## Consequences ### Positive - Extremely simple implementation (< 100 lines of code) - No authentication code to maintain - Secure by default (delegated to trusted service) - True IndieWeb authentication (user owns identity) - No passwords to manage - Works immediately without setup - Community-maintained service ### Negative - Dependency on external service (indielogin.com) - Requires internet connection to authenticate - Single point of failure for login (mitigated: session stays valid) - User must have their own website/URL ### Mitigation - Sessions last 30 days, so brief indielogin.com outages don't lock out user - Document fallback: edit database to create session manually if needed - IndieLogin.com is stable, community-run service with good uptime - For V2: Consider optional email fallback or self-hosted IndieAuth ## Security Considerations ### State Token (CSRF Protection) - Generate cryptographically random state token - Store in database with short expiry (5 minutes) - Verify state matches on callback - Delete state after use (single-use tokens) ### Session Token Security - Generate with secrets.token_urlsafe(32) or similar - Store hash in database (not plaintext) - Mark cookies as HttpOnly and Secure - Set SameSite=Lax for CSRF protection - Implement session expiry (30 days) - Support manual logout (session deletion) ### Identity Verification - Only allow ADMIN_ME URL to authenticate - Verify "me" URL from indielogin.com exactly matches config - Reject any other authenticated users - Log authentication attempts ### Network Security - Use HTTPS for all communication - Verify SSL certificates on httpx requests - Handle network timeouts gracefully - Log authentication failures ## Testing Strategy ### Unit Tests - State token generation and validation - Session creation and expiry - URL validation - Cookie handling ### Integration Tests - Mock indielogin.com API responses - Test full authentication flow - Test session expiry - Test unauthorized user rejection - Test CSRF protection (invalid state) ### Manual Testing - Authenticate with real indielogin.com - Verify session persistence - Test logout functionality - Test session expiry - Test with wrong "me" URL ## Alternatives Considered ### Self-Hosted IndieAuth Server (Rejected) - **Complexity**: Must implement full IndieAuth spec - **Maintenance**: Security updates, endpoint discovery, token generation - **Verdict**: Too complex for V1, violates simplicity principle ### Password Authentication (Rejected) - **Security**: Must hash passwords, handle resets, prevent brute force - **IndieWeb**: Violates IndieWeb principle of URL-based identity - **Verdict**: Not aligned with project goals ### OAuth via GitHub/Google (Rejected) - **Simplicity**: Easy to implement - **IndieWeb**: Not IndieWeb-compatible, user doesn't own identity - **Verdict**: Violates IndieWeb requirements ### Email Magic Links (Rejected) - **Simplicity**: Requires email sending infrastructure - **IndieWeb**: Not standard IndieWeb authentication - **Verdict**: Deferred to V2 as fallback option ### Multi-User IndieAuth (Rejected for V1) - **Scope**: V1 is explicitly single-user - **Complexity**: Would require user management - **Verdict**: Out of scope, defer to V2 ## Implementation Checklist - [ ] Add SESSION_SECRET and ADMIN_ME to configuration - [ ] Create sessions and auth_state database tables - [ ] Implement state token generation and storage - [ ] Create login form template - [ ] Implement /admin/login routes (GET and POST) - [ ] Implement /auth/callback route - [ ] Implement session creation and validation - [ ] Create require_auth decorator - [ ] Implement logout functionality - [ ] Set secure cookie parameters - [ ] Add authentication error handling - [ ] Write unit tests for auth flow - [ ] Write integration tests with mocked indielogin.com - [ ] Test with real indielogin.com - [ ] Document setup process for users ## Configuration Example ```bash # .env file SITE_URL=https://starpunk.example.com ADMIN_ME=https://your-website.com SESSION_SECRET=your-random-secret-key-here ``` ## User Setup Documentation 1. Deploy StarPunk to your server at `https://starpunk.example.com` 2. Configure `ADMIN_ME` to your personal website URL 3. Visit `/admin/login` 4. Enter your website URL (must match ADMIN_ME) 5. indielogin.com will verify your identity 6. Authenticate via your chosen method 7. Redirected back to StarPunk admin interface ## References - IndieLogin.com: https://indielogin.com/ - IndieLogin API Documentation: https://indielogin.com/api - IndieAuth Specification: https://indieauth.spec.indieweb.org/ - OAuth 2.0 Spec: https://oauth.net/2/ - Web Authentication Best Practices: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html