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>
10 KiB
0002. Authentication Strategy
Date: 2025-12-22
Status
Accepted
Context
Sneaky Klaus has two distinct user types with different authentication needs:
-
Administrator: Single admin account for entire installation. Needs persistent access to manage exchanges. Must be able to recover access if password is forgotten.
-
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:
- Admin enters email and password
- Password hashed with bcrypt, compared to stored hash
- On success, session created with admin role
- 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:
- Admin requests password reset from login page
- System sends time-limited reset token (1 hour expiration) to admin email
- Reset link directs to password reset form
- Token validated, new password set
- 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:
- Participant requests access (from registration page or email)
- System generates cryptographically random token (256-bit)
- Token stored in database with 1-hour expiration
- Email sent with magic link:
/participant/auth/{token} - Clicking link validates token and creates session
- 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
secretsmodule 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.
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:
- Admin 2FA: Time-based OTP for additional admin security
- Passkeys: WebAuthn support for passwordless admin auth
- Session device tracking: Show admin active sessions and allow revocation
- Remember-me for admin: Optional extended session with re-authentication for sensitive actions
- 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