that initial commit
This commit is contained in:
421
docs/decisions/ADR-005-indielogin-authentication.md
Normal file
421
docs/decisions/ADR-005-indielogin-authentication.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user