Files
web/docs/PASSKEYS.md
T
gamertan 6f0b597943
verify / verify (push) Successful in 3m39s
Add owner-assisted account recovery
2026-09-03 23:56:42 -04:00

5.8 KiB

Passkey integration

authwebauthn is a passkey ceremony service, not a login page or account policy. An application supplies its exact relying-party identity, routes, authorization decisions, session cookie, HTML, and local recovery command.

Fixed security policy

  • Use an exact HTTPS origin whose hostname equals the relying-party ID.
  • Keep production origins portless. For local development only, AllowDevelopmentPort permits one explicit non-default port when the RP ID is exactly localhost or beneath the reserved .test top-level domain. The configured origin, browser Origin, and WebAuthn verifier origin must still match exactly.
  • Reject cross-origin ceremonies.
  • Require discoverable credentials and user verification.
  • Request no attestation conveyance.
  • Permit ES256 only until another algorithm has explicit interoperability and security evidence.
  • Store random challenges and verifier session data only behind opaque, single-use ceremony tokens.
  • Treat clone warnings as audit signals rather than automatic lockout for synchronized passkeys.

The service uses a random WebAuthn challenge for every ceremony. A sensitive application operation is bound separately by storing the SHA-256 digest of its canonical payload with that ceremony. Never substitute an operation hash, timestamp, UUID, or counter for the random challenge.

Application flow

  1. A local command calls authwebauthn.Bootstrap, authwebauthn.Recover, or bootstrap.Start and writes the returned enrollment token once to a newly created mode-0600 file. Use bootstrap.Start for the first application owner so identity, organization membership, direct owner access, and audits cannot be partially committed.
  2. A server-rendered enrollment page calls BeginEnrollment; the browser uses navigator.credentials.create with the returned public_key value.
  3. The browser posts the credential and opaque ceremony token to a bounded JSON endpoint; authenticated self-service flows use FinishRegistrationForUser so the application session's user ID is checked before any public credential is stored.
  4. Login uses BeginLogin, navigator.credentials.get, and FinishLogin. The successful result contains an ordinary opaque auth session token.
  5. Sensitive operations call BeginApproval with a canonical application payload and FinishApproval with those exact same bytes. Any drift fails.

For an existing password-backed account, call BeginPasswordMigration only from an authenticated account session and finish with FinishPasswordMigration. The registration ceremony is bound to that user. Successful completion stores the passkey, removes the password credential, clears the password-change flag, revokes every session and pending ceremony, and appends the audit event atomically. The application must clear the current session cookie and return the user to passkey login after success.

authhttp.WritePasskeyBegin and authhttp.ReadPasskeyFinish provide bounded JSON framing only. They do not register routes, authorize requests, serve JavaScript, or set sessions automatically.

Recovery and credential lifecycle

Administrator-assisted authwebauthn.Recover is deliberately host-local and must never be reachable through an HTTP handler. It revokes all user sessions and pending ceremonies, replaces prior enrollment tokens, appends a secret-free audit event, and returns one 15-minute token. It does not delete existing passkeys. After enrolling a replacement, the operator reviews credential labels and removes lost keys with a fresh passkey-bound removal ceremony. The final passkey cannot be removed remotely.

An account may separately expose self-service password-plus-recovery-code recovery through authrecovery. Begin verifies the password, consumes one printable code, revokes sessions, and returns a short-lived grant—not a normal session. Keep that grant in a narrowly scoped, Secure, HttpOnly, SameSite cookie and never place it in a URL. BeginPasskey binds its digest into the WebAuthn ceremony. FinishPasskey atomically consumes the grant, stores the verified replacement passkey, replaces the entire recovery-code set, revokes any sessions or ceremonies created during recovery, and returns the new plaintext codes exactly once. It does not issue a session; return the user to normal login after displaying and saving the new codes.

A failed storage commit leaves the restricted grant available for a fresh ceremony until expiry. A binding mismatch consumes the mismatched ceremony. Applications must use generic failure responses and the same credential-attempt rate limiting as login.

Owner-assisted recovery is a third, deliberately separate path. Configure authrecovery.Options.OwnerRole, authorize an active direct organization owner, and bind that owner's fresh passkey assertion to the exact organization, target user, request identifier, and bounded human-review reason before calling IssueAssistedRecovery. The SQLite transaction rechecks the active direct owner and target membership, invalidates the target's password, passkeys, recovery codes, sessions, and pending ceremonies, then stores only a digest of the 15-minute grant with identity and organization-visible audits.

Deliver the returned grant exactly once in a URL fragment. A public recovery page can pass it to BeginAssistedPasskey and FinishAssistedRecovery while keeping it out of request URLs, referrers, and access logs. Completion consumes the grant atomically with one replacement password, passkey, recovery-code set, and both audit trails. It issues no session. Losing the fragment after issuance requires another reviewed owner or root-local recovery; old authenticators must not become valid again as a fallback.

Before enabling production mutations, applications should require at least two independent passkeys and complete a local recovery drill.