# 0002. Authentication Strategy Date: 2025-12-22 ## Status Accepted ## Context Sneaky Klaus has two distinct user types with different authentication needs: 1. **Administrator**: Single admin account for entire installation. Needs persistent access to manage exchanges. Must be able to recover access if password is forgotten. 2. **Participants**: Multiple participants across multiple exchanges. Should have frictionless authentication without password management burden. Same participant may join multiple exchanges using same email. Key requirements: - **Security**: Authentication must be secure and follow best practices - **Simplicity for participants**: No password required; minimal friction to access information - **Admin control**: Admin needs traditional authenticated session for management tasks - **Password recovery**: Admin must be able to recover access via email - **Session management**: Sessions should persist appropriately but expire for security - **Email verification**: Participant email addresses must be verified (implicit via magic link) ## Decision We will implement a **dual authentication strategy**: ### Admin Authentication: Password-Based **Login Flow**: 1. Admin enters email and password 2. Password hashed with bcrypt, compared to stored hash 3. On success, session created with admin role 4. Session cookie set with appropriate security flags **Password Requirements**: - Minimum 12 characters - No complexity requirements (no mandatory special chars, numbers, etc.) - This follows modern NIST guidance: length matters more than complexity **Password Recovery Flow**: 1. Admin requests password reset from login page 2. System sends time-limited reset token (1 hour expiration) to admin email 3. Reset link directs to password reset form 4. Token validated, new password set 5. Token invalidated after single use **Session Management**: - Server-side sessions stored in database or cache - 7-day sliding expiration window (extends on activity) - Secure, HTTP-only session cookies - SameSite=Lax for CSRF protection - Logout explicitly destroys session ### Participant Authentication: Magic Links **Magic Link Flow**: 1. Participant requests access (from registration page or email) 2. System generates cryptographically random token (256-bit) 3. Token stored in database with 1-hour expiration 4. Email sent with magic link: `/participant/auth/{token}` 5. Clicking link validates token and creates session 6. Token invalidated after single use **Session Management**: - Server-side sessions stored in database - 7-day sliding expiration window (extends on activity) - Secure, HTTP-only session cookies - SameSite=Lax for CSRF protection - Sessions scoped to participant's exchanges only - No explicit logout needed (session expires naturally) **Token Generation**: - Use Python's `secrets` module for cryptographic randomness - Tokens are 32-byte random values, URL-safe base64 encoded - Tokens stored as hashed values in database (using SHA-256) - Original token never stored in plain text ### Security Measures **Password Storage**: - bcrypt with cost factor 12 (adjustable) - Passwords never logged or exposed in error messages - Password reset tokens hashed before storage **Session Security**: - Session IDs are cryptographically random - Sessions stored server-side (not client-side JWTs) - Session data includes: user ID, role (admin/participant), creation time, last activity - Cookie flags: `Secure=True` (HTTPS only), `HttpOnly=True`, `SameSite=Lax` **Rate Limiting**: - Login attempts: 5 per email per 15 minutes - Magic link requests: 3 per email per hour - Password reset requests: 3 per email per hour - Implemented at application level, tracked in database or cache **Token Expiration**: - Magic link tokens: 1 hour - Password reset tokens: 1 hour - Admin sessions: 7 days (sliding window) - Participant sessions: 7 days (sliding window) ## Consequences ### Positive - **Participant convenience**: No password to remember; access via email - **Email verification**: Magic links implicitly verify participant email addresses - **Admin security**: Traditional password-based auth provides familiar security model - **Password recovery**: Admin can self-serve password reset without external support - **Sliding sessions**: Activity extends session, reducing re-authentication friction - **Security best practices**: Modern password requirements (length over complexity) - **CSRF protection**: SameSite cookies prevent cross-site request forgery - **Token security**: One-time-use tokens prevent replay attacks ### Negative - **Email dependency**: Magic links require working email delivery (mitigated by Resend reliability) - **Token expiration UX**: 1-hour expiration may frustrate slow email checkers (acceptable trade-off for security) - **Session storage**: Server-side sessions require database/cache storage (minimal overhead) - **No remember-me for admin**: 7-day max session requires re-login (acceptable for security) ### Neutral - **Dual auth complexity**: Maintaining two auth flows adds implementation complexity (necessary for different user needs) - **Rate limiting overhead**: Requires tracking attempts per user (minimal performance impact) - **Session cleanup**: Expired sessions must be periodically purged (handled via background job) ## Implementation Details ### Database Schema **Admin User**: ```python class Admin(Model): id: int email: str (unique, indexed) password_hash: str created_at: datetime updated_at: datetime ``` **Participant** (simplified for auth): ```python class Participant(Model): id: int email: str (indexed) exchange_id: int (foreign key) # ... other fields ``` **Session**: ```python class Session(Model): id: str (session ID, primary key) user_id: int user_type: str ('admin' | 'participant') created_at: datetime last_activity: datetime expires_at: datetime data: JSON (optional additional session data) ``` **Auth Token** (magic links and password reset): ```python class AuthToken(Model): id: int token_hash: str (indexed) token_type: str ('magic_link' | 'password_reset') email: str participant_id: int (nullable, for magic links) exchange_id: int (nullable, for magic links) created_at: datetime expires_at: datetime used_at: datetime (nullable) ``` **Rate Limit**: ```python class RateLimit(Model): id: int key: str (e.g., "login:admin@example.com", indexed) attempts: int window_start: datetime expires_at: datetime ``` ### Flask Session Configuration ```python app.config['SESSION_TYPE'] = 'sqlalchemy' # Server-side sessions app.config['SESSION_PERMANENT'] = True app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) app.config['SESSION_COOKIE_SECURE'] = True # HTTPS only app.config['SESSION_COOKIE_HTTPONLY'] = True app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' app.config['SESSION_REFRESH_EACH_REQUEST'] = True # Sliding window ``` ### Authentication Decorators ```python @login_required # Requires any authenticated user @admin_required # Requires admin role @participant_required # Requires participant role ``` ### URL Structure **Admin**: - `/admin/login` - Login form - `/admin/logout` - Logout - `/admin/forgot-password` - Request password reset - `/admin/reset-password/{token}` - Reset password form **Participant**: - `/participant/auth/{token}` - Magic link endpoint - `/participant/logout` - Optional logout ## Alternatives Considered ### OAuth/Social Login **Rejected**: Adds external dependencies, complicates self-hosting, and provides minimal benefit for a self-hosted application where users control the deployment. ### JWT Tokens **Rejected for sessions**: JWTs are stateless, making them difficult to invalidate (e.g., on logout or security incident). Server-side sessions provide better control. **Considered for magic links**: Could use JWTs for magic links, but custom tokens are simpler and equally secure. ### Passkeys/WebAuthn **Deferred**: Modern and secure but adds implementation complexity. Could be added in future version for admin auth. ### Email Verification Codes **Rejected**: 6-digit codes are less secure than magic links and require users to manually copy/paste, reducing convenience. ### Participant Passwords **Rejected**: Violates core principle of frictionless participant experience. Participants joining Secret Santa events shouldn't need to manage yet another password. ### Longer Magic Link Expiration **Rejected**: 1 hour balances security with usability. Longer expiration increases risk if email account is compromised. ### Shorter Session Duration **Considered**: 24-hour sessions would be more secure but require frequent re-authentication. 7-day sliding window balances security with convenience. ## Security Considerations ### Password Reset Token Timing Attack To prevent email enumeration via timing attacks: - Always show "If an account exists, you'll receive an email" message - Perform same-time operations regardless of email existence - Don't reveal whether email is registered ### Magic Link Security - Tokens are single-use and time-limited - Token hashing prevents database compromise from exposing valid tokens - Rate limiting prevents brute force token guessing - Tokens scoped to specific participant and exchange ### Session Fixation Prevention - New session ID generated on login - Old session destroyed on logout - Session ID rotated on privilege elevation ### Brute Force Protection - Rate limiting on all auth endpoints - Progressive delays on repeated failures (optional enhancement) - Account lockout not implemented (single admin, participant magic links) ## Future Enhancements Potential improvements for future versions: 1. **Admin 2FA**: Time-based OTP for additional admin security 2. **Passkeys**: WebAuthn support for passwordless admin auth 3. **Session device tracking**: Show admin active sessions and allow revocation 4. **Remember-me for admin**: Optional extended session with re-authentication for sensitive actions 5. **Magic link preview protection**: Use confirmation step before activating magic link ## References - NIST Password Guidelines: https://pages.nist.gov/800-63-3/sp800-63b.html - OWASP Authentication Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html - Flask Session Management: https://flask.palletsprojects.com/en/latest/quickstart/#sessions - Python secrets module: https://docs.python.org/3/library/secrets.html