App icon indicating copy to clipboard operation
App copied to clipboard

[$250] Web - Log in -2FA occur when using expired magic code link with modified characters

Open IuliiaHerets opened this issue 1 year ago β€’ 9 comments

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
  • [x] MacOS: Chrome / Safari
  • [ ] MacOS: Desktop

Screenshots/Videos

https://github.com/user-attachments/assets/01575956-a54b-47ec-b35f-574bcdf0db7d

View all open jobs on GitHub

Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~021865040747269371660
  • Upwork Job ID: 1865040747269371660
  • Last Price Increase: 2024-12-06
Issue OwnerCurrent Issue Owner: @suneox

IuliiaHerets avatar Dec 02 '24 21:12 IuliiaHerets

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.

melvin-bot[bot] avatar Dec 02 '24 21:12 melvin-bot[bot]

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

https://github.com/Expensify/App/blob/ab40264dff9a6c21372617c8744b1013c22dd2d2/src/pages/ValidateLoginPage/index.website.tsx#L52

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

https://github.com/Expensify/App/blob/ab40264dff9a6c21372617c8744b1013c22dd2d2/src/pages/ValidateLoginPage/index.website.tsx#L78

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

https://github.com/user-attachments/assets/1962ab05-7371-4d9f-9d27-65374f6e3f98

What alternative solutions did you explore? (Optional)

huult avatar Dec 03 '24 03:12 huult

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

saifelance avatar Dec 03 '24 12:12 saifelance

@twisterdotcom Whoops! This issue is 2 days overdue. Let's get this updated quick!

melvin-bot[bot] avatar Dec 06 '24 09:12 melvin-bot[bot]

Job added to Upwork: https://www.upwork.com/jobs/~021865040747269371660

melvin-bot[bot] avatar Dec 06 '24 14:12 melvin-bot[bot]

Triggered auto assignment to Contributor-plus team member for initial proposal review - @suneox (External)

melvin-bot[bot] avatar Dec 06 '24 14:12 melvin-bot[bot]

https://drive.google.com/file/d/1liBWp_4h2g0SRp5YH2eMqgX7K0HpFWex/view?usp=sharing

twisterdotcom avatar Dec 06 '24 14:12 twisterdotcom

@huult proposal to validate the code before proceeding with sign-in LGTM.

πŸŽ€ πŸ‘€ πŸŽ€ C+ reviewed

suneox avatar Dec 08 '24 07:12 suneox

Triggered auto assignment to @nkuoch, see https://stackoverflow.com/c/expensify/questions/7972 for more details.

melvin-bot[bot] avatar Dec 08 '24 07:12 melvin-bot[bot]

πŸ“£ @suneox πŸŽ‰ An offer has been automatically sent to your Upwork account for the Reviewer role πŸŽ‰ Thanks for contributing to the Expensify app!

Offer link Upwork job

melvin-bot[bot] avatar Dec 09 '24 10:12 melvin-bot[bot]

πŸ“£ @huult πŸŽ‰ An offer has been automatically sent to your Upwork account for the Contributor role πŸŽ‰ Thanks for contributing to the Expensify app!

Offer link Upwork job Please accept the offer and leave a comment on the Github issue letting us know when we can expect a PR to be ready for review πŸ§‘β€πŸ’» Keep in mind: Code of Conduct | Contributing πŸ“–

melvin-bot[bot] avatar Dec 09 '24 10:12 melvin-bot[bot]

Reviewing label has been removed, please complete the "BugZero Checklist".

melvin-bot[bot] avatar Dec 12 '24 01:12 melvin-bot[bot]

The solution for this issue has been :rocket: deployed to production :rocket: in version 9.0.74-8 and is now subject to a 7-day regression period :calendar:. Here is the list of pull requests that resolve this issue:

  • https://github.com/Expensify/App/pull/53756

If no regressions arise, payment will be issued on 2024-12-19. :confetti_ball:

For reference, here are some details about the assignees on this issue:

melvin-bot[bot] avatar Dec 12 '24 01:12 melvin-bot[bot]

@suneox @twisterdotcom @suneox The PR fixing this issue has been merged! The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the BugZero Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]

melvin-bot[bot] avatar Dec 12 '24 01:12 melvin-bot[bot]

BugZero Checklist:

  • [x] [Contributor] Classify the bug:
Bug classification

Source of bug:

  • [ ] 1a. Result of the original design (eg. a case wasn't considered)
  • [x] 1b. Mistake during implementation
  • [ ] 1c. Backend bug
  • [ ] 1z. Other:

Where bug was reported:

  • [x] 2a. Reported on production (eg. bug slipped through the normal regression and PR testing process on staging)
  • [ ] 2b. Reported on staging (eg. found during regression or PR testing)
  • [ ] 2d. Reported on a PR
  • [ ] 2z. Other:

Who reported the bug:

  • [ ] 3a. Expensify user
  • [ ] 3b. Expensify employee
  • [ ] 3c. Contributor
  • [x] 3d. QA
  • [ ] 3z. Other:
  • [x] [Contributor] The offending PR has been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.

    Link to comment: I think this is an edge case we missed implementing, so there isn’t an offending PR here. This feature was implemented long time ago, and it’s possible that the backend has been updated to return a new error. We need to add validation for the code before submitting the sign-in request.

  • [x] [Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.

    Link to discussion: N/A

  • [x] [Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.

    N/A: Due to it isn't an impactful bug

suneox avatar Dec 15 '24 16:12 suneox

Payment Summary:

  • @suneox paid $250 here (Reviewer)
  • @huult paid $250 here (Contributor)

twisterdotcom avatar Dec 19 '24 00:12 twisterdotcom