Files
StarPunk/docs/decisions/ADR-005-indielogin-authentication.md
2025-11-18 19:21:31 -07:00

12 KiB

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

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

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

# /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

# /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

# /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

# 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
  • 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

# .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