[$250] Web - Log in -2FA occur when using expired magic code link with modified characters
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:
- Navigate to https://staging.new.expensify.com/
- On the log in screen, enter a existing user email
- Navigate to the email inbox and open the validate email and click to magic link
- Insert "staging." in the URL before the "new.expensify.com"
- Wait until the time is out and ask a new magic code
- 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**
- 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
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 Owner
Current Issue Owner: @suneox
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.
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
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)
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:
-
Validate Link Length: Added a length check for
validateCode. -
Expiration Check: Added a check to verify if the code is expired using a hypothetical
Session.isValidateCodeExpired. -
Fail State: Immediately sets the state to
FAILEDif validation fails. - Security Improvements: Handles modified or tampered links by failing early.
- Ensure the
Session.isValidateCodeExpiredfunction 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);
@twisterdotcom Whoops! This issue is 2 days overdue. Let's get this updated quick!
Job added to Upwork: https://www.upwork.com/jobs/~021865040747269371660
Triggered auto assignment to Contributor-plus team member for initial proposal review - @suneox (External)
https://drive.google.com/file/d/1liBWp_4h2g0SRp5YH2eMqgX7K0HpFWex/view?usp=sharing
@huult proposal to validate the code before proceeding with sign-in LGTM.
π π π C+ reviewed
Triggered auto assignment to @nkuoch, see https://stackoverflow.com/c/expensify/questions/7972 for more details.
π£ @suneox π An offer has been automatically sent to your Upwork account for the Reviewer role π Thanks for contributing to the Expensify app!
π£ @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 π
Reviewing label has been removed, please complete the "BugZero Checklist".
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:
- @suneox requires payment automatic offer (Reviewer)
- @huult requires payment automatic offer (Contributor)
@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]
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