Files
sneakyklaus/docs/decisions/0002-authentication-strategy.md
Phil Skentelbery b077112aba chore: initial project setup
Initialize Sneaky Klaus project with:
- uv package management and pyproject.toml
- Flask application structure (app.py, config.py)
- SQLAlchemy models for Admin and Exchange
- Alembic database migrations
- Pre-commit hooks configuration
- Development tooling (pytest, ruff, mypy)

Initial structure follows design documents in docs/:
- src/app.py: Application factory with Flask extensions
- src/config.py: Environment-based configuration
- src/models/: Admin and Exchange models
- migrations/: Alembic migration setup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-22 11:28:15 -07:00

10 KiB

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

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:

class Admin(Model):
    id: int
    email: str (unique, indexed)
    password_hash: str
    created_at: datetime
    updated_at: datetime

Participant (simplified for auth):

class Participant(Model):
    id: int
    email: str (indexed)
    exchange_id: int (foreign key)
    # ... other fields

Session:

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):

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:

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

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

@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.

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
  • 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