From e41d224ccd073d2a640c7df9378fc281caf0017c Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 25 May 2026 21:53:37 +0530 Subject: [PATCH 1/7] Add RAM-only key for isAuthenticatingWithShortLivedToken --- src/ONYXKEYS.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 8e30de4e3f7b..2992b26e6c19 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -36,6 +36,9 @@ const ONYXKEYS = { /** Boolean flag set whenever we are searching for reports in the server */ RAM_ONLY_IS_SEARCHING_FOR_REPORTS: 'isSearchingForReports', + /** Boolean flag indicating a SignInWithShortLivedAuthToken request is in flight. RAM-only so an interrupted request never persists a stuck `true` to IndexedDB and blocks future reauth attempts. */ + RAM_ONLY_IS_AUTHENTICATING_WITH_SHORT_LIVED_TOKEN: 'isAuthenticatingWithShortLivedToken', + /** Note: These are Persisted Requests - not all requests in the main queue as the key name might lead one to believe */ PERSISTED_REQUESTS: 'networkRequestQueue', PERSISTED_ONGOING_REQUESTS: 'networkOngoingRequestQueue', @@ -1530,6 +1533,7 @@ type OnyxValuesMapping = { [ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID]: string; [ONYXKEYS.ONBOARDING_LAST_VISITED_PATH]: string; [ONYXKEYS.RAM_ONLY_IS_SEARCHING_FOR_REPORTS]: boolean; + [ONYXKEYS.RAM_ONLY_IS_AUTHENTICATING_WITH_SHORT_LIVED_TOKEN]: boolean; [ONYXKEYS.LAST_VISITED_PATH]: string | undefined; [ONYXKEYS.REPORT_LAST_VISIT_TIMES]: OnyxTypes.ReportLastVisitTimes; [ONYXKEYS.RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields; From 702fb89daa52fde2d796d71107c6d84e6d516198 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 25 May 2026 21:54:05 +0530 Subject: [PATCH 2/7] Register RAM-only auth flag in Onyx.init --- src/setup/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/setup/index.ts b/src/setup/index.ts index 7ee6c5dad5a3..1340ef9db9f5 100644 --- a/src/setup/index.ts +++ b/src/setup/index.ts @@ -65,6 +65,7 @@ export default function () { ONYXKEYS.RAM_ONLY_UPDATE_AVAILABLE, ONYXKEYS.RAM_ONLY_UPDATE_REQUIRED, ONYXKEYS.RAM_ONLY_IS_SEARCHING_FOR_REPORTS, + ONYXKEYS.RAM_ONLY_IS_AUTHENTICATING_WITH_SHORT_LIVED_TOKEN, ONYXKEYS.RAM_ONLY_WALLET_ONFIDO, ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE, ONYXKEYS.RAM_ONLY_PLAID_LINK_TOKEN, From e3691ba8bf05d42fc2fec71c305aceb121c4d418 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 25 May 2026 21:55:06 +0530 Subject: [PATCH 3/7] Move isAuthenticatingWithShortLivedToken to RAM-only key in getShortLivedLoginParams --- src/libs/actions/Session/index.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 93b3f7f0352c..bb31cf2556e1 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -154,7 +154,9 @@ function setSupportAuthToken(supportAuthToken: string, email: string, accountID: } function getShortLivedLoginParams(isSupportAuthTokenUsed = false, isSAML = false) { - const optimisticData: Array> = [ + const optimisticData: Array< + OnyxUpdate + > = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.ACCOUNT, @@ -170,14 +172,19 @@ function getShortLivedLoginParams(isSupportAuthTokenUsed = false, isSAML = false value: { signedInWithShortLivedAuthToken: true, signedInWithSAML: isSAML, - isAuthenticatingWithShortLivedToken: true, isSupportAuthTokenUsed, }, }, + // Kept on a RAM-only key so an interrupted SignIn cannot persist a stuck `true` to IndexedDB and block all future reauth attempts. + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.RAM_ONLY_IS_AUTHENTICATING_WITH_SHORT_LIVED_TOKEN, + value: true, + }, ]; // Subsequently, we revert it back to the default value of 'signedInWithShortLivedAuthToken' in 'finallyData' to ensure the user is logged out on refresh - const finallyData: Array> = [ + const finallyData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.ACCOUNT, @@ -192,9 +199,13 @@ function getShortLivedLoginParams(isSupportAuthTokenUsed = false, isSAML = false signedInWithShortLivedAuthToken: null, signedInWithSAML: isSAML, isSupportAuthTokenUsed: null, - isAuthenticatingWithShortLivedToken: false, }, }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.RAM_ONLY_IS_AUTHENTICATING_WITH_SHORT_LIVED_TOKEN, + value: false, + }, ]; const failureData: Array> = []; From 1fb603a202e7a3a5c4a67c131d800a6de712f88a Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 25 May 2026 21:55:37 +0530 Subject: [PATCH 4/7] Read isAuthenticatingWithShortLivedToken from RAM-only key in Reauthentication --- src/libs/Reauthentication.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/libs/Reauthentication.ts b/src/libs/Reauthentication.ts index 3a4fffbeb27f..6cedba8598e5 100644 --- a/src/libs/Reauthentication.ts +++ b/src/libs/Reauthentication.ts @@ -36,7 +36,6 @@ let isSupportAuthTokenUsed = false; Onyx.connectWithoutView({ key: ONYXKEYS.SESSION, callback: (value) => { - isAuthenticatingWithShortLivedToken = !!value?.isAuthenticatingWithShortLivedToken; isSupportAuthTokenUsed = !!value?.isSupportAuthTokenUsed; Sentry.setUser({ @@ -46,6 +45,14 @@ Onyx.connectWithoutView({ }, }); +// Kept on a RAM-only key so an interrupted SignIn cannot persist a stuck `true` to IndexedDB and block all future reauth attempts. +Onyx.connectWithoutView({ + key: ONYXKEYS.RAM_ONLY_IS_AUTHENTICATING_WITH_SHORT_LIVED_TOKEN, + callback: (value) => { + isAuthenticatingWithShortLivedToken = !!value; + }, +}); + let account: OnyxEntry; // Authentication lib is not connected to any changes on the UI // So it is okay to use connectWithoutView here. From bed65d5b3a4adbeedcb5025806e6b8a8d19a2ab9 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 25 May 2026 21:55:48 +0530 Subject: [PATCH 5/7] Remove isAuthenticatingWithShortLivedToken from Session type --- src/types/onyx/Session.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/types/onyx/Session.ts b/src/types/onyx/Session.ts index ecc004f56f57..2709fc56bf2e 100644 --- a/src/types/onyx/Session.ts +++ b/src/types/onyx/Session.ts @@ -37,9 +37,6 @@ type Session = { /** User signed in with short lived token */ signedInWithShortLivedAuthToken?: boolean; - /** Indicates whether the user is re-authenticating with shortLivedToken */ - isAuthenticatingWithShortLivedToken?: boolean; - /** User signed in with SAML */ signedInWithSAML?: boolean; From 990c3fc1667767f03eae28859e66bba3b9f52263 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 25 May 2026 21:56:22 +0530 Subject: [PATCH 6/7] Read isAuthenticatingWithShortLivedToken from RAM-only key in AppState --- src/libs/AppState/index.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/libs/AppState/index.ts b/src/libs/AppState/index.ts index cd12ce459b15..613e90e25d9e 100644 --- a/src/libs/AppState/index.ts +++ b/src/libs/AppState/index.ts @@ -12,6 +12,7 @@ import type {ExtraLoadingContext, GlobalStateSnapshot, NavigationStateInfo, Netw let currentSession: OnyxEntry; let currentNetwork: OnyxEntry; +let currentIsAuthenticatingWithShortLivedToken = false; // We have opted for connectWithoutView here as this is strictly non-UI and only for logging. Onyx.connectWithoutView({ @@ -29,6 +30,14 @@ Onyx.connectWithoutView({ }, }); +// We have opted for connectWithoutView here as this is strictly non-UI and only for logging. +Onyx.connectWithoutView({ + key: ONYXKEYS.RAM_ONLY_IS_AUTHENTICATING_WITH_SHORT_LIVED_TOKEN, + callback: (value) => { + currentIsAuthenticatingWithShortLivedToken = !!value; + }, +}); + /** * Captures current navigation state. */ @@ -54,12 +63,11 @@ function captureNavigationState(): NavigationStateInfo { function captureSessionState(): SessionStateInfo { // Check multiple authentication states to get complete picture const isSessionLoading = !!currentSession?.loading; - const isAuthenticatingWithShortLivedToken = !!currentSession?.isAuthenticatingWithShortLivedToken; const isAuthenticatingFromNetworkStore = isAuthenticatingNetworkStore(); return { isSessionLoading, - isAuthenticatingWithShortLivedToken, + isAuthenticatingWithShortLivedToken: currentIsAuthenticatingWithShortLivedToken, isAuthenticatingFromNetworkStore, }; } From 64a2f010670d394acb3772e61e1a4beb2285e97e Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 25 May 2026 22:02:24 +0530 Subject: [PATCH 7/7] Add unit tests for RAM-only auth flag and legacy session recovery --- tests/actions/SessionTest.ts | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/actions/SessionTest.ts b/tests/actions/SessionTest.ts index fac013420337..1792d690ca3d 100644 --- a/tests/actions/SessionTest.ts +++ b/tests/actions/SessionTest.ts @@ -81,6 +81,43 @@ describe('Session', () => { redirectToSignInSpy.mockRestore(); }); + test('reauthenticate aborts when RAM_ONLY_IS_AUTHENTICATING_WITH_SHORT_LIVED_TOKEN is true', async () => { + // Given a SignIn with short lived token is currently in flight + await Onyx.set(ONYXKEYS.RAM_ONLY_IS_AUTHENTICATING_WITH_SHORT_LIVED_TOKEN, true); + await waitForBatchedUpdates(); + + const redirectToSignInSpy = jest.spyOn(SignInRedirect, 'default').mockImplementation(() => Promise.resolve()); + + // When reauthenticate is called + const result = await reauthenticate('TestCommand'); + await waitForBatchedUpdates(); + + // Then it aborts cleanly without redirecting to sign in + expect(result).toBe(false); + expect(redirectToSignInSpy).not.toHaveBeenCalled(); + + redirectToSignInSpy.mockRestore(); + }); + + test('reauthenticate proceeds even when a legacy session.isAuthenticatingWithShortLivedToken=true is persisted (recovers stuck users)', async () => { + // Given a session in Onyx that still carries the legacy stuck flag from before the RAM-only migration. + // The Session type no longer declares the field, so cast to write the legacy shape. + await Onyx.merge(ONYXKEYS.SESSION, {isAuthenticatingWithShortLivedToken: true} as unknown as Session); + await waitForBatchedUpdates(); + + const redirectToSignInSpy = jest.spyOn(SignInRedirect, 'default').mockImplementation(() => Promise.resolve()); + + // When reauthenticate is called with no credentials stored + const result = await reauthenticate('TestCommand'); + await waitForBatchedUpdates(); + + // Then the legacy persisted flag does NOT block reauth. Reauth proceeds, finds no credentials, and redirects to sign in. + expect(result).toBe(false); + expect(redirectToSignInSpy).toHaveBeenCalledWith('No credentials available'); + + redirectToSignInSpy.mockRestore(); + }); + test('Authenticate is called with saved credentials when a session expires', async () => { // Given a test user and set of authToken with subscriptions to session and credentials const TEST_USER_LOGIN = 'test@testguy.com';