mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 08:56:41 +00:00
Replace separate JWT_EXPIRY_MAGIC_LINK (365d) and JWT_EXPIRY_PASSKEY (7d) constants with a single JWT_EXPIRY (365d). The auth method only matters during login — once a JWT is issued, it represents a verified session regardless of how the user authenticated.
181 lines
7.8 KiB
Markdown
181 lines
7.8 KiB
Markdown
# Authentication Architecture
|
|
|
|
This document explains the design decisions and security features of the Super Sync Server authentication system.
|
|
|
|
## Overview
|
|
|
|
The server uses **stateless JWT authentication** with:
|
|
|
|
- **Token versioning** for revocation (no blacklist needed)
|
|
- **Email verification** for new accounts
|
|
- **Account lockout** protection against brute force
|
|
|
|
## Design Decisions
|
|
|
|
### Why Stateless JWTs (Not Stored in DB)
|
|
|
|
JWTs are issued but never persisted to the database.
|
|
|
|
**Benefits:**
|
|
|
|
- No session store needed - scales horizontally without shared state
|
|
- Reduced DB load - no lookup on every authenticated request
|
|
- Simpler architecture - no session cleanup jobs
|
|
|
|
**Trade-off:**
|
|
|
|
- Cannot revoke individual tokens - only all tokens via version increment
|
|
- Token remains valid until expiry (365 days) unless version bumped
|
|
|
|
### Why Token Versioning (Not Blacklisting)
|
|
|
|
Each user has a `tokenVersion` integer. JWTs include this version, and verification checks it matches the current DB value.
|
|
|
|
```
|
|
Token issued with version 5 → User changes password → DB version becomes 6 → Token rejected (5 ≠ 6)
|
|
```
|
|
|
|
**Benefits:**
|
|
|
|
- O(1) storage per user (single integer vs. unbounded blacklist)
|
|
- No cleanup jobs needed (blacklists grow indefinitely)
|
|
- Instant revocation of ALL tokens with one DB update
|
|
|
|
**Trade-off:**
|
|
|
|
- Cannot selectively revoke one device's token
|
|
- All devices must re-authenticate when version changes
|
|
|
|
**When version increments:**
|
|
|
|
- Password change
|
|
- Explicit "log out all devices" action
|
|
- Token replacement (`/api/replace-token`)
|
|
|
|
### Why Verification Tokens Are Plain Strings
|
|
|
|
Email verification tokens are stored as plain 64-character hex strings (32 random bytes).
|
|
|
|
**Why this is acceptable:**
|
|
|
|
1. **Cryptographically unguessable** - 256 bits of entropy from `crypto.randomBytes(32)`
|
|
2. **One-time use** - Cleared immediately after successful verification
|
|
3. **Short-lived** - 24-hour expiry
|
|
4. **Low-value target** - Only activates an account, grants no ongoing access
|
|
|
|
**Trade-off:**
|
|
|
|
- If DB is compromised, attacker could verify pending (unverified) accounts
|
|
- Minimal impact: they still don't know the password
|
|
|
|
**Alternative considered:** Hashing verification tokens (like password reset tokens in some systems) would add complexity with minimal security benefit for this use case.
|
|
|
|
### Why bcrypt with 12 Rounds
|
|
|
|
**Why bcrypt:**
|
|
|
|
- Industry standard, battle-tested
|
|
- Built-in salt generation
|
|
- Resistant to GPU/ASIC attacks (memory-hard)
|
|
|
|
**Why 12 rounds:**
|
|
|
|
- ~250ms on modern hardware (balances security and UX)
|
|
- OWASP recommends 10+ rounds
|
|
- Adjustable if hardware improves
|
|
|
|
## Security Features
|
|
|
|
| Feature | Implementation | Value |
|
|
| ------------------------ | --------------------- | ------------------- |
|
|
| Password hashing | bcrypt | 12 rounds |
|
|
| Password minimum | Zod validation | 12 characters |
|
|
| JWT signing | HMAC-SHA256 | Secret min 32 chars |
|
|
| JWT expiry | Uniform | 365 days |
|
|
| Verification token | `crypto.randomBytes` | 32 bytes (256 bits) |
|
|
| Verification expiry | Time-based | 24 hours |
|
|
| Lockout threshold | Failed attempts | 5 attempts |
|
|
| Lockout duration | Time-based | 15 minutes |
|
|
| Timing attack mitigation | Dummy hash comparison | Always compare |
|
|
|
|
### Timing Attack Mitigation
|
|
|
|
Even when a user doesn't exist, the login flow compares the provided password against a dummy hash. This ensures the response time is consistent whether the user exists or not, preventing attackers from enumerating valid emails.
|
|
|
|
```typescript
|
|
const dummyHash = '$2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW';
|
|
const hashToCompare = user ? user.passwordHash : dummyHash;
|
|
await bcrypt.compare(password, hashToCompare);
|
|
```
|
|
|
|
## Token Lifecycle
|
|
|
|
```
|
|
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
|
│ Register │────▶│ Verification │────▶│ Verified │
|
|
│ (email + │ │ Token (24h) │ │ Account │
|
|
│ password) │ │ sent via email │ │ │
|
|
└─────────────┘ └──────────────────┘ └────────┬────────┘
|
|
│
|
|
▼
|
|
┌─────────────────┐
|
|
│ Login │
|
|
│ (email + │
|
|
│ password) │
|
|
└────────┬────────┘
|
|
│
|
|
▼
|
|
┌─────────────────┐
|
|
│ JWT (365d) │
|
|
│ contains: │
|
|
│ - userId │
|
|
│ - email │
|
|
│ - tokenVersion │
|
|
└────────┬────────┘
|
|
│
|
|
┌────────────────────────┴────────────────────────┐
|
|
│ │
|
|
▼ ▼
|
|
┌─────────────────┐ ┌─────────────────┐
|
|
│ Token expires │ │ Password change │
|
|
│ (after expiry) │ │ tokenVersion++ │
|
|
│ │ │ │
|
|
│ User must │ │ ALL tokens │
|
|
│ re-login │ │ invalidated │
|
|
└─────────────────┘ └─────────────────┘
|
|
```
|
|
|
|
## API Reference
|
|
|
|
See [README.md](../README.md) for endpoint documentation.
|
|
|
|
**Password requirements:**
|
|
|
|
- Minimum 12 characters (validated via Zod schema in `api.ts`)
|
|
|
|
**JWT Secret requirements:**
|
|
|
|
- Minimum 32 characters
|
|
- Generate with: `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`
|
|
|
|
## Configuration
|
|
|
|
All auth-related constants are defined in `src/auth.ts`:
|
|
|
|
```typescript
|
|
const MIN_JWT_SECRET_LENGTH = 32;
|
|
const BCRYPT_ROUNDS = 12;
|
|
const JWT_EXPIRY = '365d'; // All JWT tokens, regardless of auth method
|
|
const VERIFICATION_TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
```
|
|
|
|
To modify these values, edit `src/auth.ts` and rebuild.
|
|
|
|
## Future Considerations
|
|
|
|
Features not currently implemented but could be added:
|
|
|
|
- **2FA/MFA** - TOTP or WebAuthn
|
|
- **Refresh tokens** - Separate short-lived access + long-lived refresh
|
|
- **Per-device token revocation** - Track device IDs in JWT
|
|
- **Rate limiting per-user** - Currently only IP-based for auth endpoints
|