Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/libs/actions/Session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,11 @@ function getShortLivedLoginParams(isSupportAuthTokenUsed = false, isSAML = false
function signInWithSupportAuthToken(authToken: string) {
const {optimisticData, finallyData} = getShortLivedLoginParams(true);
API.read(READ_COMMANDS.SIGN_IN_WITH_SUPPORT_AUTH_TOKEN, {authToken}, {optimisticData, finallyData});

// Record the token so the transition pages skip a duplicate sign-in for it. The Public/Auth navigator
// swap re-mounts the transition screen mid-login, and without this the re-mounted page fires this call
// again and trips the support-token rate limit. Matches signInWithShortLivedAuthToken.
NetworkStore.setLastShortAuthToken(authToken);
}

/**
Expand Down
8 changes: 7 additions & 1 deletion src/pages/LogOutPreviousUserPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import {useInitialURLState} from '@components/InitialURLContextProvider';
import useOnyx from '@hooks/useOnyx';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import {getLastShortAuthToken} from '@libs/Network/NetworkStore';
import {isLoggingInAsDelegate as isLoggingInAsDelegateSessionUtils, isLoggingInAsNewUser as isLoggingInAsNewUserSessionUtils} from '@libs/SessionUtils';
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
import Navigation from '@navigation/Navigation';
Expand Down Expand Up @@ -41,7 +42,12 @@ function LogOutPreviousUserPage({route}: LogOutPreviousUserPageProps) {
}

if (isSupportalLogin) {
signInWithSupportAuthToken(shortLivedAuthToken);
// The public transition page may already have started this exact sign-in before the Public/Auth
// navigator swap re-mounted us here. Firing it again trips the support-token rate limit, so skip
// the duplicate but still finish navigating home.
if (shortLivedAuthToken !== getLastShortAuthToken()) {
signInWithSupportAuthToken(shortLivedAuthToken);
}
Comment on lines 44 to +50

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the legacy shortLivedToken is only read on the non-support magic-link path (signInWithShortLivedAuthToken(token)). the supportal branch always uses shortLivedAuthToken, both here and in LogInWithShortLivedAuthTokenPage's support branch, and OldDot's supportLoginNewDot.php only ever sends shortLivedAuthToken for a support transition. so shortLivedToken is always empty on this branch and normalizing wouldn't change anything, keeping shortLivedAuthToken matches how the support token is passed everywhere.

Navigation.isNavigationReady().then(() => {
// We must call goBack() to remove the /transition route from history
Navigation.goBack();
Expand Down
83 changes: 83 additions & 0 deletions tests/ui/SessionTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ function getInitialURL() {
return deeplinkUrl;
}

// cspell:disable-next-line
const TEST_SUPPORT_AUTH_TOKEN = 'supporttoken123';

function getSupportAuthURL() {
const params = new URLSearchParams();

params.set('email', TEST_USER_LOGIN_1);
params.set('delegatorEmail', TEST_USER_LOGIN_2);
params.set('shortLivedAuthToken', TEST_SUPPORT_AUTH_TOKEN);
params.set('authTokenType', CONST.AUTH_TOKEN_TYPES.SUPPORT);

return `${CONST.DEEPLINK_BASE_URL}/transition?${params.toString()}`;
}

describe('Deep linking', () => {
let lastVisitedPath: string | undefined;
let lastVisitedPathConnectionID: ReturnType<typeof Onyx.connect> | undefined;
Expand Down Expand Up @@ -223,3 +237,72 @@ describe('Deep linking', () => {
4 * 60 * 1000,
);
});

describe('Support auth token login', () => {
beforeEach(() => {
jest.restoreAllMocks();
wrapOnyxWithWaitForBatchedUpdates(Onyx);

jest.spyOn(Session, 'signInWithSupportAuthToken').mockImplementation(() => {});

// Set the keys the app needs to finish loading rather than going through a full OpenApp round-trip.
jest.spyOn(AppActions, 'openApp').mockImplementation(() =>
Onyx.multiSet({
[ONYXKEYS.IS_LOADING_APP]: false,
[ONYXKEYS.IS_LOADING_REPORT_DATA]: false,
[ONYXKEYS.HAS_LOADED_APP]: true,
[ONYXKEYS.NVP_ONBOARDING]: {hasCompletedGuidedSetupFlow: true},
}),
);
});

afterEach(async () => {
cleanup();
await act(async () => {
await Onyx.clear();
});
await waitForBatchedUpdatesWithAct();
await waitForNetworkPromises();
PusherHelper.teardown();
jest.clearAllMocks();
Linking.setInitialURL('');
setLastShortAuthToken(null);
});

// Renders the full App and processes a supportal transition, so it runs longer than the suite default.
it(
'does not fire the support sign-in again when LogOutPreviousUserPage re-processes an already-handled token',
async () => {
expect(hasAuthToken()).toBe(false);

// Sign in so the app is on AuthScreens, where LogOutPreviousUserPage owns the /transition route.
const {unmount: unmount1} = render(<App />);
await TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID_2, TEST_USER_LOGIN_2, undefined, TEST_AUTH_TOKEN_2);

await waitForBatchedUpdatesWithAct();

expect(hasAuthToken()).toBe(true);
unmount1();

await waitForBatchedUpdatesWithAct();
await waitForNetworkPromises();

// The public transition page (LogInWithShortLivedAuthTokenPage) records the support token when it
// fires the sign-in. Simulate that, then re-process the SAME support deep link, which lands on
// LogOutPreviousUserPage. It must skip the duplicate sign-in; before the fix it fired
// unconditionally and tripped the support-token rate limit.
setLastShortAuthToken(TEST_SUPPORT_AUTH_TOKEN);
Linking.setInitialURL(getSupportAuthURL());
const {unmount: unmount2} = render(<App />);

await waitForBatchedUpdatesWithAct();

expect(Session.signInWithSupportAuthToken).not.toHaveBeenCalled();

unmount2();
await waitForBatchedUpdatesWithAct();
await waitForNetworkPromises();
},
4 * 60 * 1000,
);
});
Loading