Skip to content

feat(settings): pre-keys keyless login for desktop passkey Sync sign-in#20864

Closed
vpomerleau wants to merge 1 commit into
mainfrom
FXA-14129-prekeys-poc
Closed

feat(settings): pre-keys keyless login for desktop passkey Sync sign-in#20864
vpomerleau wants to merge 1 commit into
mainfrom
FXA-14129-prekeys-poc

Conversation

@vpomerleau

@vpomerleau vpomerleau commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Because

  • A keys-required (Sync) passkey sign-in previously withheld both WebChannel messages until after the password step, so the browser reflected nothing as signed-in until Sync keys were ready.
  • The keys_optional check lived in the fxa_status hook, conflating the raw browser capability with per-integration client requirements.
  • Turning Sync on later for an already-signed-in keyless account (the "Turn on Sync" resume) had no clean password step — it fell through to a full re-sign-in, and a render-time redirect could wedge the set-password page.

This pull request

  • Fires a keyless fxaccounts:login right after passkey verification on desktop when the browser advertises keys_optional, and defers only the keyed fxaccounts:oauth_login (which carries the Sync keys) until keys exist — the browser reflects "signed in" immediately and Sync enables once keys arrive (packages/fxa-settings/src/lib/passkeys/signin-flow.ts).
  • Moves the per-login keys policy off useFxAStatus (now reports only the raw browserSupportsKeysOptional) onto the integration model — requiresPasswordForLogin, supportsKeylessLogin, allowsPreKeysSyncLogin, all unit-tested (packages/fxa-settings/src/models/integrations/integration.ts).
  • Renames the threaded supportsKeysOptionalLogin prop to browserSupportsKeysOptional across settings.
  • Routes an already-signed-in keyless account that needs Sync keys to the enter-password fallback — gated on a valid browser session (isSignedIntoFirefox) — and generalizes that page for non-passkey flows via a reason ('passkey' | 'resume') that switches the copy, Glean, and verificationMethod (SigninDecider, SigninPasskeyFallback).
  • Recovers from a stale local session (e.g. after signing out from the browser menu): an INVALID_TOKEN reauth clears the token and restarts a fresh sign-in.
  • Fixes a render-time navigate() in PostVerify/SetPassword by moving the redirect guards into a useEffect.
  • Preserves behaviour for non-Sync integrations and never sends the keyed oauth_login without keys.

Issue that this pull request solves

Closes: FXA-14129

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 (keyless login after passkey), models/integrations/integration.ts (the new methods), pages/Signin/components/SigninDecider/index.tsx (resume reroute), SigninPasskeyFallback (generalized enter-password page + stale-session recovery), PostVerify/SetPassword.
  • Suggested review order: integration methods → signin-flowSigninDecider reroute → destination containers → prop threading.
  • Risky or complex parts: keyless-vs-keyed WebChannel timing (the keyed oauth_login must never be sent without keys); the resume reroute gate (isSignedIntoFirefox + a stored account) and its INVALID_TOKEN stale-session recovery.

Screenshots (Optional)

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

Other information (Optional)

Any other information that is important to this pull request.

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 updates FxA Settings sign-in/sign-up flows to support “pre-keys” keyless login for desktop Sync passkey sign-in, so Firefox can reflect “signed in” immediately after passkey verification while deferring the keyed fxaccounts:oauth_login until Sync keys exist. It also refactors keys_optional handling so useFxAStatus reports only the raw browser capability and the Integration model applies per-integration policy.

Changes:

  • Add a desktop-only “pre-keys” fxaccounts:login in the passkey sign-in flow when keys_optional is advertised, and thread syncPreKeysLoginSent to avoid duplicate login messages later.
  • Move keys-related policy decisions onto the Integration model (requiresPasswordForLogin, supportsKeylessLogin, allowsPreKeysSyncLogin, nonSyncKeysRequirePassword) and add unit tests.
  • Rename the threaded capability prop from supportsKeysOptionalLogin to browserSupportsKeysOptional across Settings (components, containers, stories, tests), plus formatting-only route/component readability updates.

Reviewed changes

Copilot reviewed 51 out of 51 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/fxa-settings/src/pages/Signup/mocks.tsx Update signup mocks to implement supportsKeylessLogin and rename prop to browserSupportsKeysOptional.
packages/fxa-settings/src/pages/Signup/interfaces.ts Extend signup integration picks to include supportsKeylessLogin.
packages/fxa-settings/src/pages/Signup/index.tsx Use integration policy (supportsKeylessLogin / requiresPasswordForLogin) and rename capability prop.
packages/fxa-settings/src/pages/Signup/index.test.tsx Update tests for renamed capability prop and third-party-auth visibility logic.
packages/fxa-settings/src/pages/Signup/index.stories.tsx Update stories for renamed capability prop.
packages/fxa-settings/src/pages/Signup/container.test.tsx Update container test integration mock + renamed capability prop.
packages/fxa-settings/src/pages/Signin/utils.ts Rename navigation option field and formatting adjustments.
packages/fxa-settings/src/pages/Signin/utils.test.ts Update tests for renamed navigation option field and formatting.
packages/fxa-settings/src/pages/Signin/SigninUnblock/mocks.tsx Add new integration policy methods in mocks (supportsKeylessLogin, allowsPreKeysSyncLogin, nonSyncKeysRequirePassword).
packages/fxa-settings/src/pages/Signin/SigninTotpCode/mocks.tsx Rename parameter/prop to browserSupportsKeysOptional in mocks.
packages/fxa-settings/src/pages/Signin/SigninTotpCode/index.tsx Use browserSupportsKeysOptional from useFxAStatusResult.
packages/fxa-settings/src/pages/Signin/SigninTotpCode/index.test.tsx Formatting updates + renamed prop wiring in tests.
packages/fxa-settings/src/pages/Signin/SigninRecoveryPhone/interfaces.ts Rename container prop to browserSupportsKeysOptional.
packages/fxa-settings/src/pages/Signin/SigninRecoveryPhone/container.tsx Use renamed prop for requiresPasswordForLogin decision.
packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/interfaces.ts Rename prop to browserSupportsKeysOptional.
packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/index.tsx Use renamed prop consistently for password redirect logic.
packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/index.test.tsx Update tests for renamed prop.
packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/container.tsx Thread renamed prop into recovery code page.
packages/fxa-settings/src/pages/Signin/SigninPasswordlessCode/mocks.tsx Rename prop and update mock useFxAStatus wiring.
packages/fxa-settings/src/pages/Signin/SigninPasswordlessCode/index.tsx Thread renamed capability into passkey sign-in hook and set-password redirect logic.
packages/fxa-settings/src/pages/Signin/SigninPasswordlessCode/index.test.tsx Update tests for renamed prop.
packages/fxa-settings/src/pages/Signin/SigninPasskeyFallback/container.tsx Suppress handleFxaLogin if pre-keys keyless login already fired (syncPreKeysLoginSent).
packages/fxa-settings/src/pages/Signin/SigninPasskeyFallback/container.test.tsx Add coverage ensuring keyless login suppression when syncPreKeysLoginSent.
packages/fxa-settings/src/pages/Signin/mocks.tsx Update signin mocks with new integration policy methods + rename capability prop.
packages/fxa-settings/src/pages/Signin/interfaces.ts Add policy methods to integration pick types; rename navigation option field; add syncPreKeysLoginSent to location state.
packages/fxa-settings/src/pages/Signin/index.tsx Use renamed capability and integration policy for third-party-auth visibility.
packages/fxa-settings/src/pages/Signin/index.test.tsx Update tests for renamed capability prop.
packages/fxa-settings/src/pages/Signin/index.stories.tsx Update stories for renamed capability prop.
packages/fxa-settings/src/pages/Signin/components/SigninDecider/index.tsx Move non-sync password/keys logic to integration helpers and thread renamed capability prop.
packages/fxa-settings/src/pages/Signin/components/SigninDecider/index.test.tsx Update tests for renamed capability prop.
packages/fxa-settings/src/pages/Signin/components/SigninCached/index.tsx Thread renamed capability prop and use it for redirect decisions.
packages/fxa-settings/src/pages/Signin/components/SigninCached/index.test.tsx Update tests for renamed prop.
packages/fxa-settings/src/pages/Signin/components/SigninAlternativeAuthOptions/index.tsx Thread renamed capability prop into passkey sign-in hook.
packages/fxa-settings/src/pages/PostVerify/ThirdPartyAuthCallback/index.tsx Use renamed capability in requiresPasswordForLogin and navigation options.
packages/fxa-settings/src/pages/PostVerify/ThirdPartyAuthCallback/index.test.tsx Update tests for renamed capability prop.
packages/fxa-settings/src/pages/PostVerify/SetPassword/interfaces.ts Add syncPreKeysLoginSent to location state type.
packages/fxa-settings/src/pages/PostVerify/SetPassword/container.tsx Suppress handleFxaLogin if pre-keys login already sent; gate route validity on renamed capability prop.
packages/fxa-settings/src/pages/PostVerify/SetPassword/container.test.tsx Add coverage ensuring keyless login suppression when syncPreKeysLoginSent + rename prop.
packages/fxa-settings/src/pages/Index/mocks.tsx Update index mocks with new integration policy methods + rename capability prop.
packages/fxa-settings/src/pages/Index/interfaces.ts Add policy methods to index integration picks.
packages/fxa-settings/src/pages/Index/index.tsx Thread renamed capability and use integration policy for third-party-auth visibility.
packages/fxa-settings/src/pages/Index/index.test.tsx Update tests for renamed capability prop.
packages/fxa-settings/src/pages/Index/index.stories.tsx Update stories for renamed capability prop.
packages/fxa-settings/src/models/integrations/integration.ts Add supportsKeylessLogin, allowsPreKeysSyncLogin, nonSyncKeysRequirePassword; refactor requiresPasswordForLogin.
packages/fxa-settings/src/models/integrations/integration.test.ts Add unit tests for new integration policy methods.
packages/fxa-settings/src/lib/passkeys/signin-flow.ts Add pre-keys keyless fxaLogin for desktop Sync passkey flow; thread syncPreKeysLoginSent.
packages/fxa-settings/src/lib/passkeys/signin-flow.test.tsx Add tests for pre-keys keyless login behavior and state threading.
packages/fxa-settings/src/lib/hooks/useFxAStatus/mocks.tsx Rename mocked capability field to browserSupportsKeysOptional.
packages/fxa-settings/src/lib/hooks/useFxAStatus/index.tsx Make hook report raw keys_optional capability as browserSupportsKeysOptional (integration-agnostic).
packages/fxa-settings/src/lib/hooks/useFxAStatus/index.test.tsx Update tests to match raw capability reporting semantics.
packages/fxa-settings/src/components/App/index.tsx Formatting-only route/component readability updates; thread renamed capability into recovery containers.
Comments suppressed due to low confidence (1)

packages/fxa-settings/src/pages/PostVerify/SetPassword/container.tsx:193

  • When syncPreKeysLoginSent is true, this suppresses handleFxaLogin, but this container is also providing the Sync engine payload (syncEngines) that is normally forwarded via fxaLogin (see sendFxaLogin in pages/Signin/utils.ts). In the pre-keys passkey flow the earlier keyless fxaLogin sent from lib/passkeys/signin-flow.ts uses integration.getWebChannelServices() without sync engine info (Sync defaults to { sync: {} }), so with handleFxaLogin: false the browser may never receive the offered/declined engine lists for this flow.
            handleFxaLogin: !syncPreKeysLoginSent,
            handleFxaOAuthLogin: true,
            showSignupConfirmedSync: true,
            origin: 'post-verify-set-password',
            syncEngines: {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


const Settings = lazy(() => import('../Settings'));

export const App = ({

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.

Most of the changes in this file are linting - I guess we haven't touched this one in a while.

@vpomerleau
vpomerleau marked this pull request as ready for review July 14, 2026 21:53
@vpomerleau
vpomerleau requested a review from a team as a code owner July 14, 2026 21:53
@vpomerleau
vpomerleau requested a review from LZoog July 14, 2026 22:08
/>
<Route
path="/pair/supp/allow/*"
element={<PairSuppAllow integration={integration} />}

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.

Welp I guess if the linter touched these lines anyway you could go ahead and change these to {...{ integration }} 😅

...argOverrides,
});

it('sends a keyless fxaLogin before the password step when browserSupportsKeysOptional is true', async () => {

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.

Mentioned this in Slack but bringing it here as well: why introduce a new pattern to send fxaLogin and then fxaOAuthLogin later for a Sync sign-in, instead of deferring both fxaLogin and fxaOAuthLogin like we do currently for passwordless accounts trying to sign into Sync, via "Set password"?

Thinking this through:

  1. I know we send the events together for other passwordless Sync sign-ins
  2. I know we send them together during a 2FA sign in
  3. I know we don't send them together if the user is signing up for a new account and that it puts the user into a "Finish account set up" state.
  4. I think we also don't send them together when we ask for an email confirmation code, but at that point we send verified: false and already have the Sync key. Maybe what you have is OK if it matches this behavior?

I guess it does make me wonder though, if we like this approach, if a user needing to provide or set a password because they're trying to sign into Sync and they're passwordless, if we would want to sign those users in halfway too. Not sure. You could say, "Sign in with password is different than Set Password and this should follow the confirmation code flow," but I wonder if it'd be a little less complicated if we sent them together here.

@LZoog LZoog Jul 15, 2026

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.

@vpomerleau I thought about this a bit more:

  • This will probably leave the user in an "Account disconnected" state the next time they try to finish the flow since fxaOAuthLogin is never sent and the browser expects it to be part of one flow with the given query params
  • I think you can do what you're wanting by sending fxaLogin, and then sending fxaOAuthLogin as a keyless signin immediately also during navigation when the user is taken to this page. Then, use the new web channel message Vijay implemented a few months ago to get "new oauth login query params" from Firefox. Send both together again after the user enters a password using the new query params. Firefox will see this as two logins but that seems fine to me
  • One other oddity, is for the first set of messages that's a keyless login, I don't know what you should send as the 'service' value inside the webchannel message. I also am not sure what Firefox does with this on their side but it's something Mark and I had talked about long ago, but it feels funky to send services: { sync: {}} with the denied/approved engine list when the user might abandon and not actually be signed into Sync. I think it's probably more proper to introduce a "no service login" in the web channel message this like, can Firefox handle you sending services: {} blank there or does it expect at least one key in that object?
  • This isn't going to work for Mobile because Mobile requires keys, but that's covered with the capability check and should be fine, just be sure you don't send the first sets of logins for that case, and be sure that there's an issue filed on the iOS and Android side to support this web channel messaging (the "grab new oauth query params" new one) and probably let Ross know about it or post a message in #fxa-mobile-integrations and mention we may want to support it as part of Sync decoupling because the special casing for this is going to have to ALSO be on if there's a response to that new web channel message and if keys optional is supported (we'd want this anyway with Mobile moving away from the custom tabs)
  • Be sure to send the canLinkAccount message and check if the browser version supports the UID version and send if so (I'm actually not sure if we need the check since either way for this one, we send the can link account message at the same time regardless of if UID version is supported or not; you might be able to just send it with UID and assume the field will be ignored by the browser if unsupported)

Because:
 - A keys-required (Sync) passkey sign-in withheld both WebChannel messages
   until after the password step, so the browser reflected nothing as
   signed-in until keys were ready.
 - The keys-optional check lived in the fxa_status hook, conflating the raw
   browser capability with per-integration client requirements.

This commit:
 - Fires a keyless fxaccounts:login right after passkey verification on desktop
   when the browser advertises keys_optional; defers only the keyed oauth_login
   until keys exist (Sync enables once keys arrive).
 - useFxAStatus now reports only the raw browser capability
   (browserSupportsKeysOptional). The per-login keys policy moves onto the
   integration model: requiresPasswordForLogin, supportsKeylessLogin,
   allowsPreKeysSyncLogin (unit-tested).
 - Renames the threaded supportsKeysOptionalLogin prop to
   browserSupportsKeysOptional across settings.
 - No functional change for non-Sync integrations: supportsKeylessLogin
   reproduces the prior derived value (isFirefoxNonSync implies isOAuthNative).

Closes #FXA-14129
@vpomerleau
vpomerleau force-pushed the FXA-14129-prekeys-poc branch from e66521e to 12411c5 Compare July 15, 2026 08:51
@vpomerleau
vpomerleau requested a review from a team as a code owner July 15, 2026 08:51
@vpomerleau
vpomerleau marked this pull request as draft July 15, 2026 15:14
@vpomerleau

Copy link
Copy Markdown
Contributor Author

Closing this PR - request is on hold pending product decisions.

@vpomerleau vpomerleau closed this Jul 16, 2026
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.

3 participants