Skip to content

feat(settings): wire up the password-free passkey opt-in - #21223

Merged
vpomerleau merged 1 commit into
mainfrom
FXA-13151
Sep 21, 2026
Merged

vpomerleau merged 1 commit into
mainfrom
FXA-13151

Conversation

@vpomerleau

@vpomerleau vpomerleau commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Because

  • A desktop browser sign-in that needs encryption keys (Sync, for example) still requires a password after a PRF-capable passkey until a wrap is stored, and sign-in is the one place the user has everything needed to store one.
  • The proof minted at sign-in lives ten minutes and the password step can outlast it.
  • Behind the passkeyPasswordlessSyncEnabled flag; nothing renders when it is off.

This pull request

  • Requests the passkey scope on desktop Sync sign-ins and holds the PRF output, proof and kB in SensitiveDataClient.
  • Skips the offer when the account has no password yet, the passkey already has a wrap, the client is mobile, or the service is not Sync.
  • Routes eligible sign-ins to /inline_passwordless_sync_setup in place of the browser's post-sign-in landing page.
  • Adds the opt-in container: stores the wrap, lands in Settings with a success or error alert, and zeroes the held material on every exit.
  • Steps up with the passkey for a fresh proof when the sign-in one has expired, then submits the same envelope again.
  • POST /passkey/wraps replaces a stored wrap that predates keysChangedAt instead of refusing it, deleting only the row it read; the opt-in treats a remaining conflict as already stored.

Issue that this pull request solves

Closes: FXA-13151

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: lib/passkeys/signin-flow.ts (eligibility and stash), lib/passkeys/wrap/creation.ts (store and step-up retry), pages/InlinePasswordlessSyncSetup/container.tsx (exit paths), passkey.service.ts storePasskeyWrap (stale replacement).
  • Suggested review order:
  • Risky or complex parts: key material lives only in SensitiveDataClient and is zeroed on every exit, including unmount and a ceremony or navigation that finishes after the page is gone. The retry re-POSTs the already-sealed envelope, so it needs no kB. A wrap conflict on this surface can only be a live wrap from the same key epoch, hence success. The stale replacement is a read, then a delete pinned to that row's createdAt, then an insert; zero rows deleted, or a non-finite keysChangedAt, is a conflict.

Screenshots (Optional)

Please attach the screenshots of the changes made in case of change in user interface.

Other information (Optional)

  • Non-Sync Firefox services that require keys (Relay, VPN on pre-147 desktop) are not offered the opt-in; their landing pages are unchanged pending product and RP review.
  • The CMS syncHidePromoAfterLogin flag also withdraws this offer. Whether it should is an open product question.
  • The page still uses the Sync clouds illustration.
  • The wrap probe at sign-in treats errno 202 and 114 as "withhold the offer" without a Sentry report, so a client-first flag rollout stays quiet.
  • The passwordless sign-in branch stacks on this one as feat(settings): sign in to Sync with a PRF passkey and no password #21224.

This comment was marked as outdated.

@vpomerleau vpomerleau changed the title feat(settings): wire up the passwordless Sync opt-in feat(settings): wire up the password-free passkey opt-in Sep 16, 2026
@vpomerleau
vpomerleau force-pushed the FXA-13151 branch 2 times, most recently from 9ef908f to c1863cf Compare September 16, 2026 21:04
@vpomerleau
vpomerleau requested a balanced review from Copilot September 16, 2026 21:08

This comment was marked as outdated.

This comment was marked as outdated.

This comment was marked as outdated.

@vpomerleau
vpomerleau marked this pull request as ready for review September 16, 2026 23:15
@vpomerleau
vpomerleau requested review from a team as code owners September 16, 2026 23:15

@bcolsson bcolsson 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.

Would it be possible to keep the old string IDs for the duplicate strings to reduce the churn of having to retranslate these?

@vpomerleau
vpomerleau marked this pull request as draft September 17, 2026 16:37
@vpomerleau

Copy link
Copy Markdown
Contributor Author

Would it be possible to keep the old string IDs for the duplicate strings to reduce the churn of having to retranslate these?

Sorry about that! Reverted now.

@vpomerleau
vpomerleau force-pushed the FXA-13151 branch 2 times, most recently from 0c37e19 to 9aa449b Compare September 17, 2026 17:27

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.

🔵 Needs a closer look

It introduces cross-surface auth-flow changes involving sensitive key material handling and server-side wrap replacement semantics that warrant final human review for security and correctness.

Review details
  • Files reviewed: 32/32 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

inline-passwordless-sync-setup-enabling = Enabling…
inline-passwordless-sync-setup-not-now-button = Not now
# Success message shown in the Settings alert bar after the passkey was stored.
inline-passwordless-sync-setup-success-alert = This passkey is set up for password-free sign-in.

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.

The success and error message strings are under discussion and will likely change before this is merged

Comment thread libs/accounts/passkey/src/lib/passkey.wrap.repository.ts Outdated
Comment thread packages/fxa-settings/src/lib/oauth/hooks.test.tsx
Comment thread packages/fxa-settings/src/lib/oauth/hooks.tsx Outdated
Comment thread packages/fxa-settings/src/lib/passkeys/wrap/creation.ts
Comment thread packages/fxa-settings/src/lib/passkeys/signin-flow.test.tsx
Comment thread packages/fxa-settings/src/lib/passkeys/signin-flow.ts Outdated
Comment thread packages/fxa-settings/src/lib/sensitive-data-client.ts

@nshirley nshirley 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.

Just a few more questions! 🙂

Comment thread packages/fxa-settings/src/lib/passkeys/wrap/creation.ts Outdated
Comment thread packages/fxa-settings/src/pages/InlinePasswordlessSyncSetup/container.tsx Outdated
Comment thread packages/fxa-settings/src/pages/Signin/utils.ts Outdated
Comment thread packages/fxa-settings/src/pages/InlinePasswordlessSyncSetup/container.tsx Outdated
Comment thread packages/fxa-settings/src/lib/passkeys/signin-flow.ts
* by the passkey ceremony; `kB` is filled in by the password step that
* follows.
*/
export type PasskeyWrapData = {

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.

Kind of thinking out loud here. I don't know exactly what the mechanism might look like (took a shot at it below), but I see this state is having to be carefully managed across several places and I wonder if there's a way to centralize the management of it; hoisting the guard to clear it to the app index?

// something like this in app/index to prevent it from leaking out
const WRAP_MATERIAL_ROUTES = ['/signin_passkey_fallback', '/inline_passwordless_sync_setup'];

useEffect(() => {
  if (!WRAP_MATERIAL_ROUTES.some((r) => location.pathname.startsWith(r))) {
    sensitiveDataClient.clearPasskeyWrapData();
  }
}, [location.pathname, sensitiveDataClient]);

I also could be over-thinking this, but curious what you think?

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.

I like this. Updated.

This comment was marked as outdated.

Because:

- A desktop browser sign-in that needs encryption keys (Sync, for example) still requires a password after a PRF passkey until a wrap is stored; this is the one place the user can store one.
- The proof minted at sign-in lives ten minutes and the password step can outlast it.

This commit:

- Requests the `passkey` scope on eligible sign-ins and holds the PRF output, proof and `kB` in SensitiveDataClient.
- Routes sign-ins where the account has a password and the passkey has no wrap to /inline_passwordless_sync_setup in place of the browser's post-sign-in landing page.
- Adds the opt-in container: stores the wrap, lands in Settings with a banner, and zeroes the held material on every exit.
- Steps up with the passkey for a fresh proof when the sign-in one has expired, then submits the same envelope again.
- Replaces a stored wrap that predates `keysChangedAt` on POST /passkey/wraps instead of refusing it.

Closes #FXA-13151
@vpomerleau
vpomerleau marked this pull request as ready for review September 21, 2026 23:17
@vpomerleau
vpomerleau merged commit 397dd41 into main Sep 21, 2026
22 checks passed
@vpomerleau
vpomerleau deleted the FXA-13151 branch September 21, 2026 23:17
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.

4 participants