Skip to content

fix(passkeys): supply static PRF salt and silently retry without PRF#20786

Draft
vpomerleau wants to merge 1 commit into
mainfrom
FXA-13991-windows
Draft

fix(passkeys): supply static PRF salt and silently retry without PRF#20786
vpomerleau wants to merge 1 commit into
mainfrom
FXA-13991-windows

Conversation

@vpomerleau

Copy link
Copy Markdown
Contributor

Because

  • On Windows, Firefox passkey registration fails with "Passkey setup failed. Try again or choose another method." When the WebAuthn PRF extension is requested at registration, Windows Hello rejects an empty PRF eval (prf: {}) with an UnknownError and aborts the whole ceremony, so affected users cannot create a passkey while PRF is enabled.
  • Other platforms tolerate an empty eval, so rather than suppress PRF we supply a salt at registration — which fixes Windows and extends PRF support to it.

This pull request

  • Adds a static base64url PRF salt to the passkey config (PASSKEYS__PRF_SALT, default empty) in packages/fxa-auth-server/config/index.ts, threaded through RawPasskeyConfig and PasskeyConfig (libs/accounts/passkey/src/lib/passkey.provider.ts, passkey.config.ts) with base64url validation.
  • Updates libs/accounts/passkey/src/lib/webauthn-adapter.ts to send extensions.prf.eval.first only when requestPrfAtRegistration is on and a salt is configured, and to omit the prf extension entirely otherwise — never the empty prf: {} that breaks Windows.
  • Adds packages/fxa-settings/src/lib/passkeys/prf-fallback.ts: a client-side silent retry that re-runs navigator.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.
  • Wires the fallback into packages/fxa-settings/src/components/Settings/PagePasskeyAdd/index.tsx (swaps createCredential for createCredentialWithPrfFallback).
  • Adds unit tests for the helper and the registration page, and updates the accounts-passkey adapter/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 x in the boxes that apply

  • My commit is GPG signed.
  • If applicable, I have modified or added tests which pass locally.
  • I have added necessary documentation (if appropriate).
  • I have verified that my changes render correctly in RTL (if appropriate).
  • I have manually reviewed all AI generated code.

How to review (Optional)

  • Key files/areas to focus on: webauthn-adapter.ts (the gated PRF eval), prf-fallback.ts (retry decision + option stripping), and the PagePasskeyAdd wiring.
  • Suggested review order: config plumbing (passkey.config.tspasskey.provider.tsconfig/index.ts) → webauthn-adapter.tsprf-fallback.tsPagePasskeyAdd/index.tsx.
  • Risky or complex parts: the retry must not re-prompt on user cancel/timeout (gated to the Unexpected error 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)

  • Windows/desktop half of FXA-13991. The iOS PublicKeyCredential.toJSON() crash (the infinite-loop symptom) is fixed in a separate branch — hence Issue: rather than Closes:, so this PR does not auto-resolve the shared ticket.
  • Deploy companion (webservices-infra). PASSKEYS__PRF_SALT must be set alongside enabling PASSKEYS__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.
  • Steps to reproduce / verify. Locally, set PASSKEYS__REQUEST_PRF_AT_REGISTRATION=true and a generic base64url salt, e.g. PASSKEYS__PRF_SALT=dGVzdC1wcmYtc2FsdA (an empty PASSKEYS__PRF_SALT omits the PRF extension):
    1. Sign in to Mozilla Accounts and open Settings → Security → Passkeys.
    2. Click Create and complete the platform / Windows Hello prompt.
    3. Look up the account in the admin panel (Account Search → Passkeys table) and confirm the new passkey shows PRF Enabled = ✓ on a PRF-capable platform.
    • Expected (with this PR): the passkey is created and listed on the account.
    • Bug (on Windows, before this PR): setup fails with "Passkey setup failed. Try again or choose another method." — Windows Hello rejects the empty prf: {} eval with UnknownError.
    • Extra checks: flag on + empty salt → no prf in the registration options; a simulated UnknownError on the first create() → silent no-PRF retry still succeeds.

@vpomerleau vpomerleau requested a review from nshirley June 25, 2026 00:21
@vpomerleau

Copy link
Copy Markdown
Contributor Author

@nshirley This is an early draft, but would you mind testing if this fix works on Windows when you have a chance?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_SALT config 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.first only when both the feature flag and salt are present, otherwise omit prf entirely.
  • Introduce a Settings-side createCredentialWithPrfFallback wrapper 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.

Comment on lines +88 to +92
return await createCredential(options, timeoutMs, externalSignal);
} catch (error) {
if (isRetriableWithoutPrf(error) && options.extensions?.prf) {
return await createCredential(
stripPrfExtension(options),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants