Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fxa-settings): Implement using recovery phone to sign in #18255

Merged
merged 1 commit into from
Jan 28, 2025

Conversation

vpomerleau
Copy link
Contributor

@vpomerleau vpomerleau commented Jan 20, 2025

Because

  • New feature: recovery phone as recovery method for 2FA during sign in

This pull request

  • Hook up new pages to choose recovery method and use recovery phone during sign in

Issue that this pull request solves

Closes: #FXA-10374

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).

Screenshots (Optional)

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

Other information (Optional)

Notes for testing - options to manually test the flow until #18276 is merged: either manually add a recovery phone to an existing account with 2FA via mysql or cherry pick the latest commit for PR-18276 to use UI to add a phone number
generated codes can be retrieved with redis-commander (or cherry pick #18277 to use inbox logs)

@vpomerleau vpomerleau force-pushed the FXA-10374 branch 6 times, most recently from abe0538 to 406b221 Compare January 23, 2025 21:34
@@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { ConfigService } from '@nestjs/config';
import { SmsManagerConfig } from './sms.manger.config';
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks!

});

const mockAuthClient = new AuthClient('http://localhost:9000', {
keyStretchVersion: 1,
Copy link
Contributor

@dschom dschom Jan 23, 2025

Choose a reason for hiding this comment

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

We should use version 2

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Guilty of copy pasta from another test file - @dschom should this be updated to v2 in all auth client mocks?

@vpomerleau vpomerleau force-pushed the FXA-10374 branch 4 times, most recently from 1e644ab to a08013f Compare January 27, 2025 17:35
@vpomerleau vpomerleau changed the title WIP feat(fxa-settings): Implement using recovery phone to sign in feat(fxa-settings): Implement using recovery phone to sign in Jan 27, 2025
@vpomerleau vpomerleau marked this pull request as ready for review January 27, 2025 17:42
@vpomerleau vpomerleau requested review from a team as code owners January 27, 2025 17:42
Copy link
Contributor

@vbudhram vbudhram left a comment

Choose a reason for hiding this comment

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

@vpomerleau Took a first pass at this. Gonna do some manual testing now

user.type(screen.getByRole('textbox'), MOCK_RECOVERY_CODE)
);

expect(screen.getByRole('button', { name: /Confirm/i })).toHaveAttribute(
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice!

Copy link
Contributor

Choose a reason for hiding this comment

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

return;
}
try {
await authClient.recoveryPhoneSigninSendCode(signinState.sessionToken);
Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW, it seems odd to me to send the code from this page. I wonder if the recovery phone page should handle sending codes instead. Don't need to block on this though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't disagree, but we'd have to review UX for error handling in that scenario because the designs currently have us showing an error on the choice screen if send fails for the regular path to recovery phone.

I'll file a follow up ticket to review if we should send the code before or after navigation.

signin-recovery-code-instruction-v2 = Enter one of the one-time use backup authentication codes you saved during two-step authentication setup.
signin-recovery-code-input-label-v2 = Enter 10-character code
# codes here refers to backup authentication codes
signin-recovery-code-instruction-v3 = Enter one of the one-time-use codes you saved when you set up two-step authentication.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
signin-recovery-code-instruction-v3 = Enter one of the one-time-use codes you saved when you set up two-step authentication.
signin-recovery-code-instruction-v3 = Enter one of the one-time use codes you saved when you set up two-step authentication.

Nit, but aligns with previous string in terms of hyphenation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Content reviewed and recommended "one-time-use" codes, so this is an intentional update :)

Copy link
Contributor

@vbudhram vbudhram left a comment

Choose a reason for hiding this comment

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

@vpomerleau Manual testing looks good! I think this will be ok to merge, I can still do some fixes while working on #18287.

:shipit: 🚀

@@ -82,6 +82,8 @@ const getReactRouteGroups = (showReactApp, reactRoute) => {
'signin_unblock',
'force_auth',
'signin_recovery_code',
'signin_recovery_choice',
'signin_recovery_phone',
Copy link
Contributor

Choose a reason for hiding this comment

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

You probably already know this but just to be clear / for others too: we only need to add routes to content-server this way if it's not behind /settings/* - content-server already points all settings routes to our React app if users hit them directly. Otherwise, without these content-server modifications, the server doesn't know what to do on a refresh of that page and it ends up blank, so this is probably preferred for routes not behind settings.

(I wanted to think that through myself too, so, 👍)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you, I wasn't 💯 sure on this so appreciate the confirmation!

packages/fxa-settings/src/components/Settings/index.tsx Outdated Show resolved Hide resolved
}),
mockRecoveryPhoneGet = jest.fn().mockResolvedValue({
exists: true,
phoneNumber: '7890',
Copy link
Contributor

Choose a reason for hiding this comment

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

Right now this will return MOCK_MASKED_PHONE_NUMBER FWIW, and not just the last 4, if the session isn't verified. I need to fill out details for FXA-11044.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you, I'll update to test that the masked number is handled properly!

const [loading, setLoading] = useState(true);

useEffect(() => {
if (!signinState || !signinState.sessionToken) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is sessionToken already in local storage at this point? Probably not here since I don't want you to have to refactor this all but I think we might as well just pull it from local storage when we can. (I'm not sure we should have ever passed it as part of signin state heh)

signinState.sessionToken
);
// TODO verify that recoveryPhoneGet returns a masked phone number (last four digits only)
phoneNumber && setLastFourPhoneDigits(phoneNumber.slice(-4));
Copy link
Contributor

Choose a reason for hiding this comment

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

You might could create a little helper function for this:

{phoneNumber.includes('•')
? phoneNumber

See:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would be nice to be more consistent. Could we clean this up in the ticket you filled for the twilio national number format?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a note in FXA-11044

return;
}

if (phoneNumber && (!count || count === 0)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the type of this null | number? Not super nice that any of these returned values from authClient are any 😅

}, [authClient, lastFourPhoneDigits, signinState, navigateWithQuery]);

const handlePhoneChoice = async () => {
if (!signinState) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If you want to use the session token from signin state, you could curry this function with it so you don't have to recheck if (!signinState) {, but maybe later we should just pull from local storage instead and curry that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a note in FXA-9875 about reviewing what's included in signinState (vs local storage)

}

if (!signinState || !lastFourPhoneDigits) {
return <LoadingSpinner fullScreen />;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this where the redirect should happen instead of useEffect?

user.type(screen.getByRole('textbox'), MOCK_RECOVERY_CODE)
);

expect(screen.getByRole('button', { name: /Confirm/i })).toHaveAttribute(
Copy link
Contributor

Choose a reason for hiding this comment

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

@@ -0,0 +1,99 @@
import React from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: missing license

React.useState(false);
const ftlMsgResolver = useFtlMsgResolver();

const maskedPhoneNumber = `+1-•••-${lastFourPhoneDigits}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

You may want to update this to just the dots, for consistency across add/delete inside Settings. In that new ticket I referenced, I think we can iron this out with that national_format from Twilio if we want these formatted properly.

Because:

* New feature: recovery phone as recovery method for 2FA during sign in

This commit:

* Hook up new pages to choose recovery method and use recovery phone during sign in

Closes #FXA-10374
@vpomerleau vpomerleau merged commit 153efdd into main Jan 28, 2025
24 checks passed
@vpomerleau vpomerleau deleted the FXA-10374 branch January 28, 2025 23:55
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.

5 participants