Repository Public
1# Authentication23Single administrator. Password plus a mandatory second factor (TOTP or passkey); a passkey with user4verification is also accepted on its own. Source: `apps/server/src/security/*`,5`apps/server/src/modules/auth/*`; web pages `apps/web/src/app/(auth)/{setup,login}`.6The threat model and the test mapping are in [security.md](security.md).78## Route authorisation levels910One global `onRequest` hook (`security/guard.ts`) applies to every path under `/api/`, including WebSocket11upgrades. A route is `full` unless it opts down.1213| Level | Requirement | Routes |14|---|---|---|15| `public` | none | `GET /health` |16| `preauth` | none to read; POSTs need `Origin` = `CR_PUBLIC_ORIGIN` and `Content-Type: application/json` | `GET /auth/status`, `POST /auth/setup`, `POST /auth/login`, `POST /auth/passkey/login/options`, `POST /auth/passkey/login/verify` |17| `restricted` | a valid session, second factor possibly pending; mutations need Origin + CSRF token | `POST /auth/totp`, `POST /auth/logout`, TOTP enrolment (`/auth/totp/enroll/start|finish`), passkey registration (`/auth/passkey/register/options|verify`) |18| `full` | a valid session **with a completed second factor**; mutations need Origin + CSRF token; WebSocket upgrades need Origin | everything else |1920Failures: 401 `unauthenticated`, 403 `second_factor_required`, 403 `bad_origin`, 403 `bad_csrf`,21403 `bad_content_type`. Unknown API paths answer a JSON 404.2223## First-run setup24251. While the `users` table is empty, the server writes a random token (32 bytes, base64url) to26 `$CR_DATA_DIR/setup-token` (mode 0600) at every start. It is never logged. **A server restart before27 setup replaces the token**, so read it just before use.282. The owner reads it on the host (`ssh M5M36 'cat ~/claude-remote-data/setup-token'`) and opens `/setup/`.293. `POST /auth/setup` with `{setupToken, username, password}`: username 3–32 characters30 (`[a-z0-9_.-]`, stored lower-case), password 12–256 characters. A wrong token is 40131 `invalid_setup_token` and counts as a failure for the caller's IP.324. The administrator row is created under an advisory lock (two concurrent setups cannot both succeed),33 the token file is deleted, and a **restricted** session is issued. The response is34 `{ok: true, next: "enroll_second_factor"}`.355. The setup page then requires TOTP or passkey enrolment. Until a second factor exists, the session can36 only reach the `restricted` routes.376. Once an administrator exists, `POST /auth/setup` answers 404 permanently, and the token file is removed38 at every start.3940## Login flows4142`POST /auth/login` returns a `LoginStep` (`{ok, next}`):4344| Situation | `next` | Session issued |45|---|---|---|46| Wrong username or password | — (401 `invalid_credentials`; same answer and same work for an unknown user) | none |47| Password correct, no second factor enrolled | `enroll_second_factor` | restricted |48| Password correct, body also carries a valid `totp` | `done` | full |49| Password correct, TOTP enrolled | `totp` → `POST /auth/totp {code}` | restricted, upgraded on success |50| Password correct, only passkeys enrolled | `passkey` → passkey login options/verify | restricted, upgraded on success |5152Passkey-first login (no password): `POST /auth/passkey/login/options` without a session returns options53for discoverable credentials (no credential ids are disclosed) and sets a short-lived cookie54`__Host-cr_pre`; the challenge is stored server-side in `preauth_challenges`, keyed by the hash of that55cookie. `POST /auth/passkey/login/verify` consumes the challenge and, on a valid assertion with user56verification, issues a full session.5758When a restricted session is upgraded, its token and CSRF token are replaced (new cookie). When a new59session is issued to a caller that already presents one, the old one is revoked.6061## Second factors6263| Factor | Details |64|---|---|65| TOTP | RFC 6238, HMAC-SHA1, 6 digits, 30 s step, ±1 step accepted. Secret generated server-side, shown once (secret + `otpauth://` URL; the web client renders the QR code locally). Enrolment must be confirmed with a valid code within 10 minutes. Stored sealed with AES-256-GCM (`CR_SECRET_KEY`), bound to the user id. Each time-step counter is accepted once (`totp_last_counter`, conditional update). |66| Passkey | `@simplewebauthn/server` v14. RP ID = hostname of `CR_PUBLIC_ORIGIN`, expected origin = `CR_PUBLIC_ORIGIN`, resident key and user verification required. Challenges are server-side, single-use, valid 5 minutes. Signature counter stored and checked. Several passkeys can be registered, each with a label. |6768Rules enforced by the server:6970- Enrolling a new factor requires a full session — or a restricted one only while the account has no71 factor at all (first run). A password alone can never attach a factor to an account that already has one.72- The last remaining second factor cannot be removed (`409 last_second_factor`): `DELETE /auth/totp`73 requires at least one passkey; deleting a passkey requires TOTP or another passkey.7475## Sessions and cookies7677| Property | Value |78|---|---|79| Cookie | `__Host-cr_sid` — HttpOnly, Secure, SameSite=Strict, Path=/, no Domain. Outside production on `http://localhost` only, the name is `cr_sid` and `Secure` is off. |80| Token | 32 random bytes; only its SHA-256 is stored (`auth_sessions.token_hash`) |81| CSRF token | 24 random bytes per session, returned by `GET /auth/status`, sent as `X-CSRF-Token`; the web client keeps it in memory only |82| Idle timeout | 12 hours, sliding (`last_seen_at` refreshed at most once a minute on HTTP requests and WebSocket upgrades) |83| Absolute lifetime | 30 days |84| Before the second factor | 15 minutes |85| Revocation | logout; `DELETE /auth/devices/:id` for a session; password change revokes every other session |86| WebSockets | registered per auth session; closed with 4401 as soon as the session is revoked, and re-checked every 60 s for expiry |8788`GET /auth/status` returns `setupRequired`, `authenticated` (true only with a completed second factor),89the user, enrolled methods (only for a caller with a session), the CSRF token and the effective expiry.9091`GET /auth/devices` lists passkeys and active browser sessions (user agent, IP, last use) and marks the92current one.9394## Throttling and rate limits9596Failure throttling (`security/throttle.ts`, table `login_attempts`, survives restarts) applies to97password, TOTP, setup-token, passkey verification and password change:9899| Key | Limit per 15 minutes |100|---|---|101| username | 5 failures |102| client IP | 10 failures |103| global | 100 failures |104105At the limit the key is locked for 60 s, doubling with each further failure, capped at 15 minutes. The106response is 429 with `Retry-After` and `{error:{code:"locked",details:{retryAfterSec}}, ok:false, next:"done", retryAfterSec}`.107A success clears the username and IP counters (never the global one). A correct password followed by a108pending second factor does not count as a success, so re-entering the password cannot reset the counter109that protects the 6-digit code.110111Request rate limits per minute (`app.ts`): credential endpoints 20 per client IP (one shared bucket),112anonymous `GET /auth/status` 240 per IP, `GET /health` 600 per IP, all other API calls 600 per session.113114The client IP is the socket peer unless the peer is listed in `CR_TRUSTED_PROXIES`, in which case115`X-Forwarded-For` is honoured.116117## Password118119argon2id through Node's built-in `crypto.argon2` when available (64 MiB, 3 passes, parallelism 1),120otherwise scrypt (N = 2^17, r = 8, p = 1); stored as a PHC-style string with its parameters. Hashes are121upgraded at login when the cost parameters changed. `POST /auth/password` requires the current password122and revokes all other sessions.123124## Audit125126Every authentication event is written to `audit_events` with actor, client IP and outcome127(`auth.setup`, `auth.login`, `auth.totp`, `auth.passkey.login`, `auth.passkey.register`,128`auth.totp.enroll.*`, `auth.totp.delete`, `auth.device.delete`, `auth.password`, `auth.logout`).129Secrets never appear in the audit detail or in logs.130131## Owner first-run steps on production1321331. `ssh M5M36 'cat ~/claude-remote-data/setup-token'`1342. Open `https://claude.spboucher.ai/setup/`, paste the token, create the administrator (password ≥ 12 characters).1353. Enrol a passkey and/or TOTP (mandatory).1364. Create a project, start a Claude session, run `/login` inside it (Claude Code's own authentication).1375. Copy `~/.config/claude-remote/backup.pass` from the node to a safe place.138139## Limitations / not verified140141- As of 2026-11-10 no administrator account exists on production; the steps above are pending.142- Passkey registration and login were verified against a software authenticator143 (`apps/server/src/testkit/soft-authenticator.ts`), not in a real browser or on a real device.144- There is no "forgot password" or recovery-code flow. Recovery requires direct database access on the145 node (see [backup-recovery.md](backup-recovery.md)).146- Idle time is refreshed by HTTP requests and WebSocket upgrades, not by traffic on an already open147 WebSocket. A terminal left open is closed (4401) once the session's idle or absolute limit passes without148 any other request from that browser.149- There is a single account; no roles.150