# 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://www.w3.org/TR/indieauth/
- OAuth 2.0 Spec: https://oauth.net/2/
- Web Authentication Best Practices: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
