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 (in local.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/session for 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_token factor: 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.