Discoverable passkey login, replacing the password #115
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
rosa/vernier!115
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "passkey-login"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Replaces username + Argon2 password authentication with discoverable (usernameless) WebAuthn passkeys as the sole login factor. Registration collects a username + email (still email-confirmed) and enrols a passkey; login is a one-tap discoverable ceremony that resolves the account from the credential's user handle. A lost passkey is recovered by an email magic-link that re-enrols a new passkey. The password machinery and the account-lockout mechanism (ADR-0011) are removed entirely.
Implements the plan in
thoughts/shared/plans/011_discoverable_passkey_login_replacing_password.md.Phases
webauthn-rs0.5, a config-derivedPasskeyAuthenticator, the passkey domain model + errors, thepasskey_credentialstable + a random v4webauthn_handleonusers, and the repo methods with mocks + Postgres conformance tests.begin/complete_passkey_registration, discoverable + username-first login, atomiccreate_user_with_passkey, counter/clone-detection./register+/loginrewritten to the passkey ceremonies (/options+/verifyJSON endpoints),assets/passkey.js, a shared session-establishment helper, and the CSRF/rate-limit wiring./accounts/edit(add/remove, never the last one) and email recovery via a newPasskeyRecoveryconfirmation action, with a dedicated governor + per-account Displace throttle + neutral flash.password_hash/failed_login_attempts/locked_untilcolumns, dropargon2, mark ADR-0011 superseded, and add ADR-0014.Notable decisions
webauthn_handle, not the v7UserId— the handle is surfaced to authenticators in every assertion, so using the primary key would leak the account-creation timestamp and correlate credentials.residentKey: preferred) for authenticators with no free resident slot. The username-first endpoint answers unknown usernames with a shape-matching decoy challenge (enumeration-safe).AppService), sincewebauthn-rsis a side-effect-free crypto library and the logic is authentication policy.Testing
mise ciis green (audit, fmtcheck, lint, full suite), plusclippy,format, andsqlcheck. Registration, username-first login, recovery, the enumeration decoy, stale-state rejection, and CSRF are covered end to end by a soft-authenticator (webauthn-authenticator-rs) driving the real HTTP surface.Manual verification still required (the soft authenticators can't drive the discoverable, empty-allow-list flow): register with a platform authenticator → confirm email → log out → discoverable one-tap login → recovery, and confirm no password field appears anywhere.
Migration notes
Greenfield — no production accounts. Migrations are additive then subtractive: Phase 1 adds
passkey_credentials+webauthn_handle; Phase 5 drops the password/lockout columns..sqlxregenerated.Let a signed-in user list, add, and remove passkeys from /accounts/edit. - Domain: list_passkeys, begin/complete_passkey_addition (add a credential to the existing user, reusing their handle and excluding held credentials), and delete_passkey with a last-passkey guard (DeletePasskeyError::LastPasskey). CredentialId gains url-safe encoding for the delete route. - Web: /accounts/passkeys/options+/verify (a new AddPasskey parked ceremony behind AuthUser) and /accounts/passkeys/{id}/delete. The account page gains a Passkeys section (list with add/remove), and passkey.js an add-passkey button. - Integration tests: a second passkey can be added and removed; the last passkey cannot be deleted.AddPasskeyBody.label was read by post_add_passkey_options, but passkey.js only ever posts {} and there is no rename surface, so the field was an untested input path. Remove the DTO, drop the Json body extractor, and enrol with PasskeyLabel::default() (as recovery already does), parking no label with the AddPasskey ceremony.338f8ca7ac99dfb05924View command line instructions
Manual merge helper
Use this merge commit message when completing the merge manually.
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.