Conversation
Because: - Ahead of passwordless Sync (Phase 2) we need the true PRF capability of the passkey base; the registration-time prfEnabled flag under-reports it. This commit: - Requests the WebAuthn PRF extension at passkey sign-in behind a new scope-aware, default-off flag (PASSKEYS__REQUEST_PRF_AT_AUTHENTICATION). - Detects PRF support client-side, strips the output so it never reaches the server, and sends only a boolean on authentication/finish. - Rolls prfEnabled forward false->true (monotonic, best-effort) inside the verified finish flow; never blocks sign-in. - Adds Glean + StatsD telemetry to back the kill switch.
There was a problem hiding this comment.
Pull request overview
Adds opt-in PRF evaluation at passkey sign-in to measure real-world PRF capability (telemetry), while ensuring PRF outputs never reach the server and best-effort rolling prfEnabled forward upon verified authentication.
Changes:
- Introduces a default-off, scope-based config flag to request PRF at authentication (
off/keys-required/all) and threads akeysRequiredhint throughauthentication/start. - Client: detects PRF output presence, strips PRF results before sending assertions to the server, and retries once without PRF on PRF-attributable failures; adds Glean events.
- Server: optionally attaches PRF to authentication options and performs a monotonic, best-effort
prfEnabledroll-forward during verified sign-in; adds StatsD signals.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/fxa-shared/metrics/glean/web/passkey.ts | Adds new passkey sign-in PRF Glean event definitions. |
| packages/fxa-shared/metrics/glean/web/index.ts | Extends allowed string event extra keys and maps new passkey events. |
| packages/fxa-shared/metrics/glean/fxa-ui-metrics.yaml | Declares the two new passkey PRF-related event metrics and extras. |
| packages/fxa-settings/src/lib/passkeys/signin-flow.ts | Implements PRF support detection, output stripping, keysRequired hinting, and finish payload prfSupported. |
| packages/fxa-settings/src/lib/passkeys/signin-flow.test.tsx | Adds tests for PRF detection/stripping and keysRequired forwarding; adds toCoarsePlatform tests. |
| packages/fxa-settings/src/lib/passkeys/prf-fallback.ts | Adds PRF support extraction, PRF result stripping, and a sign-in get() fallback retry without PRF. |
| packages/fxa-settings/src/lib/passkeys/prf-fallback.test.ts | Adds unit tests for auth-time PRF fallback, extraction, and stripping helpers. |
| packages/fxa-settings/src/lib/glean/index.ts | Wires new passkey PRF events into the Glean submission switch. |
| packages/fxa-settings/src/lib/glean/index.test.ts | Adds tests validating the new Glean passkey PRF events and extras. |
| packages/fxa-auth-server/lib/routes/passkeys.ts | Threads keysRequired into auth start; accepts prfSupported on finish and forwards to service; adds payload validation. |
| packages/fxa-auth-server/lib/routes/passkeys.spec.ts | Adds/updates route tests for keysRequired and prfSupported forwarding. |
| packages/fxa-auth-server/config/index.ts | Adds PASSKEYS__REQUEST_PRF_AT_AUTHENTICATION flag to server config. |
| packages/fxa-auth-client/lib/client.ts | Adds keysRequired hint to beginPasskeyAuthentication and optional prfSupported to completePasskeyAuthentication. |
| libs/accounts/passkey/src/lib/webauthn-adapter.ts | Adds PRF-at-auth decision helper and optional PRF extension attachment to authentication options. |
| libs/accounts/passkey/src/lib/webauthn-adapter.spec.ts | Adds tests for PRF extension attachment and scope decision helper. |
| libs/accounts/passkey/src/lib/passkey.service.ts | Requests PRF based on scope/keysRequired and performs best-effort monotonic roll-forward of prfEnabled. |
| libs/accounts/passkey/src/lib/passkey.service.spec.ts | Adds tests for PRF gating metrics and prfEnabled roll-forward behavior. |
| libs/accounts/passkey/src/lib/passkey.repository.ts | Adds monotonic updatePasskeyPrfEnabled DB update helper. |
| libs/accounts/passkey/src/lib/passkey.repository.in.spec.ts | Adds integration tests for monotonic prfEnabled roll-forward semantics. |
| libs/accounts/passkey/src/lib/passkey.provider.ts | Extends raw config typing to include requestPrfAtAuthentication. |
| libs/accounts/passkey/src/lib/passkey.provider.spec.ts | Adds validation coverage for requestPrfAtAuthentication scope values. |
| libs/accounts/passkey/src/lib/passkey.manager.ts | Adds setPasskeyPrfEnabled manager method wrapping the monotonic repository update. |
| libs/accounts/passkey/src/lib/passkey.manager.spec.ts | Adds unit tests for setPasskeyPrfEnabled. |
| libs/accounts/passkey/src/lib/passkey.manager.in.spec.ts | Updates integration config fixtures to include requestPrfAtAuthentication. |
| libs/accounts/passkey/src/lib/passkey.config.ts | Adds PRF auth scopes/types and config field validation. |
| libs/accounts/passkey/src/lib/passkey.challenge.manager.spec.ts | Updates config fixtures to include requestPrfAtAuthentication. |
| libs/accounts/passkey/src/lib/passkey.challenge.manager.in.spec.ts | Updates integration config fixtures to include requestPrfAtAuthentication. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const requestPrf = shouldRequestPrfAtAuth( | ||
| this.config.requestPrfAtAuthentication, | ||
| keysRequired | ||
| ); | ||
| if (requestPrf) { | ||
| this.metrics.increment('passkey.prf.signin.requested'); | ||
| } |
vpomerleau
left a comment
There was a problem hiding this comment.
Looking good! Just a few question, and I'll take this for a manual test run before circling back with approval!
| // Keep the real UserVerificationRequiredError class (used for instanceof checks | ||
| // in the service) while stubbing the adapter's ceremony functions. | ||
| jest.mock('./webauthn-adapter', () => ({ | ||
| // shouldRequestPrfAtAuth is a pure scope-decision helper with no deps; use |
There was a problem hiding this comment.
Is this comment needed?
| keysRequired | ||
| ); | ||
| if (requestPrf) { | ||
| this.metrics.increment('passkey.prf.signin.requested'); |
There was a problem hiding this comment.
To align with other metrics names, should this be passkey.authentication.prf.requested?
| * Whether to request the PRF extension at sign-in for the given scope. | ||
| * `keysRequired` only matters under the `keys-required` scope. | ||
| */ | ||
| export function shouldRequestPrfAtAuth( |
There was a problem hiding this comment.
This looks at the config - would it also be a good spot to centralize the flag and salt check?
| .unknown(true) | ||
| .required(), | ||
| }) | ||
| .unknown(true) |
There was a problem hiding this comment.
We may want to remove this unknown(true) as defence in depth to make sure that PRF output never reaches the server (or Sentry). We have stripUnknown set to true, so Joi should simply ignore any additional values that we aren't using without causing any breakage.
| case 'passkey_signin_prf_support': | ||
| passkey.signinPrfSupport.record({ | ||
| supported: gleanPingMetrics?.event?.['supported'] || '', | ||
| platform: gleanPingMetrics?.event?.['platform'] || '', |
There was a problem hiding this comment.
Is recording a platform extra needed, considering that Glean's built-in client_info.os is attached to every ping automatically?
Because
extension at sign-in. Ahead of that we need to learn the true PRF capability
of the existing passkey base as an early telemetry signal.
prfEnabledflag is an incomplete, false-negativesignal: it reads "no" for passkeys created before PRF-at-create was enabled,
for platforms that only expose PRF at authentication, and for capability that
appears later via an OS update. PRF capability is not fixed at creation for
synced/platform passkeys, so evaluating at sign-in is the only way to learn
the real capability of the base.
This pull request
PASSKEYS__REQUEST_PRF_AT_AUTHENTICATION(
off/keys-required/all) inpackages/fxa-auth-server/config/index.tsand
libs/accounts/passkey/src/lib/passkey.config.ts, and requests the PRFextension in
generateWebauthnAuthenticationOptions(
libs/accounts/passkey/src/lib/webauthn-adapter.ts) using the same staticsalt as registration. The scope decision is threaded via an optional
keysRequiredhint onPOST /passkey/authentication/start.packages/fxa-settings/src/lib/passkeys/signin-flow.ts, detects PRFsupport from the
get()output, strips the output (stripPrfResultsinprf-fallback.ts) so it never reaches the server, and sends only aprfSupportedboolean onauthentication/finish. Adds a silent no-PRF retry(
getCredentialWithPrfFallback).prfEnabledforward false→true inside the verifiedverifyAuthenticationResponseflow(
libs/accounts/passkey/src/lib/passkey.service.ts) — monotonic(
updatePasskeyPrfEnabledinpasskey.repository.ts) and best-effort, so afailure never blocks a verified sign-in.
passkey.signin_prf_support,passkey.signin_retry_without_prf_request) and server StatsD(
passkey.prf.signin.*) to back the kill switch.Issue that this pull request solves
Closes: FXA-14085
Checklist
Put an
xin the boxes that applyHow to review (Optional)
webauthn-adapter.ts(PRF eval + scope helper),passkey.service.ts(roll-forward in the verify path),signin-flow.ts+prf-fallback.ts(detect / strip / retry), androutes/passkeys.ts(starthint + finish
prfSupported).and the monotonic, best-effort roll-forward.
Screenshots (Optional)
No user-facing UI changes.
Other information (Optional)
new session-token
PATCH /passkey/{credentialId}/prf-enabledendpoint, thisrecords capability via a
prfSupportedboolean on the existingauthentication/finishrequest — smaller surface, no extra round trip, and noownership re-check needed since the assertion already proves credential control.
off; the intended first enablement iskeys-required(Sync flows, lowest blast radius), widening toalllater forfuller population telemetry.