From f018c2bc91c1a173926d7f2ec88ce23f0eb76e4a Mon Sep 17 00:00:00 2001 From: Nikki Wines Date: Tue, 1 Sep 2026 23:37:54 +0100 Subject: [PATCH 1/4] pass authtoken back throuhg signinuser --- src/libs/API/parameters/SignInUserParams.ts | 1 + .../parameters/SignInUserWithLinkParams.ts | 1 + src/libs/actions/Session/index.ts | 23 +++++++++++++++++-- .../ValidateCodeForm/BaseValidateCodeForm.tsx | 5 ++-- src/types/onyx/Credentials.ts | 3 +++ 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/libs/API/parameters/SignInUserParams.ts b/src/libs/API/parameters/SignInUserParams.ts index ddb148fd338a..ad43f7de528d 100644 --- a/src/libs/API/parameters/SignInUserParams.ts +++ b/src/libs/API/parameters/SignInUserParams.ts @@ -6,6 +6,7 @@ type SignInUserParams = { preferredLocale: Locale | null; validateCode?: string; deviceInfo: string; + authToken?: string; }; export default SignInUserParams; diff --git a/src/libs/API/parameters/SignInUserWithLinkParams.ts b/src/libs/API/parameters/SignInUserWithLinkParams.ts index bc479d9f5b6d..05acb50fccb3 100644 --- a/src/libs/API/parameters/SignInUserWithLinkParams.ts +++ b/src/libs/API/parameters/SignInUserWithLinkParams.ts @@ -6,6 +6,7 @@ type SignInUserWithLinkParams = { twoFactorAuthCode?: string; preferredLocale: Locale | null; deviceInfo: string; + authToken?: string; }; export default SignInUserWithLinkParams; diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index c69bf42ec5e3..1874ad0bf6da 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -896,7 +896,14 @@ function setIsAuthenticatingWithShortLivedToken(isAuthenticating: boolean) { * * @param validateCode - 6 digit code required for login */ -function signIn(validateCode: string, preferredLocale: Locale | undefined, twoFactorAuthCode: string | undefined, login: string | undefined, storedValidateCode: string | undefined) { +function signIn( + validateCode: string, + preferredLocale: Locale | undefined, + twoFactorAuthCode: string | undefined, + login: string | undefined, + storedValidateCode: string | undefined, + storedAuthToken: string | undefined, +) { const optimisticData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -954,11 +961,17 @@ function signIn(validateCode: string, preferredLocale: Locale | undefined, twoFa params.validateCode = validateCode || storedValidateCode; } + // If this is the 2FA step we have a short-lived 2FA authentication authToken from the earlier call, send it + // so the backend can complete the login + if (twoFactorAuthCode && storedAuthToken) { + params.authToken = storedAuthToken; + } + API.write(WRITE_COMMANDS.SIGN_IN_USER, params, {optimisticData, successData, failureData}); }); } -function signInWithValidateCode(accountID: number, code: string, preferredLocale: Locale | undefined, twoFactorAuthCode = '', storedValidateCode?: string) { +function signInWithValidateCode(accountID: number, code: string, preferredLocale: Locale | undefined, twoFactorAuthCode = '', storedValidateCode?: string, storedAuthToken?: string) { // If this is called from the 2fa step, use the validateCode stored in Onyx (passed in as `storedValidateCode`) // instead of the one passed from the component state because the state is changing when this method is called. const validateCode = twoFactorAuthCode ? storedValidateCode : code; @@ -1030,6 +1043,12 @@ function signInWithValidateCode(accountID: number, code: string, preferredLocale deviceInfo, }; + // If this is the 2FA step and Auth issued a short-lived continuation authToken on the earlier call, send it + // so Auth can complete the login without needing to re-verify the (now-expired) validateCode. + if (twoFactorAuthCode && storedAuthToken) { + params.authToken = storedAuthToken; + } + API.write(WRITE_COMMANDS.SIGN_IN_USER_WITH_LINK, params, {optimisticData, successData, failureData}); }); } diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx index 73bb2a705c72..fef0475d24cf 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -299,9 +299,9 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco const accountID = credentials?.accountID; if (accountID) { - signInWithValidateCode(accountID, validateCode, preferredLocale, recoveryCodeOr2faCode, credentials?.validateCode); + signInWithValidateCode(accountID, validateCode, preferredLocale, recoveryCodeOr2faCode, credentials?.validateCode, credentials?.authToken); } else { - signIn(validateCode, preferredLocale, recoveryCodeOr2faCode, credentials?.login, credentials?.validateCode); + signIn(validateCode, preferredLocale, recoveryCodeOr2faCode, credentials?.login, credentials?.validateCode, credentials?.authToken); } }, [ account?.isLoading, @@ -310,6 +310,7 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco credentials?.validateCode, credentials?.accountID, credentials?.login, + credentials?.authToken, isUsingRecoveryCode, recoveryCode, twoFactorAuthCode, diff --git a/src/types/onyx/Credentials.ts b/src/types/onyx/Credentials.ts index 135cbae5fe76..d33ada296bac 100644 --- a/src/types/onyx/Credentials.ts +++ b/src/types/onyx/Credentials.ts @@ -12,6 +12,9 @@ type Credentials = { /** The validate code */ validateCode?: string; + /** The short-lived authToken issued when a 2FA code is required */ + authToken?: string; + /** The auto-generated login. */ autoGeneratedLogin?: string; From 8baf9d1ef0beb69b59acffde1234385524eee721 Mon Sep 17 00:00:00 2001 From: Nikki Wines Date: Wed, 2 Sep 2026 00:23:43 +0100 Subject: [PATCH 2/4] add unit tests --- tests/actions/SessionTest.ts | 45 ++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/actions/SessionTest.ts b/tests/actions/SessionTest.ts index 7fae7b63f1f0..0dbafa815cc5 100644 --- a/tests/actions/SessionTest.ts +++ b/tests/actions/SessionTest.ts @@ -1171,6 +1171,29 @@ describe('Session', () => { writeSpy.mockRestore(); }); + + test('sends the stored authToken during the 2FA step', async () => { + const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + + SessionUtil.signIn('', undefined, '654321', 'user@expensify.com', 'stored-code', 'stored-auth-token'); + await waitForBatchedUpdates(); + + expect(writeSpy.mock.calls.at(0)?.at(1)).toEqual(expect.objectContaining({authToken: 'stored-auth-token'})); + + writeSpy.mockRestore(); + }); + + test('does not send an authToken on the initial validate code submission', async () => { + const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + + // No 2FA code yet, even though a stored authToken is passed in - shouldn't be sent. + SessionUtil.signIn('112233', undefined, undefined, 'user@expensify.com', undefined, 'stored-auth-token'); + await waitForBatchedUpdates(); + + expect(writeSpy.mock.calls.at(0)?.at(1)).not.toHaveProperty('authToken'); + + writeSpy.mockRestore(); + }); }); describe('requestUnlinkValidationLink', () => { @@ -1214,5 +1237,27 @@ describe('Session', () => { writeSpy.mockRestore(); }); + + test('sends the stored authToken during the 2FA step', async () => { + const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + + SessionUtil.signInWithValidateCode(123, 'ignored-code', undefined, '654321', 'stored-code', 'stored-auth-token'); + await waitForBatchedUpdates(); + + expect(writeSpy.mock.calls.at(0)?.at(1)).toEqual(expect.objectContaining({authToken: 'stored-auth-token'})); + + writeSpy.mockRestore(); + }); + + test('does not send an authToken on the initial validate code submission', async () => { + const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined); + + SessionUtil.signInWithValidateCode(123, '112233', undefined, undefined, undefined, 'stored-auth-token'); + await waitForBatchedUpdates(); + + expect(writeSpy.mock.calls.at(0)?.at(1)).not.toHaveProperty('authToken'); + + writeSpy.mockRestore(); + }); }); }); From 966996db6902f555ebfdf384f3d564206ed68072 Mon Sep 17 00:00:00 2001 From: Nikki Wines Date: Wed, 2 Sep 2026 01:52:18 +0100 Subject: [PATCH 3/4] remove comment --- src/libs/actions/Session/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 1874ad0bf6da..06a73c0dcb35 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -1043,8 +1043,6 @@ function signInWithValidateCode(accountID: number, code: string, preferredLocale deviceInfo, }; - // If this is the 2FA step and Auth issued a short-lived continuation authToken on the earlier call, send it - // so Auth can complete the login without needing to re-verify the (now-expired) validateCode. if (twoFactorAuthCode && storedAuthToken) { params.authToken = storedAuthToken; } From 8c467053c9c22141d6d715a817c6b7c9ca38d4d0 Mon Sep 17 00:00:00 2001 From: Nikki Wines Date: Wed, 2 Sep 2026 01:57:13 +0100 Subject: [PATCH 4/4] fix type error --- src/libs/actions/Session/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 06a73c0dcb35..69b54b71081d 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -902,7 +902,7 @@ function signIn( twoFactorAuthCode: string | undefined, login: string | undefined, storedValidateCode: string | undefined, - storedAuthToken: string | undefined, + storedAuthToken?: string, ) { const optimisticData: Array> = [ {