timescale.info
Sign inSign up

How OpenChronology was built

Every merged pull request of openchronology/timeline-app, its first week of commits and its releases; and before them the 2020 PureScript editor, from snapshots of its deleted repositories in the Wayback Machine. Written to this server through the HTTP API.

327 moments · 2020-03-09T00:15:08Z CE – 2026-11-02T20:11:21Z CE · by @athan · @athan

#402 · Passkeys (WebAuthn) for two-factor and sign-in

  • timeline-app
  • pull request
  • security

Fixes #372 ## What - **Manage passkeys** under Account → Security: add (name + the usual confirmation: password, and a code or another passkey when two-factor is on), list with added/last-used dates, rename (no re-auth), remove (confirmed). Adding/removing revokes other sessions and pending desktop sign-ins and emails the account, as TOTP does. At most 20 per account. - **Second factor**: two-factor is on while an account has an authenticator app *or* a passkey (`mfa_enabled` keeps that meaning; `mfa_secret IS NOT NULL` is "app set up"). The first passkey of an account without an app turns two-factor on and shows ten recovery codes; removing the last one (with no app) turns it off. Removing the app with passkeys left keeps two-factor and the codes. The sign-in's `mfa` step offers **Use a passkey instead**; recovery codes still work. - **Everywhere a code is accepted** (`security.fresh`: security changes, API keys, unlinking, account deletion, `/admin` changes and moderation decisions) a `passkey` answer is accepted instead. The `Confirmation` fields get a **Use a passkey** button; in `/admin`, leaving the code empty asks for a passkey. - **Passwordless**: **Sign in with a passkey** on `/login` with discoverable credentials. Site setting `PASSKEY_SIGN_IN` (default on; `false` hides it and the routes answer 404). A UV passkey counts as both factors, so the session is `mfa_verified`. ## Design decisions - **Own verification, no library**: `server/webauthn.mjs` on `node:crypto` only (strict CBOR decoder, COSE → JWK for EdDSA/Ed25519, ES256/P-256, RS256 ≥2048). The project keeps dependencies minimal and in-house (TOTP, three lockfiles, license checks), so a new dependency did not seem worth it. Attestation `none`, not verified; AAGUID stored unverified. - **Checks**: origin must equal `APP_ORIGIN` exactly (no cross-origin), RP ID = its host, UP and UV flags required, BS without BE refused, signature over authData‖SHA-256(clientData), sign-count must increase when either count is non-zero (else refused as possibly copied). - **Challenges**: `oc_passkey_challenges`, 256-bit, stored hashed, five minutes, bound to the session (`register`, confirming) or the sign-in browser nonce (`sign-in`, two-factor step), and deleted on their own connection when answered, so a failed or rolled-back change still uses them up. - **Rate limits** (`oc_auth_attempts`): passwordless options/sign-in share the password sign-in's per-address limit; enrollment start and removal count as security changes (per address and per account); finish/confirm/rename have 30/min per account. - **Setting**: site-wide env only, not per account: a UV passkey is stronger than password+TOTP, and an account that doesn't want it can just not hold a passkey. - **Migration**: idempotent additions to `schema.sql`: `oc_passkeys` (credential ID unique, COSE public key, algorithm, sign count, transports, AAGUID, name, created/last used) and `oc_passkey_challenges`, both cascading from `oc_users`. - **Desktop**: unchanged; it signs in through the system browser, where passkeys work. Its native form is password-only. Without a server the desktop has no accounts, so passkeys don't apply locally (documented). ## Docs `docs/authentication.md` (new "Passkeys" section), FAQ, OpenAPI (`server/openapi/passkeys.mjs`, plus `passkey` fields on confirmations, `security.passkeys`/`passkeySignIn`, account `totp_enabled`/`passkeys`), `.env.example`/compose/Debian env. **Privacy policy**: `legal/PRIVACY.md` lists stored security data, so I added the passkey data (public keys, credential IDs, counters, AAGUID/transports, names, dates) and its retention. The **effective date and `POLICY_VERSION` are unchanged**. Whether this is a material change that should re-prompt every account is for the operator to decide. ## Tests - `test/passkeys.test.mjs`: each algorithm; wrong origin/RP ID/challenge/type; UV or UP missing; cross-origin; forged signature; clone detection (equal, lower, reset-to-zero counters;…

Sources

Open #402 · Passkeys (WebAuthn) for two-factor and sign-in in the timeline