fix(passkeys): supply static PRF salt and silently retry without PRF#20786
fix(passkeys): supply static PRF salt and silently retry without PRF#20786vpomerleau wants to merge 1 commit into
Conversation
|
@nshirley This is an early draft, but would you mind testing if this fix works on Windows when you have a chance? |
There was a problem hiding this comment.
Pull request overview
This PR addresses Windows (Firefox + Windows Hello) passkey registration failures when the WebAuthn PRF extension is requested with an empty eval (prf: {}), by (1) only requesting PRF when a configured salt is present server-side and (2) adding a client-side silent retry path that retries registration once without PRF on “unexpected” WebAuthn failures.
Changes:
- Add
PASSKEYS__PRF_SALTconfig plumbing (with base64url character validation) through the passkey config/provider and use it to gate PRF eval emission at registration. - Update the passkey WebAuthn adapter to emit
extensions.prf.eval.firstonly when both the feature flag and salt are present, otherwise omitprfentirely. - Introduce a Settings-side
createCredentialWithPrfFallbackwrapper that retries once without PRF on “Unexpected” DOMExceptions, and wire it into the passkey creation page with new/updated unit tests.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/fxa-settings/src/lib/passkeys/webauthn.ts | Updates PRF extension docs to reflect salt-gated registration behavior. |
| packages/fxa-settings/src/lib/passkeys/prf-fallback.ts | Adds retry wrapper to re-run registration once without PRF on “unexpected” failures. |
| packages/fxa-settings/src/lib/passkeys/prf-fallback.test.ts | Unit tests for retry decision logic and PRF stripping behavior. |
| packages/fxa-settings/src/components/Settings/PagePasskeyAdd/index.tsx | Switches passkey creation to use PRF fallback wrapper. |
| packages/fxa-settings/src/components/Settings/PagePasskeyAdd/index.test.tsx | Adds coverage ensuring retry-without-PRF succeeds silently on UnknownError. |
| packages/fxa-auth-server/config/index.ts | Adds PASSKEYS__PRF_SALT to server config (default empty). |
| libs/accounts/passkey/src/lib/webauthn-adapter.ts | Emits PRF eval only when enabled and salt is configured; otherwise omits PRF. |
| libs/accounts/passkey/src/lib/webauthn-adapter.spec.ts | Updates/extends adapter tests for salted PRF and omission when salt is absent. |
| libs/accounts/passkey/src/lib/passkey.service.spec.ts | Updates PasskeyConfig construction in tests to include prfSalt. |
| libs/accounts/passkey/src/lib/passkey.provider.ts | Extends raw config type to include prfSalt. |
| libs/accounts/passkey/src/lib/passkey.provider.spec.ts | Adds tests for copying prfSalt and rejecting non-base64url characters. |
| libs/accounts/passkey/src/lib/passkey.manager.spec.ts | Updates PasskeyConfig construction in tests to include prfSalt. |
| libs/accounts/passkey/src/lib/passkey.manager.in.spec.ts | Updates integration test config to include prfSalt. |
| libs/accounts/passkey/src/lib/passkey.config.ts | Adds prfSalt field with base64url-character validation and wiring in constructor. |
| libs/accounts/passkey/src/lib/passkey.challenge.manager.spec.ts | Updates test config to include prfSalt. |
| libs/accounts/passkey/src/lib/passkey.challenge.manager.in.spec.ts | Updates integration test configs to include prfSalt. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return await createCredential(options, timeoutMs, externalSignal); | ||
| } catch (error) { | ||
| if (isRetriableWithoutPrf(error) && options.extensions?.prf) { | ||
| return await createCredential( | ||
| stripPrfExtension(options), |
There was a problem hiding this comment.
Fixed — the no-PRF retry is now bounded to the remaining timeout budget, so the two attempts together never exceed a single timeout window (kept aligned with the server-side challenge TTL).
Because:
- On Windows, requesting the WebAuthn PRF extension with an empty eval
(prf: {}) makes Windows Hello reject passkey registration with
UnknownError.
This commit:
- adds a static base64url PRF salt to the passkey config
(PASSKEYS__PRF_SALT, default ''); registration options now send
prf.eval.first only when PRF is enabled and a salt is configured, and
omit the prf extension entirely otherwise.
- adds a client-side silent retry without PRF when registration fails
with a PRF-attributable unexpected error; the retry reuses the abort
signal so a user cancel or timeout never re-prompts, and is bounded to
the remaining timeout budget so the two attempts never exceed a single
timeout window (aligned with the server-side challenge TTL).
Issue: FXA-13991
55c6f10 to
a0be097
Compare
Because
prf: {}) with anUnknownErrorand aborts the whole ceremony, so affected users cannot create a passkey while PRF is enabled.This pull request
PASSKEYS__PRF_SALT, default empty) inpackages/fxa-auth-server/config/index.ts, threaded throughRawPasskeyConfigandPasskeyConfig(libs/accounts/passkey/src/lib/passkey.provider.ts,passkey.config.ts) with base64url validation.libs/accounts/passkey/src/lib/webauthn-adapter.tsto sendextensions.prf.eval.firstonly whenrequestPrfAtRegistrationis on and a salt is configured, and to omit theprfextension entirely otherwise — never the emptyprf: {}that breaks Windows.packages/fxa-settings/src/lib/passkeys/prf-fallback.ts: a client-side silent retry that re-runsnavigator.credentials.create()once without PRF when the first attempt fails with a PRF-attributable "unexpected" error. It reuses the abort signal, so a user cancel or timeout never re-prompts.packages/fxa-settings/src/components/Settings/PagePasskeyAdd/index.tsx(swapscreateCredentialforcreateCredentialWithPrfFallback).accounts-passkeyadapter/provider specs.Issue that this pull request solves
Issue: FXA-13991 (partial — Windows/desktop PRF cause only; the iOS fix is a separate branch)
Checklist
Put an
xin the boxes that applyHow to review (Optional)
webauthn-adapter.ts(the gated PRF eval),prf-fallback.ts(retry decision + option stripping), and thePagePasskeyAddwiring.passkey.config.ts→passkey.provider.ts→config/index.ts) →webauthn-adapter.ts→prf-fallback.ts→PagePasskeyAdd/index.tsx.Unexpectederror category + shared abort signal); the salt is a non-secret public value and is never used in server-side key derivation.Screenshots (Optional)
Please attach the screenshots of the changes made in case of change in user interface.
Other information (Optional)
PublicKeyCredential.toJSON()crash (the infinite-loop symptom) is fixed in a separate branch — henceIssue:rather thanCloses:, so this PR does not auto-resolve the shared ticket.PASSKEYS__PRF_SALTmust be set alongside enablingPASSKEYS__REQUEST_PRF_AT_REGISTRATION. Without a salt, PRF is gracefully omitted (no Windows breakage) but PRF is not requested. The production salt value is managed in webservices-infra, not in this repo.PASSKEYS__REQUEST_PRF_AT_REGISTRATION=trueand a generic base64url salt, e.g.PASSKEYS__PRF_SALT=dGVzdC1wcmYtc2FsdA(an emptyPASSKEYS__PRF_SALTomits the PRF extension):prf: {}eval withUnknownError.prfin the registration options; a simulatedUnknownErroron the firstcreate()→ silent no-PRF retry still succeeds.