Security

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.

EndpointLimit
POST /auth/register5/min/IP
POST /auth/login10/min/IP
GET /auth/social/{provider}/begin10/min/IP
POST /oauth/token30/min/IP
OTP sends (email/phone/passkey-begin)3/hour/IP + 3/hour/user/channel
POST /console/projects5/hour/IP
POST /console/projects/{id}/clients20/hour/IP

OTP codes additionally: ≤5 verification attempts, hashed at rest.

Token lifetimes

TokenLifetime
Authorization code90 s, single-use
Access token15 min
Refresh token30 days, rotating
Browser session30 days, sliding
Email/phone OTP5 min, ≤5 attempts
Password reset (email link)1 h · (SMS code) 15 min
MFA login token5 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

  1. Verify signatures locally: RS256-only, check iss, aud, exp
  2. Key users by sub — never email/phone
  3. Store refresh tokens securely; overwrite on every rotation; never retry a failed refresh
  4. Never embed client secrets in frontend/mobile binaries — confidential secrets belong to server environments only
  5. Android: Custom Tabs via AppAuth, never WebView
  6. Treat unexpected 401s as "possibly revoked" → fall back to interactive login

Incident behaviors

EventAutomatic effect
Rotated refresh token replayedWhole family revoked for that user+client
Connection revoked / client deletedLive access tokens die immediately (jti records)
Password resetAll browser sessions of the user revoked
First-seen IP login / sensitive changeAlert email to the user + security-feed entry