Skip to content

Commit 9147732

Browse files
fix: [UIE-9661] - oAuth callback improvements (#13105)
* oAuth callback improvements * cleanup
1 parent 78aada0 commit 9147732

File tree

5 files changed

+70
-22
lines changed

5 files changed

+70
-22
lines changed
Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import * as Sentry from '@sentry/react';
22
import { useNavigate } from '@tanstack/react-router';
3+
import { useSearch } from '@tanstack/react-router';
34
import React from 'react';
45

56
import { SplashScreen } from 'src/components/SplashScreen';
67

78
import { clearStorageAndRedirectToLogout, handleOAuthCallback } from './oauth';
89

10+
import type { LinkProps } from '@tanstack/react-router';
11+
912
/**
1013
* Login will redirect back to Cloud Manager with a URL like:
1114
* https://cloud.linode.com/oauth/callback?returnTo=%2F&state=066a6ad9-b19a-43bb-b99a-ef0b5d4fc58d&code=42ddf75dfa2cacbad897
@@ -14,24 +17,57 @@ import { clearStorageAndRedirectToLogout, handleOAuthCallback } from './oauth';
1417
*/
1518
export const OAuthCallback = () => {
1619
const navigate = useNavigate();
17-
const authenticate = async () => {
18-
try {
19-
const { returnTo } = await handleOAuthCallback({
20-
params: location.search,
21-
});
22-
23-
navigate({ to: returnTo });
24-
} catch (error) {
25-
// eslint-disable-next-line no-console
26-
console.error(error);
27-
Sentry.captureException(error);
28-
clearStorageAndRedirectToLogout();
29-
}
30-
};
20+
const search = useSearch({
21+
from: '/oauth/callback',
22+
});
23+
24+
const hasStartedAuth = React.useRef(false);
25+
const isAuthenticating = React.useRef(false);
3126

3227
React.useEffect(() => {
28+
// Prevent running if already started or currently running
29+
if (hasStartedAuth.current || isAuthenticating.current) {
30+
return;
31+
}
32+
33+
hasStartedAuth.current = true;
34+
isAuthenticating.current = true;
35+
36+
const authenticate = async () => {
37+
try {
38+
const { returnTo } = await handleOAuthCallback({
39+
params: search,
40+
});
41+
42+
// None of these paths are valid return destinations
43+
const invalidReturnToPaths: LinkProps['to'][] = [
44+
'/logout',
45+
'/admin/callback',
46+
'/oauth/callback',
47+
'/cancel',
48+
];
49+
50+
const isInvalidReturnTo =
51+
!returnTo || invalidReturnToPaths.some((path) => returnTo === path);
52+
53+
if (isInvalidReturnTo) {
54+
navigate({ to: '/' });
55+
return;
56+
}
57+
58+
navigate({ to: returnTo });
59+
} catch (error) {
60+
// eslint-disable-next-line no-console
61+
console.error(error);
62+
Sentry.captureException(error);
63+
clearStorageAndRedirectToLogout();
64+
} finally {
65+
isAuthenticating.current = false;
66+
}
67+
};
68+
3369
authenticate();
34-
}, []);
70+
}, [navigate, search]);
3571

3672
return <SplashScreen />;
3773
};

packages/manager/src/OAuth/oauth.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,13 @@ export async function redirectToLogin() {
187187
* @returns Some information about the new session because authentication was successfull
188188
*/
189189
export async function handleOAuthCallback(options: AuthCallbackOptions) {
190+
const paramsObject =
191+
typeof options.params === 'string'
192+
? getQueryParamsFromQueryString(options.params)
193+
: options.params;
194+
190195
const { data: params, error: parseParamsError } = await tryCatch(
191-
OAuthCallbackParamsSchema.validate(
192-
getQueryParamsFromQueryString(options.params)
193-
)
196+
OAuthCallbackParamsSchema.validate(paramsObject)
194197
);
195198

196199
if (parseParamsError) {
@@ -296,7 +299,9 @@ export async function handleLoginAsCustomerCallback(
296299
) {
297300
const { data: params, error } = await tryCatch(
298301
LoginAsCustomerCallbackParamsSchema.validate(
299-
getQueryParamsFromQueryString(options.params)
302+
typeof options.params === 'string'
303+
? getQueryParamsFromQueryString(options.params)
304+
: options.params
300305
)
301306
);
302307

packages/manager/src/OAuth/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export interface AuthCallbackOptions {
6161
/**
6262
* The raw search or has params sent by the login server
6363
*/
64-
params: string;
64+
params: Record<string, unknown> | string;
6565
}
6666

6767
/**

packages/manager/src/features/IAM/hooks/useIsIAMEnabled.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,12 @@ export const checkIAMEnabled = async (
5050
flags: FlagSet,
5151
profile: Profile | undefined
5252
): Promise<boolean> => {
53-
if (!flags?.iam?.enabled) {
53+
if (!flags?.iam?.enabled || !profile) {
5454
return false;
5555
}
5656

5757
try {
58-
if (profile?.username) {
58+
if (profile.username) {
5959
// For restricted users ONLY, get permissions
6060
const permissions = await queryClient.ensureQueryData(
6161
queryOptions(iamQueries.user(profile.username)._ctx.accountPermissions)

packages/manager/src/routes/auth/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import { OAuthCallback } from 'src/OAuth/OAuthCallback';
77

88
import { rootRoute } from '../root';
99

10+
interface OAuthCallbackSearch {
11+
code?: string;
12+
returnTo?: string;
13+
state?: string;
14+
}
15+
1016
interface CancelLandingSearch {
1117
survey_link?: string;
1218
}
@@ -34,6 +40,7 @@ const oauthCallbackRoute = createRoute({
3440
getParentRoute: () => rootRoute,
3541
path: 'oauth/callback',
3642
component: OAuthCallback,
43+
validateSearch: (search: OAuthCallbackSearch) => search,
3744
});
3845

3946
export {

0 commit comments

Comments
 (0)