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

Require a 2FA code to disable 2FA #48030

Merged
merged 16 commits into from
Sep 2, 2024
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3881,6 +3881,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 @@ -1115,7 +1115,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 @@ -1120,7 +1120,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 @@ -419,7 +419,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}
Copy link
Contributor

Choose a reason for hiding this comment

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

shouldEnableKeyboardAvoidingView caused an issue here. where code input was left behind keyboard

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
Copy link
Member

@parasharrajat parasharrajat Aug 30, 2024

Choose a reason for hiding this comment

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

Technically, it should have bottom margin. I think we can leave this as it is as all of the page for 2FA flow does not have this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm, OK. We should maybe make a followup GH issue to have someone fix that.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I can fix that.

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
Loading