422 lines
12 KiB
Markdown
422 lines
12 KiB
Markdown
# 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
|