Implement complete authentication system following ADR-010 and Phase 3 design specs. This is a MINOR version increment (0.3.0 -> 0.4.0) as it adds new functionality. Authentication Features: - IndieLogin authentication flow via indielogin.com - Secure session management with SHA-256 token hashing - CSRF protection with single-use state tokens - Session lifecycle (create, verify, destroy) - require_auth decorator for protected routes - Automatic cleanup of expired sessions - IP address and user agent tracking Security Measures: - Cryptographically secure token generation (secrets module) - Token hashing for storage (never plaintext) - SQL injection prevention (prepared statements) - Single-use CSRF state tokens - 30-day session expiry with activity refresh - Comprehensive security logging Implementation Details: - starpunk/auth.py: 406 lines, 6 core functions, 4 helpers, 4 exceptions - tests/test_auth.py: 648 lines, 37 tests, 96% coverage - Database schema updates for sessions and auth_state tables - URL validation utility added to utils.py Test Coverage: - 37 authentication tests - 96% code coverage (exceeds 90% target) - All security features tested - Edge cases and error paths covered Documentation: - Implementation report in docs/reports/ - Updated CHANGELOG.md with detailed changes - Version incremented to 0.4.0 - ADR-010 and Phase 3 design docs included Follows project standards: - Black code formatting (88 char lines) - Flake8 linting (no errors) - Python coding standards - Type hints on all functions - Comprehensive docstrings 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
6.9 KiB
ADR-010: Authentication Module Design
Status
Accepted
Context
With the core utilities and notes management complete, StarPunk needs an authentication system for the admin interface. ADR-005 already decided on using IndieLogin.com as the authentication provider. This ADR defines the specific module architecture and implementation approach for the authentication system.
The authentication module must:
- Integrate with IndieLogin.com for identity verification
- Manage secure sessions for authenticated users
- Protect admin routes from unauthorized access
- Handle CSRF protection for the authentication flow
- Support single-user authorization (V1 requirement)
Decision
Module Architecture
Single Module Approach: Implement all authentication logic in a single starpunk/auth.py module.
Rationale:
- Authentication is ~200-300 lines of focused code
- Single responsibility: manage authentication and sessions
- No need to split into multiple files for V1
- Easier to understand and maintain
Session Storage Strategy
Database-Backed Sessions with these characteristics:
- Session tokens stored in SQLite (not Redis/Memcached)
- Cryptographically secure token generation (32 bytes)
- 30-day expiry with activity-based refresh
- Automatic cleanup of expired sessions
Schema:
CREATE TABLE sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_token TEXT UNIQUE NOT NULL,
me TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
expires_at TIMESTAMP NOT NULL,
last_used_at TIMESTAMP,
user_agent TEXT,
ip_address TEXT
);
CREATE TABLE auth_state (
state TEXT PRIMARY KEY,
created_at TIMESTAMP NOT NULL,
expires_at TIMESTAMP NOT NULL,
redirect_uri TEXT
);
Security Architecture
Token Security:
- Use
secrets.token_urlsafe(32)for session tokens - Store token hash in database (SHA-256), not plaintext
- HttpOnly, Secure, SameSite=Lax cookies
- No JavaScript access to session tokens
CSRF Protection:
- Generate state token for each auth request
- Store in
auth_statetable with 5-minute expiry - Verify state on callback before code exchange
- Single-use tokens (delete after verification)
Authorization Model:
- Single admin user (configured via ADMIN_ME environment variable)
- Strict equality check (no substring matching)
- Reject any user whose
meURL doesn't exactly match ADMIN_ME - Log all authentication attempts for security audit
Function Architecture
Core Functions (6 total):
initiate_login(me_url: str) -> str- Start IndieLogin flowhandle_callback(code: str, state: str) -> Optional[str]- Process callbackcreate_session(me: str) -> str- Create new sessionverify_session(token: str) -> Optional[Dict]- Validate sessiondestroy_session(token: str) -> None- Logoutrequire_auth(f)- Decorator for protected routes
Helper Functions (4 total):
_generate_state_token() -> str- CSRF token generation_verify_state_token(state: str) -> bool- CSRF validation_cleanup_expired_sessions() -> None- Maintenance_hash_token(token: str) -> str- Security hashing
Error Handling Strategy
Custom Exceptions:
class AuthError(Exception): pass
class InvalidStateError(AuthError): pass
class UnauthorizedError(AuthError): pass
class IndieLoginError(AuthError): pass
Error Responses:
- Invalid state: 400 Bad Request with clear error
- Unauthorized user: 403 Forbidden
- IndieLogin failure: 502 Bad Gateway
- Session expired: Redirect to login
Integration Points
Flask Integration:
- Use Flask's
gobject for request-scoped user data - Integrate with Flask's session for flash messages
- Use Flask's
before_requestfor session refresh - Leverage Flask's error handlers for auth errors
Database Integration:
- Use existing
get_db()connection management - Transactions for session creation/deletion
- Prepared statements to prevent SQL injection
Rationale
Why Database Sessions Over Flask-Session?
Database sessions chosen:
- Already have SQLite, no new dependencies
- Persistent across server restarts
- Can query active sessions
- Supports session invalidation
- Better security (server-side storage)
Flask-Session rejected:
- Adds Redis/Memcached dependency
- Overkill for single-user system
- More complex deployment
Why Token Hashing?
Even though we're single-user, proper security practices:
- Defense in depth principle
- Prevents token leakage if database exposed
- Industry best practice
- No performance impact for our scale
Why 30-Day Sessions?
Balance between security and usability:
- Long enough to avoid frequent re-authentication
- Short enough to limit exposure window
- Activity-based refresh keeps active sessions alive
- Matches common web application patterns
Why Single Module?
Simplicity principle:
- Authentication is cohesive functionality
- ~300 lines doesn't justify splitting
- Easier to audit security in one file
- Reduces import complexity
Consequences
Positive
- Simple implementation - Single file, clear responsibilities
- Secure by default - Industry best practices applied
- Zero dependencies - Uses existing stack (SQLite, httpx)
- Maintainable - All auth logic in one place
- Testable - Clear function boundaries, mockable
- Production-ready - Proper session management, security
- IndieWeb compliant - Full IndieAuth specification support
Negative
- Manual session cleanup - Need periodic expired session removal
- No rate limiting - Could add in V2 if needed
- Single admin limitation - Architectural constraint for V1
- No 2FA - Relies entirely on IndieLogin's security
Mitigations
For session cleanup:
- Run cleanup on each login attempt
- Add admin command for manual cleanup
- Document in operations guide
For rate limiting:
- Deploy behind reverse proxy (nginx/Caddy)
- Add to V2 if abuse detected
- Log attempts for monitoring
Implementation Checklist
- Create
starpunk/auth.pymodule - Add session tables to database schema
- Implement core authentication functions
- Add custom exception classes
- Create require_auth decorator
- Write comprehensive tests (target: 90% coverage)
- Add security logging
- Document configuration requirements
- Create integration tests with IndieLogin
- Security audit checklist
References
- ADR-005: IndieLogin Authentication
- IndieAuth Specification
- OWASP Session Management
- Flask Security Best Practices
ADR: 010 Date: 2025-11-18 Status: Accepted Decision: Single-module database-backed session authentication with IndieLogin Supersedes: None