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

Web - Log in -2FA occur when using expired magic code link with modified characters #53404

Open
1 of 8 tasks
IuliiaHerets opened this issue Dec 2, 2024 · 3 comments
Open
1 of 8 tasks
Assignees
Labels
Bug Something is broken. Auto assigns a BugZero manager. Daily KSv2

Comments

@IuliiaHerets
Copy link

If you haven’t already, check out our contributing guidelines for onboarding and email [email protected] to request to join our Slack channel!


Version Number: v9.0.69-4
Reproducible in staging?: Y
Reproducible in production?: Y
Email or phone of affected tester (no customers): [email protected]
Issue reported by: Applause Internal Team

Action Performed:

  1. Navigate to https://staging.new.expensify.com/
  2. On the log in screen, enter a existing user email
  3. Navigate to the email inbox and open the validate email and click to magic link
  4. Insert "staging." in the URL before the "new.expensify.com"
  5. Wait until the time is out and ask a new magic code
  6. Modify the last portion of the link by a character, E.g if the original link was https://stagin.new.expensify.com/v/7453760/123884
    change a single character > https://staging.new.expensify.com/v/7453760/1238**
  7. Navigate to the compiled link

Expected Result:

As the link is already expired and the link character is modified the link should not lead to 2FA page

Actual Result:

2FA occur when using expired magic code link with modified characters

Workaround:

Unknown

Platforms:

  • Android: Standalone
  • Android: HybridApp
  • Android: mWeb Chrome
  • iOS: Standalone
  • iOS: HybridApp
  • iOS: mWeb Safari
  • MacOS: Chrome / Safari
  • MacOS: Desktop

Screenshots/Videos

Bug6679597_1733170971918.bandicam_2024-12-02_23-15-36-518.mp4

View all open jobs on GitHub

@IuliiaHerets IuliiaHerets added Daily KSv2 Bug Something is broken. Auto assigns a BugZero manager. labels Dec 2, 2024
Copy link

melvin-bot bot commented Dec 2, 2024

Triggered auto assignment to @twisterdotcom (Bug), see https://stackoverflow.com/c/expensify/questions/14418 for more details. Please add this bug to a GH project, as outlined in the SO.

@huult
Copy link
Contributor

huult commented Dec 3, 2024

Edited by proposal-police: This proposal was edited at 2024-12-03 03:54:18 UTC.

Proposal

Please re-state the problem that we are trying to solve in this issue.

2FA occur when using expired magic code link with modified characters

What is the root cause of that problem?

We go to the magic link, change the validation code to the wrong format, and then call this

Session.signInWithValidateCode(Number(accountID), validateCode);

When we call SigninUserWithLink with validateCode (e.g., 155***), the response requiresTwoFactorAuth is true

  • Screenshot 2024-12-03 at 10 47 06
  • Screenshot 2024-12-03 at 10 47 20

So this condition will be valid, and the two-factor authentication page will be displayed

{autoAuthStateWithDefault === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN && is2FARequired && !isSignedIn && !!login && <JustSignedInModal is2FARequired />}

What changes do you think we should make in order to solve the problem?

Because the frontend sends the validation code in the wrong format to the backend, the response returns an incorrect value. We must validate the code before sending it to the backend, and it will show a 'not found' page, as it did previously (if the validation code is wrong, the 'not found' page will be displayed).

//src/pages/ValidateLoginPage/index.website.tsx#L35
    useEffect(() => {
        if (isUserClickedSignIn) {
            // The user clicked the option to sign in the current tab
            Navigation.isNavigationReady().then(() => {
                Navigation.goBack();
            });
            return;
        }
        Session.initAutoAuthState(autoAuthStateWithDefault);

        if (!shouldStartSignInWithValidateCode) {
            if (exitTo) {
                Session.handleExitToNavigation(exitTo);
            }
            return;
        }

+        if (!ValidationUtils.isValidValidateCode(validateCode)) {
+            return;
+        }

        // The user has initiated the sign in process on the same browser, in another tab.
        Session.signInWithValidateCode(Number(accountID), validateCode);

        // Since on Desktop we don't have multi-tab functionality to handle the login flow,
        // we need to `popToTop` the stack after `signInWithValidateCode` in order to
        // perform login for both 2FA and non-2FA accounts.
        desktopLoginRedirect(autoAuthStateWithDefault, isSignedIn);
        // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
    }, []);
POC
Screen.Recording.2024-12-03.at.10.53.19.mov

What alternative solutions did you explore? (Optional)

@saifelance
Copy link

saifelance commented Dec 3, 2024

Proposal

Please re-state the problem that we are trying to solve in this issue.

Key Fixes:

1. Validate Code Length

Added a check to ensure the magic code is of the expected length.

if (!validateCode || validateCode.length !== CONST.VALIDATE_CODE_LENGTH) {
    Session.setAutoAuthState(CONST.AUTO_AUTH_STATE.FAILED);
    return;
}

This is placed inside the first useEffect block that initializes the authentication state.

2. Validate Expiration

Added a check to verify if the code has expired.

if (Session.isValidateCodeExpired(accountID, validateCode)) {
    Session.setAutoAuthState(CONST.AUTO_AUTH_STATE.FAILED);
    return;
}

This ensures the code is not only unmodified but also not expired. It is placed right after the magic code length check.

3. Updated State for Failed Validation

Set the FAILED state when the link is invalid or expired.

Session.setAutoAuthState(CONST.AUTO_AUTH_STATE.FAILED);

This is used in both the length and expiration checks.

What is the root cause of that problem?

Key Changes:

  1. Validate Link Length: Added a length check for validateCode.
  2. Expiration Check: Added a check to verify if the code is expired using a hypothetical Session.isValidateCodeExpired.
  3. Fail State: Immediately sets the state to FAILED if validation fails.
  4. Security Improvements: Handles modified or tampered links by failing early.
  5. Ensure the Session.isValidateCodeExpired function is implemented correctly to check expiration and invalid links. Let me know if you need help with that!

src/pages/ValidateLoginPage/index.website.tsx

import React, { useEffect } from 'react';
import { withOnyx } from 'react-native-onyx';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import ExpiredValidateCodeModal from '@components/ValidateCode/ExpiredValidateCodeModal';
import JustSignedInModal from '@components/ValidateCode/JustSignedInModal';
import ValidateCodeModal from '@components/ValidateCode/ValidateCodeModal';
import desktopLoginRedirect from '@libs/desktopLoginRedirect';
import Navigation from '@libs/Navigation/Navigation';
import * as Session from '@userActions/Session';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type { ValidateLoginPageOnyxProps, ValidateLoginPageProps } from './types';

function ValidateLoginPage({
    account,
    credentials,
    route: {
        params: { accountID, validateCode, exitTo },
    },
    session,
}: ValidateLoginPageProps<ValidateLoginPageOnyxProps>) {
    const login = credentials?.login;
    const autoAuthState = session?.autoAuthState ?? CONST.AUTO_AUTH_STATE.NOT_STARTED;
    const isSignedIn = !!session?.authToken && session?.authTokenType !== CONST.AUTH_TOKEN_TYPES.ANONYMOUS;
    const is2FARequired = !!account?.requiresTwoFactorAuth;
    const cachedAccountID = credentials?.accountID;
    const isUserClickedSignIn = !login && isSignedIn && (autoAuthState === CONST.AUTO_AUTH_STATE.SIGNING_IN || autoAuthState === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN);
    const shouldStartSignInWithValidateCode = !isUserClickedSignIn && !isSignedIn && (!!login || !!exitTo);

    useEffect(() => {
        if (isUserClickedSignIn) {
            // User clicked to sign in on the current tab
            Navigation.isNavigationReady().then(() => {
                Navigation.goBack();
            });
            return;
        }

        // Check if the code is valid and has not been tampered with
        if (!validateCode || validateCode.length !== CONST.VALIDATE_CODE_LENGTH) {
            Session.setAutoAuthState(CONST.AUTO_AUTH_STATE.FAILED);
            return;
        }

        // Check if the code has expired before proceeding
        if (Session.isValidateCodeExpired(accountID, validateCode)) {
            Session.setAutoAuthState(CONST.AUTO_AUTH_STATE.FAILED);
            return;
        }

        Session.initAutoAuthState(autoAuthState);

        if (!shouldStartSignInWithValidateCode) {
            if (exitTo) {
                Session.handleExitToNavigation(exitTo);
            }
            return;
        }

        // Perform sign-in with the validate code
        Session.signInWithValidateCode(Number(accountID), validateCode);

        // Desktop-specific login flow
        desktopLoginRedirect(autoAuthState, isSignedIn);
    }, []);

    useEffect(() => {
        if (!!login || !cachedAccountID || !is2FARequired) {
            if (exitTo) {
                Session.handleExitToNavigation(exitTo);
            }
            return;
        }

        // Handle navigation back for invalid states
        Navigation.isNavigationReady().then(() => {
            Navigation.goBack();
        });
    }, [login, cachedAccountID, is2FARequired, exitTo]);

    return (
        <>
            {autoAuthState === CONST.AUTO_AUTH_STATE.FAILED && <ExpiredValidateCodeModal />}
            {autoAuthState === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN && is2FARequired && !isSignedIn && login && <JustSignedInModal is2FARequired />}
            {autoAuthState === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN && isSignedIn && !exitTo && login && <JustSignedInModal is2FARequired={false} />}
            {(!session?.autoAuthState ? !shouldStartSignInWithValidateCode : autoAuthState === CONST.AUTO_AUTH_STATE.NOT_STARTED) && !exitTo && (
                <ValidateCodeModal
                    accountID={Number(accountID)}
                    code={validateCode}
                />
            )}
            {(!session?.autoAuthState ? shouldStartSignInWithValidateCode : autoAuthState === CONST.AUTO_AUTH_STATE.SIGNING_IN) && <FullScreenLoadingIndicator />}
        </>
    );
}

ValidateLoginPage.displayName = 'ValidateLoginPage';

export default withOnyx<ValidateLoginPageProps<ValidateLoginPageOnyxProps>, ValidateLoginPageOnyxProps>({
    account: { key: ONYXKEYS.ACCOUNT },
    credentials: { key: ONYXKEYS.CREDENTIALS },
    session: { key: ONYXKEYS.SESSION },
})(ValidateLoginPage);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug Something is broken. Auto assigns a BugZero manager. Daily KSv2
Projects
None yet
Development

No branches or pull requests

4 participants