fix(passkeys): extend non-decoupled keys-required sign-in to passkey, 2FA, and recovery flows#20752
fix(passkeys): extend non-decoupled keys-required sign-in to passkey, 2FA, and recovery flows#20752vpomerleau wants to merge 1 commit into
Conversation
b08d682 to
30a8f32
Compare
There was a problem hiding this comment.
Pull request overview
This PR closes a remaining “keyless login” gap for passkey sign-ins and 2FA/recovery completion flows when scoped keys are required (e.g., Sync not decoupled / Firefox non-Sync services that still need Sync-scoped keys). It shifts the redirect decision to the client-side integration.requiresPasswordForLogin(supportsKeysOptionalLogin) check, updates the passkey auth API contract accordingly, and defers login notifications/metrics until keys are actually obtained.
Changes:
- Extend the “keys-required → set_password/password-fallback” redirect logic to passkey sign-in plus TOTP/recovery-code/recovery-phone completion flows.
- Update passkey auth server/client contracts: add
requiresPasswordForLoginrequest flag, removerequiresPasswordForSyncresponse field, and adjust notification/metric timing. - Propagate
supportsKeysOptionalLoginand flowmetricsContextthrough the relevant Settings pages and passkey fallback reauth so deferred metrics correlate correctly.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/fxa-settings/src/pages/Signin/SigninTotpCode/index.tsx | Treat third-party-auth marker as passwordless for keys-required redirect; set reason accordingly. |
| packages/fxa-settings/src/pages/Signin/SigninTotpCode/index.test.tsx | Add coverage for third-party-auth + TOTP redirect to set_password. |
| packages/fxa-settings/src/pages/Signin/SigninRecoveryPhone/interfaces.ts | Add supportsKeysOptionalLogin prop for recovery phone container. |
| packages/fxa-settings/src/pages/Signin/SigninRecoveryPhone/container.tsx | Redirect passwordless completion to set_password when keys are required; store hasPassword:false. |
| packages/fxa-settings/src/pages/Signin/SigninRecoveryPhone/container.test.tsx | Add tests for passwordless redirect behavior and non-redirect case. |
| packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/interfaces.ts | Add supportsKeysOptionalLogin prop for recovery code page. |
| packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/index.tsx | Redirect passwordless completion to set_password when keys are required; store hasPassword:false. |
| packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/index.test.tsx | Add tests for passwordless redirect behavior and non-redirect case. |
| packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/container.tsx | Thread supportsKeysOptionalLogin through the container into the page component. |
| packages/fxa-settings/src/pages/Signin/SigninPasswordlessCode/index.tsx | Pass supportsKeysOptionalLogin into passkey sign-in hook. |
| packages/fxa-settings/src/pages/Signin/SigninPasskeyFallback/container.tsx | Include reason:'signin' and flow metricsContext in sessionReauthWithAuthPW for correlation. |
| packages/fxa-settings/src/pages/Signin/SigninPasskeyFallback/container.test.tsx | Verify metricsContext is forwarded to session reauth and request options include reason. |
| packages/fxa-settings/src/pages/Signin/interfaces.ts | Extend alternative-auth props to include supportsKeysOptionalLogin. |
| packages/fxa-settings/src/pages/Signin/index.tsx | Pass supportsKeysOptionalLogin into passkey sign-in hook. |
| packages/fxa-settings/src/pages/Signin/components/SigninDecider/index.tsx | Propagate supportsKeysOptionalLogin down into Signin component props. |
| packages/fxa-settings/src/pages/Signin/components/SigninAlternativeAuthOptions/index.tsx | Pass supportsKeysOptionalLogin into passkey sign-in hook for this surface. |
| packages/fxa-settings/src/pages/Index/index.tsx | Pass supportsKeysOptionalLogin into passkey sign-in hook for email-first surface. |
| packages/fxa-settings/src/lib/passkeys/signin-flow.ts | Compute requiresPasswordForLogin client-side, forward it to server, and drive redirect off it. |
| packages/fxa-settings/src/lib/passkeys/signin-flow.test.tsx | Update expectations for new request param and add redirect tests for non-Sync keys-required flows. |
| packages/fxa-settings/src/components/App/index.tsx | Thread flowQueryParams into passkey fallback route; pass supportsKeysOptionalLogin to recovery routes. |
| packages/fxa-auth-server/test/remote/passkeys.in.spec.ts | Remove assertion for removed requiresPasswordForSync response field. |
| packages/fxa-auth-server/lib/routes/passkeys.ts | Accept requiresPasswordForLogin and use it to defer login notifications/metrics; remove requiresPasswordForSync response. |
| packages/fxa-auth-server/lib/routes/passkeys.spec.ts | Update route factory typing and add coverage for deferral behavior driven by client flag. |
| packages/fxa-auth-server/lib/passkey-utils.ts | Narrow helper config typing to only the passkeys flag slice for easier testing. |
| packages/fxa-auth-server/lib/passkey-utils.spec.ts | Adjust tests for narrowed config types. |
| packages/fxa-auth-server/docs/swagger/passkeys-api.ts | Update docs to reflect response field removal (needs request field doc update). |
| packages/fxa-auth-client/lib/client.ts | Remove requiresPasswordForSync from types; add requiresPasswordForLogin request option; minor formatting fixes. |
| packages/functional-tests/tests/passkeyAuth/passkeyPasswordFallback.spec.ts | Update test description to reflect client-side redirect decision. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
6eeaa5f to
4907b80
Compare
| routes = passkeyRoutes( | ||
| customs, | ||
| db, | ||
| // Fixture provides only the passkeys flags; cast to the full ConfigType. |
There was a problem hiding this comment.
Should we just create a better config object?
There was a problem hiding this comment.
Also the passkeyRoutes should probaby have config as config: Pick<ConfigType, 'passkeys'>
| isPasswordlessSignin && | ||
| integration.requiresPasswordForLogin(supportsKeysOptionalLogin) | ||
| ) { | ||
| navigateWithQuery('/post_verify/set_password', { |
There was a problem hiding this comment.
Do we need to emit a glean metric here. This does imply the recovery succeeded, we are just now asking them to set a password.
There was a problem hiding this comment.
I moved it above since it is a success metric, but it does make "success view" (= alert bar) a bit of a misnomer.
There was a problem hiding this comment.
Oh interesting. Maybe we should double check with product and follow up if necessary.
There was a problem hiding this comment.
Related to FXA-13972
| account, | ||
| service, | ||
| requiresPasswordForSync | ||
| deferLoginNotifications |
There was a problem hiding this comment.
This feels misleading. Even if this is true, we send a login email, just not a service specific one. I wonder if could result in 'double mails'?
There was a problem hiding this comment.
I see what you mean, the metrics are deferred but not the email so the naming was confusing. Sending the email here is intentional since this login results in a verified session token, even if the browser sign-in may not be complete. Session/reauth, doesn't at the moment result in a second email.
| it('should throw featureNotEnabled error when config.passkeys.enabled is undefined', () => { | ||
| const config = { passkeys: {} }; | ||
| // Simulate a malformed config that omits the required `enabled`. | ||
| const config = { passkeys: {} } as Parameters< |
There was a problem hiding this comment.
Nothing wrong with this, but it's kind of a noisy / messy way to cast this.
There was a problem hiding this comment.
Better like this?
| challengeOptions.challenge, | ||
| { | ||
| ...(serviceForRequest ? { service: serviceForRequest } : {}), | ||
| // Keys-pending signal for the server; password-step decision for the client. |
There was a problem hiding this comment.
I feel like this comment could be a little bit more clear.
dschom
left a comment
There was a problem hiding this comment.
Thanks for these updates. Just a few comments. After you review let me know and I'll take for more quick manual test and give an R+.
… 2FA, and recovery flows Because: - PR #20750 fixed keyless logins for passwordless OTP and third-party-auth sign-ins to keys-requiring services on non-decoupled browsers, but missed passkey sign-in and the TOTP/recovery-code/recovery-phone completion paths. This commit: - Drives the passkey password-step decision from integration.requiresPasswordForLogin(), not the server's service === 'sync' signal, and extends the same keys-required redirect to TOTP, recovery code, and recovery phone. - Makes keysRequired a required field on POST /passkey/authentication/finish, dropping the service === 'sync' fallback; removes the unused requiresPasswordForSync response field. - Defers the account.login metric/flow-signal/security-event for keys-required sign-ins until keys are obtained at reauth/set-password (the new-device email is still sent, with generic framing). - Adds unit tests for every path; updates remote/functional passkey specs. Closes: FXA-13979
4907b80 to
139c847
Compare
Because
Sync scope granted, but keys_jwe is None).This pull request
integration.requiresPasswordForLogin(supportsKeysOptionalLogin)(a decoupling-aware, client-side check) instead of the server'sservice === 'sync'signal. Routes a passwordless account to/post_verify/set_passwordand an existing-password account to/signin_passkey_fallback, deferring thefxaLogin/fxaOAuthLoginweb-channel messages until keys are available.requiresPasswordForSyncresponse field.account.loginmetric / flow signal / security event (the new-device email is still sent, with generic framing) so the metric fires once keys are actually obtained at reauth / set-password. The client passes the decoupling-awarerequiresPasswordForLoginto the server, and the passkey fallback reauth now passesreason: 'signin'+ the flowmetricsContextso the deferred metric lands correlated.keysRequiredis a required field on/passkey/authentication/finish(theservice === 'sync'fallback was removed).Issue that this pull request solves
Closes: FXA-13979
Checklist
Put an
xin the boxes that applyHow to review (Optional)
main; this branch is now a single squashed commit on top ofmain— review the diff directly.requiresPasswordForLogin, (2) 2FA/recovery parity, (3) server response/type tidy + the now-requiredkeysRequiredcontract, (4) keys-required notification deferral.requiresPasswordForLogindecoupling logic andsupportsKeysOptionalLoginfail-closed default; the notification deferral and the fallback-reauthmetricsContextcorrelation.Screenshots (Optional)
Please attach the screenshots of the changes made in case of change in user interface.
Other information (Optional)