Sign-in: passkeys, setup tokens, Installations #19

Closed
kayg wants to merge 1 commit from issue/5-sign-in into dev
Owner

Closes #5

Passkey sign-in, setup tokens and Installations (ADR 0029, 0030, 0033, 0007).

What's in

  • crates/auth (calternal-auth): global auth.db (WAL, synchronous=FULL) with users (UUID), passkeys, setup tokens and Installations. Secrets are stored only as BLAKE3 hashes.
  • First owner: on first start the server logs a long setup token (52 characters in groups of four, no look-alike letters). A restart replaces it. It works once and expires after 24 hours.
  • Recovery: calternal user recover <name> prints a fresh token that adds a new passkey for that person. Old passkeys stay.
  • Passkeys: webauthn-rs. Registration asks for a discoverable passkey, so signing in is one button with no username. User verification is required, and the signature counter is updated.
  • Installations: every sign-in creates one, with a revocable token. Browsers get an HttpOnly; SameSite=Lax cookie (Secure on https), and other clients can send Authorization: Bearer. Sessions slide: 90 days from last use.
  • Routes /api/v1/auth/: status, setup/start|finish, login/start|finish, logout, me.
  • Actions (the account plugin): account.installations, and account.revoke, which is journaled and takes effect immediately.
  • Server: Authenticator is now async, and real sign-in replaces NoAuth. --dev-user stays loopback-only. New --origin flag (env CALTERNAL_ORIGIN): passkeys are bound to its host, and plain http is only allowed for localhost.
  • Catalogue: setup_token_invalid, passkey_failed, forbidden, too_many_requests, too_large, all in plain words.

Abuse and race tests

  • 16 concurrent setup finishes with the same token: exactly one wins.
  • Replayed and swapped prompts, expired tokens and sessions, sign-out, revoking another device.
  • Bodies over 64 KiB, bad JSON and unknown fields, non-JSON posts, four foreign origins on every POST.
  • Seven forged cookie shapes and a bad bearer token, junk credentials (empty, truncated, wrong user handle).
  • Setup-failure rate limit, a cap on open prompts, and no name leaking before sign-in.

69 workspace tests green. clippy -D warnings and fmt are clean.

Needs: OpenSSL headers to build (webauthn-rs). CI installs libssl-dev if the image lacks it.

🤖 Generated with Claude Code

https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T

Closes #5 Passkey sign-in, setup tokens and Installations (ADR 0029, 0030, 0033, 0007). **What's in** - `crates/auth` (`calternal-auth`): global `auth.db` (WAL, `synchronous=FULL`) with users (UUID), passkeys, setup tokens and Installations. Secrets are stored only as BLAKE3 hashes. - **First owner:** on first start the server logs a long setup token (52 characters in groups of four, no look-alike letters). A restart replaces it. It works once and expires after 24 hours. - **Recovery:** `calternal user recover <name>` prints a fresh token that adds a new passkey for that person. Old passkeys stay. - **Passkeys:** webauthn-rs. Registration asks for a discoverable passkey, so signing in is one button with no username. User verification is required, and the signature counter is updated. - **Installations:** every sign-in creates one, with a revocable token. Browsers get an `HttpOnly; SameSite=Lax` cookie (`Secure` on https), and other clients can send `Authorization: Bearer`. Sessions slide: 90 days from last use. - **Routes** `/api/v1/auth/`: `status`, `setup/start|finish`, `login/start|finish`, `logout`, `me`. - **Actions** (the `account` plugin): `account.installations`, and `account.revoke`, which is journaled and takes effect immediately. - **Server:** `Authenticator` is now async, and real sign-in replaces `NoAuth`. `--dev-user` stays loopback-only. New `--origin` flag (env `CALTERNAL_ORIGIN`): passkeys are bound to its host, and plain http is only allowed for localhost. - **Catalogue:** `setup_token_invalid`, `passkey_failed`, `forbidden`, `too_many_requests`, `too_large`, all in plain words. **Abuse and race tests** - 16 concurrent setup finishes with the same token: exactly one wins. - Replayed and swapped prompts, expired tokens and sessions, sign-out, revoking another device. - Bodies over 64 KiB, bad JSON and unknown fields, non-JSON posts, four foreign origins on every POST. - Seven forged cookie shapes and a bad bearer token, junk credentials (empty, truncated, wrong user handle). - Setup-failure rate limit, a cap on open prompts, and no name leaking before sign-in. 69 workspace tests green. `clippy -D warnings` and `fmt` are clean. **Needs:** OpenSSL headers to build (webauthn-rs). CI installs `libssl-dev` if the image lacks it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
Sign-in: passkeys, setup tokens, Installations
Some checks failed
CI / rust (pull_request) Failing after 2s
583050df70
- New crate calternal-auth owning a global auth.db (WAL, synchronous=FULL):
  users (UUID), passkeys (webauthn-rs, discoverable, user verification
  required), setup tokens (hashed, single use, 24 h) and Installations
  (hashed revocable tokens; browser now, schema ready for native, cli,
  agent, dav).
- First start prints a long first-owner setup token in the log; a restart
  replaces it. `calternal user recover <name>` prints a recovery token.
- Routes under /api/v1/auth: status, setup/start+finish, login/start+finish,
  logout, me. HttpOnly SameSite=Lax cookies (Secure on https), bearer
  tokens for non-browser clients, JSON-only POSTs, Origin check, 64 KiB
  body cap, capped open prompts, rate-limited setup failures.
- account plugin: account.installations and account.revoke (journaled).
- Server: Authenticator is async, NoAuth default replaced by real sign-in
  (--dev-user stays loopback only), new --origin flag, friendly new error
  codes in the catalogue.
- Tests: full ceremonies with a software passkey, a 16-way race proving a
  setup token works exactly once, replayed prompts, expiry, revocation,
  and an abuse suite (oversized, bad JSON, non-JSON, cross-site, forged
  sessions, junk credentials, floods).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
Author
Owner

Superseded by a fresh PR: the forge never synced this PR after the rebase (stale head and merge base).

Superseded by a fresh PR: the forge never synced this PR after the rebase (stale head and merge base).
kayg closed this pull request 2026-10-13 16:03:54 +00:00
Some checks failed
CI / rust (pull_request) Failing after 2s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal!19
No description provided.