Authentication
Status: Complete
Chatalot uses a JWT-based authentication system with Ed25519 signing, Argon2id password hashing, refresh token rotation, and optional TOTP two-factor authentication.
Overview
- Access tokens: Short-lived JWTs (15 minutes), signed with Ed25519
- Refresh tokens: Long-lived (30 days), stored as SHA-256 hashes in PostgreSQL
- Token rotation: Each refresh token use issues a new pair and revokes the old refresh token
- Password hashing: Argon2id (m=65536/64MiB, t=3, p=4, output=32 bytes)
- 2FA: TOTP with 10 backup codes
JWT Access Token
Algorithm: EdDSA (Ed25519)
Key files: ./secrets/jwt_private.pem (signing), ./secrets/jwt_public.pem (verification)
Claims
| Claim | Type | Description |
|---|---|---|
sub |
UUID | User ID |
username |
String | Username |
is_admin |
bool | Instance admin flag |
is_owner |
bool | Instance owner (god role) |
iat |
i64 | Issued-at timestamp |
exp |
i64 | Expiration (iat + 900s) |
jti |
UUID | Unique token ID |
Lifetime: 15 minutes (900 seconds) Clock skew tolerance: 60 seconds
Refresh Token
- 32 bytes of cryptographic randomness (from OsRng)
- Returned to client as hex-encoded string (64 characters)
- Stored in DB as SHA-256 hash (never stored in plaintext)
- Lifetime: 30 days
- Tracks: device_name (from User-Agent), ip_address, created_at, expires_at, revoked_at
Token Rotation
On each refresh, the old token is immediately revoked (revoked_at set) and a new pair is issued. This ensures each refresh token is single-use. If a revoked token is presented, all of that user's sessions may be invalidated (replay detection).
Password Requirements
- Length: 8-128 characters
- Must contain: 1 uppercase, 1 lowercase, 1 digit, 1 special character
- Hashed with Argon2id before storage
Username Rules
- 3-32 characters
- Allowed: letters, numbers, underscores, hyphens, dots
- Must start with letter or number
- Must not end with dot
- No consecutive dots
Account Lockout
- Max failed attempts: 10
- Lockout duration: 15 minutes
- Tracked in memory (resets on server restart)
- Shared between login and recovery endpoints
Registration Flow
- Validate username, email, password
- Validate cryptographic keys (identity key, signed prekey, one-time prekeys)
- Check uniqueness (username, email)
- Validate and consume invite code (if registration mode is invite_only)
- Hash password with Argon2id
- Compute identity key fingerprint (SHA-256 of public key)
- Create user record
- Store signed prekey and one-time prekeys (100 initial)
- First user automatically becomes admin + owner
- Issue access token (15 min) + refresh token (30 days)
- Generate one-time recovery code
- Log audit event (
register)
Login Flow
- Check account lockout (10 attempts / 15 min)
- Find user by username
- Verify password (Argon2id constant-time comparison)
- If TOTP enabled: verify TOTP code or backup code
- Check account suspension (suspended_at not null → reject)
- Clear lockout tracking on success
- Issue new access + refresh tokens
- Log audit event (
loginorlogin_failed)
Refresh Flow
- Client sends hex-encoded refresh token to
POST /auth/refresh - Server SHA-256 hashes the token
- Looks up hash in refresh_tokens table
- Validates: not expired, not revoked, user not suspended
- Revokes old refresh token (sets revoked_at)
- Issues new access token (15 min) + new refresh token (30 days)
- Returns both tokens
TOTP Two-Factor Authentication
- Secret stored as BYTEA (optionally encrypted with TOTP_ENCRYPTION_KEY)
- 10 backup codes generated in XXXX-XXXX format
- Backup codes SHA-256 hashed before storage
- Backup codes are one-time use (removed from array on use)
- Enable via
POST /auth/totp/setup, verify withPOST /auth/totp/verify - Disable via
POST /auth/totp/disable
Account Recovery
- Recovery code shown once at registration (XXXX-XXXX-XXXX-XXXX format, base32 charset)
- Stored as SHA-256 hash in recovery_code_hash column
- Recovery resets password, revokes ALL refresh tokens, generates new recovery code
- Rate-limited same as login (10 attempts / 15 min)
Auth Middleware
The auth middleware:
1. Extracts Bearer token from Authorization header
2. Decodes JWT with Ed25519 public key (with 60s clock skew leeway)
3. Inserts JWT claims into request extensions
4. Skipped for public routes: /auth/*, /health, /legal
WebSocket Authentication
WebSocket connections authenticate via the first message (not headers):
1. Client opens WS connection to /ws
2. Client sends {"type": "authenticate", "token": "..."}
3. Server validates JWT and responds with authenticated or error
4. 10-second timeout for auth message
Session Management
- One refresh token per device/login
- Device name parsed from User-Agent
- IP from X-Forwarded-For or X-Real-IP headers
- Users can view active sessions and revoke individual or all-except-current
Key Constants
| Constant | Value |
|---|---|
| ACCESS_TOKEN_LIFETIME | 900s (15 min) |
| REFRESH_TOKEN_LIFETIME | 2,592,000s (30 days) |
| LOCKOUT_MAX_ATTEMPTS | 10 |
| LOCKOUT_DURATION | 900s (15 min) |
| CLOCK_SKEW_TOLERANCE | 60s |
| INITIAL_ONE_TIME_PREKEYS | 100 |
| PREKEY_LOW_THRESHOLD | 20 |