Skip to content

Commit

Permalink
Merge pull request Expensify#48030 from Expensify/tgolen-require-2fa-…
Browse files Browse the repository at this point in the history
…to-disable

Require a 2FA code to disable 2FA
  • Loading branch information
tgolen authored Sep 2, 2024
2 parents 4d9f542 + 6066f8d commit c7934c5
Show file tree
Hide file tree
Showing 14 changed files with 122 additions and 31 deletions.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3887,6 +3887,7 @@ const CONST = {
SUCCESS: 'SUCCESS',
ENABLED: 'ENABLED',
DISABLED: 'DISABLED',
GETCODE: 'GETCODE',
},
DELEGATE_ROLE: {
SUBMITTER: 'submitter',
Expand Down
2 changes: 1 addition & 1 deletion src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1117,7 +1117,7 @@ export default {
twoFactorAuthEnabled: 'Two-factor authentication enabled',
whatIsTwoFactorAuth: 'Two-factor authentication (2FA) helps keep your account safe. When logging in, you’ll need to enter a code generated by your preferred authenticator app.',
disableTwoFactorAuth: 'Disable two-factor authentication',
disableTwoFactorAuthConfirmation: 'Two-factor authentication keeps your account more secure. Are you sure you want to disable it?',
explainProcessToRemove: 'In order to disable two-factor authentication (2FA), please enter a valid code from your authentication app.',
disabled: 'Two-factor authentication is now disabled',
noAuthenticatorApp: 'You’ll no longer require an authenticator app to log into Expensify.',
stepCodes: 'Recovery codes',
Expand Down
2 changes: 1 addition & 1 deletion src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1122,7 +1122,7 @@ export default {
whatIsTwoFactorAuth:
'La autenticación de dos factores (2FA) ayuda a mantener tu cuenta segura. Al iniciar sesión, deberás ingresar un código generado por tu aplicación de autenticación preferida.',
disableTwoFactorAuth: 'Deshabilitar la autenticación de dos factores',
disableTwoFactorAuthConfirmation: 'La autenticación de dos factores mantiene tu cuenta más segura. ¿Estás seguro de que quieres desactivarla?',
explainProcessToRemove: 'Para deshabilitar la autenticación de dos factores (2FA), por favor introduce un código válido de tu aplicación de autenticación.',
disabled: 'La autenticación de dos factores está ahora deshabilitada',
noAuthenticatorApp: 'Ya no necesitarás una aplicación de autenticación para iniciar sesión en Expensify.',
stepCodes: 'Códigos de recuperación',
Expand Down
5 changes: 5 additions & 0 deletions src/libs/API/parameters/DisableTwoFactorAuthParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type DisableTwoFactorAuthParams = {
twoFactorAuthCode: string;
};

export default DisableTwoFactorAuthParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export type {default as ValidateBankAccountWithTransactionsParams} from './Valid
export type {default as ValidateLoginParams} from './ValidateLoginParams';
export type {default as ValidateSecondaryLoginParams} from './ValidateSecondaryLoginParams';
export type {default as ValidateTwoFactorAuthParams} from './ValidateTwoFactorAuthParams';
export type {default as DisableTwoFactorAuthParams} from './DisableTwoFactorAuthParams';
export type {default as VerifyIdentityForBankAccountParams} from './VerifyIdentityForBankAccountParams';
export type {default as AnswerQuestionsForWalletParams} from './AnswerQuestionsForWalletParams';
export type {default as AddCommentOrAttachementParams} from './AddCommentOrAttachementParams';
Expand Down
2 changes: 1 addition & 1 deletion src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.REQUEST_UNLINK_VALIDATION_LINK]: Parameters.RequestUnlinkValidationLinkParams;
[WRITE_COMMANDS.UNLINK_LOGIN]: Parameters.UnlinkLoginParams;
[WRITE_COMMANDS.ENABLE_TWO_FACTOR_AUTH]: null;
[WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH]: null;
[WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH]: Parameters.DisableTwoFactorAuthParams;
[WRITE_COMMANDS.ADD_COMMENT]: Parameters.AddCommentOrAttachementParams;
[WRITE_COMMANDS.ADD_ATTACHMENT]: Parameters.AddCommentOrAttachementParams;
[WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT]: Parameters.AddCommentOrAttachementParams;
Expand Down
17 changes: 15 additions & 2 deletions src/libs/actions/Session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
BeginAppleSignInParams,
BeginGoogleSignInParams,
BeginSignInParams,
DisableTwoFactorAuthParams,
RequestAccountValidationLinkParams,
RequestNewValidateCodeParams,
RequestUnlinkValidationLinkParams,
Expand Down Expand Up @@ -877,7 +878,7 @@ function unlinkLogin(accountID: number, validateCode: string) {
/**
* Toggles two-factor authentication based on the `enable` parameter
*/
function toggleTwoFactorAuth(enable: boolean) {
function toggleTwoFactorAuth(enable: boolean, twoFactorAuthCode = '') {
const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
Expand All @@ -894,6 +895,9 @@ function toggleTwoFactorAuth(enable: boolean) {
key: ONYXKEYS.ACCOUNT,
value: {
isLoading: false,

// When disabling 2FA, the user needs to end up on the step that confirms the setting was disabled
twoFactorAuthStep: enable ? undefined : CONST.TWO_FACTOR_AUTH_STEPS.DISABLED,
},
},
];
Expand All @@ -908,7 +912,16 @@ function toggleTwoFactorAuth(enable: boolean) {
},
];

API.write(enable ? WRITE_COMMANDS.ENABLE_TWO_FACTOR_AUTH : WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH, null, {optimisticData, successData, failureData});
if (enable) {
API.write(WRITE_COMMANDS.ENABLE_TWO_FACTOR_AUTH, null, {optimisticData, successData, failureData});
return;
}

// A 2FA code is required to disable 2FA
const params: DisableTwoFactorAuthParams = {twoFactorAuthCode};

// eslint-disable-next-line rulesdir/no-multiple-api-calls
API.write(WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH, params, {optimisticData, successData, failureData});
}

function updateAuthTokenAndOpenApp(authToken?: string, encryptedAuthToken?: string) {
Expand Down
23 changes: 2 additions & 21 deletions src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, {useState} from 'react';
import React from 'react';
import {View} from 'react-native';
import ConfirmModal from '@components/ConfirmModal';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import ScrollView from '@components/ScrollView';
Expand All @@ -11,13 +10,11 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import StepWrapper from '@pages/settings/Security/TwoFactorAuth/StepWrapper/StepWrapper';
import useTwoFactorAuthContext from '@pages/settings/Security/TwoFactorAuth/TwoFactorAuthContext/useTwoFactorAuth';
import * as Session from '@userActions/Session';
import CONST from '@src/CONST';

function EnabledStep() {
const theme = useTheme();
const styles = useThemeStyles();
const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false);

const {setStep} = useTwoFactorAuthContext();

Expand All @@ -33,7 +30,7 @@ function EnabledStep() {
{
title: translate('twoFactorAuth.disableTwoFactorAuth'),
onPress: () => {
setIsConfirmModalVisible(true);
setStep(CONST.TWO_FACTOR_AUTH_STEPS.GETCODE);
},
icon: Expensicons.Close,
iconFill: theme.danger,
Expand All @@ -46,22 +43,6 @@ function EnabledStep() {
<Text style={styles.textLabel}>{translate('twoFactorAuth.whatIsTwoFactorAuth')}</Text>
</View>
</Section>
<ConfirmModal
title={translate('twoFactorAuth.disableTwoFactorAuth')}
onConfirm={() => {
setIsConfirmModalVisible(false);
setStep(CONST.TWO_FACTOR_AUTH_STEPS.DISABLED);
Session.toggleTwoFactorAuth(false);
}}
onCancel={() => setIsConfirmModalVisible(false)}
onModalHide={() => setIsConfirmModalVisible(false)}
isVisible={isConfirmModalVisible}
prompt={translate('twoFactorAuth.disableTwoFactorAuthConfirmation')}
confirmText={translate('common.disable')}
cancelText={translate('common.cancel')}
shouldShowCancelButton
danger
/>
</ScrollView>
</StepWrapper>
);
Expand Down
72 changes: 72 additions & 0 deletions src/pages/settings/Security/TwoFactorAuth/Steps/GetCode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, {useRef} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import FixedFooter from '@components/FixedFooter';
import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import type {BackToParams} from '@libs/Navigation/types';
import StepWrapper from '@pages/settings/Security/TwoFactorAuth/StepWrapper/StepWrapper';
import useTwoFactorAuthContext from '@pages/settings/Security/TwoFactorAuth/TwoFactorAuthContext/useTwoFactorAuth';
import TwoFactorAuthForm from '@pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm';
import type {BaseTwoFactorAuthFormOnyxProps, BaseTwoFactorAuthFormRef} from '@pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm/types';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';

type GetCodeProps = BaseTwoFactorAuthFormOnyxProps & BackToParams;

function GetCode({account}: GetCodeProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();

const formRef = useRef<BaseTwoFactorAuthFormRef>(null);

const {setStep} = useTwoFactorAuthContext();

return (
<StepWrapper
title={translate('twoFactorAuth.disableTwoFactorAuth')}
shouldEnableKeyboardAvoidingView={false}
onBackButtonPress={() => setStep(CONST.TWO_FACTOR_AUTH_STEPS.ENABLED, CONST.ANIMATION_DIRECTION.OUT)}
onEntryTransitionEnd={() => formRef.current && formRef.current.focus()}
>
<ScrollView contentContainerStyle={styles.flexGrow1}>
<View style={[styles.ph5, styles.mt3]}>
<Text>{translate('twoFactorAuth.explainProcessToRemove')}</Text>
</View>
</ScrollView>
<FixedFooter style={[styles.mt2, styles.pt2]}>
<View style={[styles.mh5, styles.mb4]}>
<TwoFactorAuthForm
innerRef={formRef}
validateInsteadOfDisable={false}
/>
</View>
<Button
success
large
text={translate('twoFactorAuth.disable')}
isLoading={account?.isLoading}
onPress={() => {
if (!formRef.current) {
return;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
formRef.current.validateAndSubmitForm();
}}
/>
</FixedFooter>
</StepWrapper>
);
}

GetCode.displayName = 'GetCode';

export default withOnyx<GetCodeProps, BaseTwoFactorAuthFormOnyxProps>({
account: {key: ONYXKEYS.ACCOUNT},
user: {
key: ONYXKEYS.USER,
},
})(GetCode);
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ import type {BaseTwoFactorAuthFormOnyxProps, BaseTwoFactorAuthFormRef} from './t

type BaseTwoFactorAuthFormProps = BaseTwoFactorAuthFormOnyxProps & {
autoComplete: AutoCompleteVariant;

// Set this to true in order to call the validateTwoFactorAuth action which is used when setting up 2FA for the first time.
// Set this to false in order to disable 2FA when a valid code is entered.
validateInsteadOfDisable?: boolean;
};

function BaseTwoFactorAuthForm({account, autoComplete}: BaseTwoFactorAuthFormProps, ref: ForwardedRef<BaseTwoFactorAuthFormRef>) {
function BaseTwoFactorAuthForm({account, autoComplete, validateInsteadOfDisable}: BaseTwoFactorAuthFormProps, ref: ForwardedRef<BaseTwoFactorAuthFormRef>) {
const {translate} = useLocalize();
const [formError, setFormError] = useState<{twoFactorAuthCode?: string}>({});
const [twoFactorAuthCode, setTwoFactorAuthCode] = useState('');
Expand Down Expand Up @@ -54,8 +58,13 @@ function BaseTwoFactorAuthForm({account, autoComplete}: BaseTwoFactorAuthFormPro
}

setFormError({});
Session.validateTwoFactorAuth(twoFactorAuthCode, shouldClearData);
}, [twoFactorAuthCode, shouldClearData, translate]);

if (validateInsteadOfDisable !== false) {
Session.validateTwoFactorAuth(twoFactorAuthCode, shouldClearData);
return;
}
Session.toggleTwoFactorAuth(false, twoFactorAuthCode);
}, [twoFactorAuthCode, validateInsteadOfDisable, translate, shouldClearData]);

useImperativeHandle(ref, () => ({
validateAndSubmitForm() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import React from 'react';
import BaseTwoFactorAuthForm from './BaseTwoFactorAuthForm';
import type {TwoFactorAuthFormProps} from './types';

function TwoFactorAuthForm({innerRef}: TwoFactorAuthFormProps) {
function TwoFactorAuthForm({innerRef, validateInsteadOfDisable}: TwoFactorAuthFormProps) {
return (
<BaseTwoFactorAuthForm
ref={innerRef}
autoComplete="sms-otp"
validateInsteadOfDisable={validateInsteadOfDisable}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import React from 'react';
import BaseTwoFactorAuthForm from './BaseTwoFactorAuthForm';
import type {TwoFactorAuthFormProps} from './types';

function TwoFactorAuthForm({innerRef}: TwoFactorAuthFormProps) {
function TwoFactorAuthForm({innerRef, validateInsteadOfDisable}: TwoFactorAuthFormProps) {
return (
<BaseTwoFactorAuthForm
ref={innerRef}
autoComplete="one-time-code"
validateInsteadOfDisable={validateInsteadOfDisable}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ type BaseTwoFactorAuthFormRef = {

type TwoFactorAuthFormProps = {
innerRef: ForwardedRef<BaseTwoFactorAuthFormRef>;

// Set this to true in order to call the validateTwoFactorAuth action which is used when setting up 2FA for the first time.
// Set this to false in order to disable 2FA when a valid code is entered.
validateInsteadOfDisable?: boolean;
};

export type {BaseTwoFactorAuthFormOnyxProps, TwoFactorAuthFormProps, BaseTwoFactorAuthFormRef};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {TwoFactorAuthStep} from '@src/types/onyx/Account';
import CodesStep from './Steps/CodesStep';
import DisabledStep from './Steps/DisabledStep';
import EnabledStep from './Steps/EnabledStep';
import GetCodeStep from './Steps/GetCode';
import SuccessStep from './Steps/SuccessStep';
import VerifyStep from './Steps/VerifyStep';
import TwoFactorAuthContext from './TwoFactorAuthContext';
Expand Down Expand Up @@ -62,6 +63,8 @@ function TwoFactorAuthSteps({account}: TwoFactorAuthStepProps) {
return <EnabledStep />;
case CONST.TWO_FACTOR_AUTH_STEPS.DISABLED:
return <DisabledStep />;
case CONST.TWO_FACTOR_AUTH_STEPS.GETCODE:
return <GetCodeStep />;
default:
return <CodesStep backTo={backTo} />;
}
Expand Down

0 comments on commit c7934c5

Please sign in to comment.