Security
What Camel Accounts enforces, and what your app must do on its side.
Server-side guarantees
Redirect URI validation — exact match
Byte-for-byte equality against each client's registered list. No wildcards, no prefix matching, no trailing-slash tolerance. Validation happens before any redirect is issued; failures return a 400 envelope instead of redirecting anywhere.
PKCE mandatory, S256 only
Every client, every request. code_challenge_method=plain is rejected outright; missing challenges are rejected outright.
Single-use short-lived codes
Authorization codes live 90 seconds in Redis and are consumed atomically — replay attempts fail.
Refresh-token rotation + family theft detection
Each refresh mints a new token; presenting a rotated one revokes the entire family (every session that client has for that user) and answers invalid_grant. Refresh tokens are stored SHA-256-hashed.
Password storage
argon2id (t=1, 64 MiB, p=4) with an optional server-side pepper; constant-time comparison. Reset flow never reveals whether an account exists (forgot-password always returns the same message).
Session cookies
camel_session: HttpOnly, Secure, SameSite=Lax, Path=/, 30-day sliding expiry, opaque UUID — nothing readable client-side. Revocable individually or wholesale (password reset revokes all).
Verification gate
Accounts without a verified email or phone can use the Cockpit but cannot complete /oauth/authorize for any app.
Disabled-client enforcement (defense in depth)
Disabled clients are refused at /authorize, re-checked at consent, and refused again at /oauth/token.
Social login trust rules
Linking into existing accounts requires a provider-verified email; unverified collisions fail closed (social_email_taken) instead of merging; new social accounts have no attackable password at all.
Passkey ceremony hygiene
Challenges are single-use (GETDEL) with 3-minute TTLs; sign-counts persist for clone detection; assertions replace password AND TOTP.
Rate limiting
Redis fixed-window per IP; fails open only if Redis itself is down.
| Endpoint | Limit |
|---|---|
POST /auth/register | 5/min/IP |
POST /auth/login | 10/min/IP |
GET /auth/social/{provider}/begin | 10/min/IP |
POST /oauth/token | 30/min/IP |
| OTP sends (email/phone/passkey-begin) | 3/hour/IP + 3/hour/user/channel |
POST /console/projects | 5/hour/IP |
POST /console/projects/{id}/clients | 20/hour/IP |
OTP codes additionally: ≤5 verification attempts, hashed at rest.
Token lifetimes
| Token | Lifetime |
|---|---|
| Authorization code | 90 s, single-use |
| Access token | 15 min |
| Refresh token | 30 days, rotating |
| Browser session | 30 days, sliding |
| Email/phone OTP | 5 min, ≤5 attempts |
| Password reset (email link) | 1 h · (SMS code) 15 min |
| MFA login token | 5 min |
Key management
- All JWTs are RS256 with a
kid; JWKS publishes keys with overlap during rotation so verifiers never see a gap. - Production signing keys are provisioned out-of-band (
JWT_PRIVATE_KEY_PATH) — dev environments auto-generate. - Verifiers must select by
kid, cache ~5 min, and support multiple simultaneous keys.
Your side of the contract
- Verify signatures locally: RS256-only, check
iss,aud,exp - Key users by
sub— never email/phone - Store refresh tokens securely; overwrite on every rotation; never retry a failed refresh
- Never embed client secrets in frontend/mobile binaries — confidential secrets belong to server environments only
- Android: Custom Tabs via AppAuth, never WebView
- Treat unexpected 401s as "possibly revoked" → fall back to interactive login
Incident behaviors
| Event | Automatic effect |
|---|---|
| Rotated refresh token replayed | Whole family revoked for that user+client |
| Connection revoked / client deleted | Live access tokens die immediately (jti records) |
| Password reset | All browser sessions of the user revoked |
| First-seen IP login / sensitive change | Alert email to the user + security-feed entry |