Auth & sessions
Tiger ships the auth you'd otherwise spend a month building — and it's the good version, not a cosmetic one. The design principle: identity, credentials, and challenges are three different things, so they get three different tables.
Identity ≠ credentials ≠ challenges
| Table | What | Why separate |
|---|---|---|
user |
who you are | pure identity — no password lives here |
user_credential |
how you prove it | 1-to-many factors: password, sms, totp, webauthn, oauth |
auth_challenge |
one-time proofs in flight | OTP / reset / magic-link — hashed, single-use, TTL, attempt-limited |
Modern auth is multi-factor by nature — a person might have a password and TOTP and a passkey and SSO, each with its own state. That's a collection, so credentials are a one-to-many table. Adding a new factor type is new rows, not a schema change.
Passwords, done to spec
- Bcrypt with a per-password salt (built into
password_hash) plus an install-wide pepper — a secret HMAC'd in, kept out of the DB (inlocal.ini). A stolen credential table can't be cracked without the pepper. - Policy is configurable (min length, history/reuse prevention, optional complexity/expiry — NIST 800-63B-informed defaults).
- Brute-force lockout + constant-time verification (no user enumeration — a wrong email and a wrong password fail identically).
The pepper is minted randomly at install (bin/tiger install:secrets) and rotates with no downtime and no forced resets — old pepper kept as a verify-only fallback, each password re-peppered on its owner's next login. (A dedicated Security overview page is coming.)
Sessions
- Database-backed — required behind a multi-instance load balancer (a file session on box A is invisible to box B). Falls back to file handling for a single-box/fresh install.
- Tiered idle TTLs by privilege — admins/superadmins/developers get the short, sensitive tier; regular users a longer one. The max timeout is overridable live from the admin Settings screen (no deploy).
- Auto-logout on inactivity — the enterprise kind. Server-authoritative (the browser polls
/auth/sessionfor the server's remaining time, so a paused tab or tampered client can't extend it), the poll reads the clock without resetting it, and when it fires you choose full logout or lock screen — all configurable live. A humane warning modal offers "stay signed in" before anything happens.
Beyond passwords
Tiger ships these flows, all on the same auth_challenge substrate:
- Two-factor (TOTP) — RFC 6238, dependency-free, verified against the RFC test vectors. Enrollment is a self-service wizard (QR + recovery codes); the secret is encrypted at rest.
- Passwordless code login — "email me a code," a 6-digit one-time code (attempt-limited, 10-min expiry). The verify path is channel-agnostic, so SMS is a small add.
- Self-service reset — an emailed tokenized link (no account enumeration) → a policy-checked reset screen. Single-use; a weak password doesn't burn the token.
- Login audit log — every attempt (success/failure/locked, IP, user agent), append-only.
- API tokens — a
personal_access_tokenfactor:Authorization: Bearer …authenticates a non-browser client statelessly, with no session at all. See API tokens.
How auth is wired
Auth is a reserved kernel service — it is not reachable through the public /api gateway (that would be a security hole). The login/logout/unlock screens post to a thin auth controller that returns the same JSON envelope:
// Sign-in is AJAX to /auth/login (not /api), same JSON contract as every service call.
fetch('/auth/login', {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
body: new URLSearchParams({ identifier: email, password: pw }),
}).then(r => r.json()).then(res => { if (res.result === 1) location = res.redirect; });
In-process, orchestration lives in Tiger_Service_Authentication (resolve identity → check factor → issue session). You rarely call it directly — the shipped screens already do. What you do touch is Authorization: once someone's signed in, the ACL decides what they can reach.