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

#414 · Passkey autofill on the sign-in form (conditional mediation)

  • timeline-app
  • pull request
  • security

Closes #403 ## What changes - **Autofill on `/login`.** The username field is `autocomplete="username webauthn"`. When `PublicKeyCredential.isConditionalMediationAvailable()` is true and `PASSKEY_SIGN_IN` allows signing in with a passkey alone, the page keeps a `navigator.credentials.get({ mediation: 'conditional' })` request pending. Picking a passkey from the autofill posts to `/api/auth/passkeys/sign-in`, the same route the **Sign in with a passkey** button uses, so the server checks and second-factor rules are the same. - **Challenge lifetime: a new challenge on a timer, not a longer-lived one.** Challenges keep their five minutes and their single use, and the server doesn't change. The new `platform/lib/passkey-autofill.js` asks `/sign-in/options` for a new challenge: - 30 s before the current one expires; - straight away after a refused sign-in, then after 5 s, doubling up to 5 min, if refusals follow (failures to fetch options, and the browser's own rejections, also back off this way); - when the page comes back into view and its challenge is stale, or when the page is restored from the back-forward cache. A hidden page fetches nothing. An open page costs about one options request every 4.5 min, well inside the `passkey-options: ` limit of 10 a minute. Replay protection is unchanged, and there's no new kind of challenge to secure. - **Aborting.** Each round has its own `AbortController`. The pending request is aborted before the button's modal request, before a password submit, when the form changes mode, and on unmount. AbortError is swallowed. If the button or password sign-in fails, the autofill starts again. - **Fallback.** Nothing changes without browser support, with `PASSKEY_SIGN_IN=false`, or without JavaScript: the field stays plain `username` and nothing is fetched. - **Docs.** The Passkeys section of `docs/authentication.md` has a new paragraph on autofill. The OpenAPI description of `/sign-in/options` has one more sentence; the API itself doesn't change. - **AAGUID model names: left out.** The `passkeydeveloper/passkey-authenticator-aaguids` repository has no LICENSE file and no licence statement in its README (GitHub reports no licence), so bundling its list isn't clearly allowed. Writing a list by hand risks wrong names. ## Tests - New `test/passkey-autofill.test.mjs` (7 tests, on a manual clock): - the challenge is refreshed before it expires; - a hidden page fetches nothing, and a page that comes back with a stale challenge gets a new one; - a bfcache restore starts a new round; - a refused sign-in gets a new challenge straight away, and later refusals wait; - repeated failures wait longer each time; - pause, resume and stop work; - a round aborted mid-fetch is dropped. - `test/passkeys-live-postgres.mjs` (Chromium virtual authenticator) now also covers: - signing in from the autofill; - the button still working while a conditional request is pending (the request is aborted first); - the register form not getting the `webauthn` token; - a removed passkey refused through the autofill, followed by a new challenge. Chromium's virtual authenticator answers a conditional request as soon as it's made, focused or not, so the test switches automatic presence off for the password and button steps. - Passed: `node --test test/*.test.mjs` (416/416), `passkeys-postgres.mjs`, `account-security-postgres.mjs`, `passkeys-live-postgres.mjs`.

Sources

Open #414 · Passkey autofill on the sign-in form (conditional mediation) in the timeline