Skip to content

Commit b3e119e

Browse files
bug: Session not cleared upon different logons
https://track.akamai.com/jira/browse/LILO-1461
1 parent 98709a9 commit b3e119e

File tree

3 files changed

+185
-3
lines changed

3 files changed

+185
-3
lines changed

packages/manager/src/OAuth/oauth.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,3 +328,5 @@ export async function handleLoginAsCustomerCallback(
328328
expiresIn: params.expires_in,
329329
};
330330
}
331+
332+
export { getLoginURL } from './constants';
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { renderHook, waitFor } from '@testing-library/react';
2+
3+
import { storage } from 'src/utilities/storage';
4+
5+
import { useInitialRequests } from './useInitialRequests';
6+
7+
vi.stubEnv('REACT_APP_CLIENT_ID', 'test-client-id');
8+
vi.stubEnv('REACT_APP_LOGIN_ROOT', 'https://login.test');
9+
10+
const queryClientMock = {
11+
prefetchQuery: vi.fn().mockResolvedValue(undefined),
12+
};
13+
14+
vi.mock('@tanstack/react-query', async (importOriginal) => {
15+
const actual = await importOriginal();
16+
return {
17+
...actual,
18+
useQueryClient: () => queryClientMock,
19+
};
20+
});
21+
22+
const oauthMocks = vi.hoisted(() => ({
23+
validateTokenAndSession: vi.fn(),
24+
}));
25+
26+
vi.mock('src/OAuth/oauth', async () => {
27+
const actual = await vi.importActual('src/OAuth/oauth');
28+
return {
29+
...actual,
30+
validateTokenAndSession: oauthMocks.validateTokenAndSession,
31+
};
32+
});
33+
34+
describe('OAuth token verification and initial data fetch', () => {
35+
beforeEach(() => {
36+
vi.resetAllMocks();
37+
storage.authentication.token.clear();
38+
vi.mocked(require('react-redux')).useSelector = vi.fn().mockReturnValue(false);
39+
});
40+
41+
it('redirects to logout when login server reports the token does not match the session', async () => {
42+
storage.authentication.token.set('Bearer faketoken');
43+
44+
global.fetch = vi.fn().mockResolvedValue({
45+
ok: true,
46+
json: async () => ({ match: false }),
47+
} as any);
48+
49+
const { result } = renderHook(() => useInitialRequests());
50+
51+
await waitFor(() => expect(oauthMocks.validateTokenAndSession).toHaveBeenCalled());
52+
});
53+
54+
it('runs initial requests when login server confirms the token belongs to the session (USER_MATCH)', async () => {
55+
storage.authentication.token.set('Bearer faketoken');
56+
57+
global.fetch = vi.fn().mockResolvedValue({
58+
ok: true,
59+
json: async () => ({ match: true }),
60+
} as any);
61+
62+
const { result } = renderHook(() => useInitialRequests());
63+
64+
await waitFor(() => expect(queryClientMock.prefetchQuery).toHaveBeenCalled());
65+
await waitFor(() => expect(result.current.isLoading).toBe(false));
66+
67+
expect(oauthMocks.validateTokenAndSession).not.toHaveBeenCalled();
68+
});
69+
70+
it('falls back to running initial requests if token verification fetch fails (network/error)', async () => {
71+
storage.authentication.token.set('Bearer faketoken');
72+
73+
global.fetch = vi.fn().mockRejectedValue(new Error('network'));
74+
75+
const { result } = renderHook(() => useInitialRequests());
76+
77+
await waitFor(() => expect(queryClientMock.prefetchQuery).toHaveBeenCalled());
78+
await waitFor(() => expect(result.current.isLoading).toBe(false));
79+
80+
expect(oauthMocks.validateTokenAndSession).not.toHaveBeenCalled();
81+
});
82+
83+
it('does not call the login server verify endpoint for Admin tokens', async () => {
84+
storage.authentication.token.set('Admin admintoken');
85+
86+
global.fetch = vi.fn().mockRejectedValue(new Error('should-not-be-called'));
87+
88+
const { result } = renderHook(() => useInitialRequests());
89+
90+
await waitFor(() => expect(result.current.isLoading).toBe(false));
91+
92+
expect(global.fetch).not.toHaveBeenCalled();
93+
});
94+
});

packages/manager/src/hooks/useInitialRequests.ts

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,106 @@
11
import { accountQueries, profileQueries } from '@linode/queries';
22
import { useQueryClient } from '@tanstack/react-query';
33
import * as React from 'react';
4+
import { useSelector } from 'react-redux';
5+
6+
import { getClientId } from 'src/OAuth/constants';
7+
import {
8+
clearStorageAndRedirectToLogout,
9+
getIsAdminToken,
10+
getLoginURL,
11+
redirectToLogin,
12+
} from 'src/OAuth/oauth';
13+
import { storage } from 'src/utilities/storage';
14+
15+
import type { ApplicationState } from 'src/store';
416

517
/**
618
* This hook is responsible for making Cloud Manager's initial requests.
19+
* It also verifies that the token in localStorage belongs to the same user
20+
* as the Flask session cookie (via /oauth/verify).
21+
*
722
* It exposes a `isLoading` value so that we can render a loading page
8-
* as we make our inital requests.
23+
* as we make our initial requests.
924
*/
1025
export const useInitialRequests = () => {
1126
const queryClient = useQueryClient();
1227

28+
const token = storage.authentication.token.get();
29+
const tokenExists = Boolean(token);
30+
31+
const pendingUpload = useSelector(
32+
(state: ApplicationState) => state.pendingUpload
33+
);
34+
1335
const [isLoading, setIsLoading] = React.useState(true);
1436

1537
React.useEffect(() => {
16-
makeInitialRequests();
17-
}, []);
38+
const isAuthCallback =
39+
window.location.pathname === '/oauth/callback' ||
40+
window.location.pathname === '/admin/callback';
41+
42+
if (isAuthCallback) {
43+
setIsLoading(false);
44+
return;
45+
}
46+
47+
if (!tokenExists && !pendingUpload) {
48+
redirectToLogin();
49+
return;
50+
}
51+
52+
if (!tokenExists) {
53+
setIsLoading(false);
54+
return;
55+
}
56+
57+
validateTokenAndSession();
58+
}, [tokenExists, pendingUpload]);
59+
60+
const validateTokenAndSession = async () => {
61+
const storedToken = storage.authentication.token.get();
62+
63+
if (!storedToken) {
64+
makeInitialRequests();
65+
return;
66+
}
67+
68+
if (getIsAdminToken(storedToken)) {
69+
makeInitialRequests();
70+
return;
71+
}
72+
73+
try {
74+
const tokenValue = storedToken.replace(/^Bearer\s+/i, '');
75+
76+
const response = await fetch(
77+
`${getLoginURL()}/oauth/verify?client_id=${getClientId()}`,
78+
{
79+
credentials: 'include',
80+
headers: {
81+
Authorization: `Bearer ${tokenValue}`,
82+
},
83+
method: 'POST',
84+
}
85+
);
86+
87+
if (response.ok) {
88+
const result = await response.json();
89+
90+
if (result.match === true) {
91+
makeInitialRequests();
92+
return;
93+
}
94+
95+
clearStorageAndRedirectToLogout();
96+
return;
97+
}
98+
99+
clearStorageAndRedirectToLogout();
100+
} catch (error) {
101+
makeInitialRequests();
102+
}
103+
};
18104

19105
/**
20106
* We make a series of requests for data on app load. The flow is:

0 commit comments

Comments
 (0)