Skip to content

Commit

Permalink
feat(fxa-settings): Implement using recovery phone to sign in
Browse files Browse the repository at this point in the history
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
  • Loading branch information
vpomerleau committed Jan 23, 2025
1 parent 2bea7c5 commit abe0538
Show file tree
Hide file tree
Showing 52 changed files with 1,271 additions and 453 deletions.
2 changes: 1 addition & 1 deletion libs/accounts/recovery-phone/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export * from './lib/recovery-phone.service';
export * from './lib/recovery-phone.provider';
export * from './lib/recovery-phone.service.config';
export * from './lib/sms.manager';
export * from './lib/sms.manger.config';
export * from './lib/sms.manager.config';
export * from './lib/twilio.config';
export * from './lib/twilio.provider';
export * from './lib/recovery-phone.errors';
Original file line number Diff line number Diff line change
Expand Up @@ -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';
import { SmsManagerConfig } from './sms.manager.config';
import Redis from 'ioredis';
import { RecoveryPhoneConfig } from './recovery-phone.service.config';

Expand Down
2 changes: 1 addition & 1 deletion libs/accounts/recovery-phone/src/lib/sms.manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { LOGGER_PROVIDER } from '@fxa/shared/log';
import { StatsDService } from '@fxa/shared/metrics/statsd';
import { Test, TestingModule } from '@nestjs/testing';
import { SmsManager } from './sms.manager';
import { SmsManagerConfig } from './sms.manger.config';
import { SmsManagerConfig } from './sms.manager.config';
import { TwilioProvider } from './twilio.provider';
import { TwilioErrorCodes } from './recovery-phone.errors';

Expand Down
3 changes: 2 additions & 1 deletion libs/accounts/recovery-phone/src/lib/sms.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Inject, Injectable, LoggerService } from '@nestjs/common';
import { StatsD } from 'hot-shots';
import { Twilio } from 'twilio';
import { MessageInstance } from 'twilio/lib/rest/api/v2010/account/message';
import { SmsManagerConfig } from './sms.manger.config';
import { SmsManagerConfig } from './sms.manager.config';
import { TwilioProvider } from './twilio.provider';
import {
RecoveryNumberInvalidFormatError,
Expand Down Expand Up @@ -70,6 +70,7 @@ export class SmsManager {
retryCount: number
): Promise<MessageInstance> {
const from = this.rotateFromNumber();

try {
const msg = await this.client.messages.create({
to,
Expand Down
2 changes: 1 addition & 1 deletion packages/fxa-auth-client/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2338,7 +2338,7 @@ export default class AuthClient {
* @param code The otp code sent to the user's phone
* @param headers
*/
async recoveryPhoneSignInConfirm(
async recoveryPhoneSigninConfirm(
sessionToken: string,
code: string,
headers?: Headers
Expand Down
1 change: 1 addition & 0 deletions packages/fxa-auth-server/lib/authMethods.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const METHOD_TO_AMR = {
'email-2fa': 'email',
'totp-2fa': 'otp',
'recovery-code': 'otp',
'sms-2fa': 'otp',
};

// Maps AMR values to the type of authenticator they represent, e.g.
Expand Down
4 changes: 2 additions & 2 deletions packages/fxa-auth-server/test/remote/recovery_phone_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe(`#integration - recovery phone`, function () {
await TestServer.stop(server);
});

it('setups a recovery phone', async function () {
it('sets up a recovery phone', async function () {
if (!isTwilioConfigured) {
this.skip('Invalid twilio accountSid or authToken. Check env / config!');
}
Expand Down Expand Up @@ -183,7 +183,7 @@ describe(`#integration - recovery phone`, function () {
assert.isFalse(checkResp2.exists);
});

it('fails to setup invalid phone number', async function () {
it('fails to set up invalid phone number', async function () {
if (!isTwilioConfigured) {
this.skip('Invalid twilio accountSid or authToken. Check env / config!');
}
Expand Down
2 changes: 1 addition & 1 deletion packages/fxa-content-server/app/scripts/lib/auth-errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ var ERRORS = {
errno: 181,
message: t('Update was rejected, please try again'),
},
INVALID_EXPIRED_SIGNUP_CODE: {
INVALID_EXPIRED_OTP_CODE: {
errno: 183,
message: t('Invalid or expired confirmation code'),
},
Expand Down
6 changes: 6 additions & 0 deletions packages/fxa-content-server/app/scripts/lib/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -527,12 +527,18 @@ Router = Router.extend({
'signin_permissions(/)': createViewHandler(PermissionsView, {
type: VerificationReasons.SIGN_IN,
}),
'signin_recovery_choice(/)': function () {
this.createReactViewHandler('signin_recovery_choice');
},
'signin_recovery_code(/)': function () {
this.createReactOrBackboneViewHandler(
'signin_recovery_code',
SignInRecoveryCodeView
);
},
'sigin_recovery_phone(/)': function () {
this.createReactViewHandler('signin_recovery_phone');
},
'signin_reported(/)': function () {
this.createReactOrBackboneViewHandler(
'signin_reported',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ class ConfirmSignupCodeView extends FormView {
})
.catch((err) => {
if (
AuthErrors.is(err, 'INVALID_EXPIRED_SIGNUP_CODE') ||
AuthErrors.is(err, 'INVALID_EXPIRED_OTP_CODE') ||
AuthErrors.is(err, 'OTP_CODE_REQUIRED') ||
AuthErrors.is(err, 'INVALID_OTP_CODE')
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ describe('views/confirm_signup_code', () => {
});

describe('invalid or expired code error', () => {
const error = AuthErrors.toError('INVALID_EXPIRED_SIGNUP_CODE');
const error = AuthErrors.toError('INVALID_EXPIRED_OTP_CODE');

beforeEach(() => {
sinon
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ const FRONTEND_ROUTES = [
'signin_push_code_confirm',
'signin_totp_code',
'signin_recovery_code',
'signin_recovery_choice',
'signin_recovery_phone',
'signin_confirmed',
'signin_permissions',
'signin_reported',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ const getReactRouteGroups = (showReactApp, reactRoute) => {
'signin_unblock',
'force_auth',
'signin_recovery_code',
'signin_recovery_choice',
'signin_recovery_phone',
'inline_totp_setup',
'inline_recovery_setup',
'inline_recovery_key_setup',
Expand Down
34 changes: 32 additions & 2 deletions packages/fxa-settings/src/components/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { RouteComponentProps, Router, useLocation } from '@reach/router';
import {
Redirect,
RouteComponentProps,
Router,
useLocation,
} from '@reach/router';
import {
lazy,
Suspense,
Expand Down Expand Up @@ -80,6 +85,8 @@ import WebChannelExample from '../../pages/WebChannelExample';
import SignoutSync from '../Settings/SignoutSync';
import InlineRecoveryKeySetupContainer from '../../pages/InlineRecoveryKeySetup/container';
import SetPasswordContainer from '../../pages/PostVerify/SetPassword/container';
import SigninRecoveryChoiceContainer from '../../pages/Signin/SigninRecoveryChoice/container';
import SigninRecoveryPhoneContainer from '../../pages/Signin/SigninRecoveryPhone/container';

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

Expand Down Expand Up @@ -290,6 +297,7 @@ const AuthAndAccountSetupRoutes = ({
integration: Integration;
flowQueryParams: QueryParams;
} & RouteComponentProps) => {
const config = useConfig();
const localAccount = currentAccount();
// TODO: MozServices / string discrepancy, FXA-6802
const serviceName = integration.getServiceName() as MozServices;
Expand Down Expand Up @@ -378,9 +386,31 @@ const AuthAndAccountSetupRoutes = ({
path="/signin_confirmed/*"
{...{ isSignedIn, serviceName }}
/>
{config.featureFlags?.enableUsing2FABackupPhone ? (
<>
<SigninRecoveryChoiceContainer path="/signin_recovery_choice/*" />
<SigninRecoveryPhoneContainer
path="/signin_recovery_phone/*"
{...{ integration }}
/>
</>
) : (
<>
<Redirect
from="/signin_recovery_choice/*"
to="/signin_recovery_code/*"
noThrow
/>
<Redirect
from="/signin_recovery_phone/*"
to="/signin_recovery_code/*"
noThrow
/>
</>
)}
<SigninRecoveryCodeContainer
path="/signin_recovery_code/*"
{...{ integration, serviceName }}
{...{ integration }}
/>
<SigninReported path="/signin_reported/*" />
<SigninTokenCodeContainer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe('ModalVerifySession', () => {

it('renders error messages', async () => {
const error: any = new Error('invalid code');
error.errno = AuthUiErrors.INVALID_EXPIRED_SIGNUP_CODE.errno;
error.errno = AuthUiErrors.INVALID_EXPIRED_OTP_CODE.errno;
const session = {
sendVerificationCode: jest.fn().mockResolvedValue(true),
verifySession: jest.fn().mockRejectedValue(error),
Expand All @@ -74,7 +74,7 @@ describe('ModalVerifySession', () => {
});

expect(screen.getByTestId('tooltip').textContent).toContain(
AuthUiErrors.INVALID_EXPIRED_SIGNUP_CODE.message
AuthUiErrors.INVALID_EXPIRED_OTP_CODE.message
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ export const ModalVerifySession = ({
try {
await session.verifySession(code);
} catch (e) {
if (e.errno === AuthUiErrors.INVALID_EXPIRED_SIGNUP_CODE.errno) {
if (e.errno === AuthUiErrors.INVALID_EXPIRED_OTP_CODE.errno) {
const errorText = l10n.getString(
getErrorFtlId(e),
null,
AuthUiErrors.INVALID_EXPIRED_SIGNUP_CODE.message
AuthUiErrors.INVALID_EXPIRED_OTP_CODE.message
);
setErrorText(errorText);
} else {
Expand Down
8 changes: 6 additions & 2 deletions packages/fxa-settings/src/lib/auth-errors/auth-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ const ERRORS = {
// We don't currently support sms but still keep the error codes to avoid conflicts
SMS_ID_INVALID: {
errno: 131,
// should not be user facing, not wrapped in t
// This message should not be user-facing or localized
message: 'SMS ID invalid',
},
SMS_REJECTED: {
Expand Down Expand Up @@ -282,7 +282,7 @@ const ERRORS = {
errno: 181,
message: 'Update was rejected, please try again',
},
INVALID_EXPIRED_SIGNUP_CODE: {
INVALID_EXPIRED_OTP_CODE: {
errno: 183,
message: 'Invalid or expired confirmation code',
version: 2,
Expand All @@ -307,6 +307,10 @@ const ERRORS = {
errno: 206,
message: 'Can not create password, password already set',
},
SMS_SEND_RATE_LIMIT_EXCEEDED: {
errno: 216,
message: 'Client has sent too many requests',
},
SERVICE_UNAVAILABLE: {
errno: 998,
message: 'System unavailable, try again soon',
Expand Down
4 changes: 4 additions & 0 deletions packages/fxa-settings/src/lib/auth-errors/en.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ auth-error-125 = The request was blocked for security reasons
auth-error-138-2 = Unconfirmed session
auth-error-139 = Secondary email must be different than your account email
auth-error-155 = TOTP token not found
# Error shown when the user submits an invalid backup authentication code
auth-error-156 = Backup authentication code not found
auth-error-159 = Invalid account recovery key
auth-error-183-2 = Invalid or expired confirmation code
auth-error-203 = System unavailable, try again soon
auth-error-206 = Can not create password, password already set
auth-error-999 = Unexpected error
auth-error-1001 = Login attempt cancelled
Expand All @@ -30,4 +33,5 @@ auth-error-1011 = Valid email required
auth-error-1031 = You must enter your age to sign up
auth-error-1032 = You must enter a valid age to sign up
auth-error-1054 = Invalid two-step authentication code
auth-error-1056 = Invalid backup authentication code
auth-error-1062 = Invalid redirect
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ function mockCurrentAccount(
}

let currentSetPasswordProps: SetPasswordProps | undefined;
function mockInlineRecoveryKeySetupModule() {
function mockSetPasswordModule() {
jest
.spyOn(SetPasswordModule, 'default')
.mockImplementation((props: SetPasswordProps) => {
Expand All @@ -123,7 +123,7 @@ function applyDefaultMocks() {
jest.resetAllMocks();
jest.restoreAllMocks();
mockModelsModule();
mockInlineRecoveryKeySetupModule();
mockSetPasswordModule();
mockCurrentAccount(MOCK_STORED_ACCOUNT);
(useFinishOAuthFlowHandler as jest.Mock).mockImplementation(() => ({
finishOAuthFlowHandler: jest
Expand Down
Loading

0 comments on commit abe0538

Please sign in to comment.