diff --git a/packages/manager/src/OAuth/OAuthCallback.tsx b/packages/manager/src/OAuth/OAuthCallback.tsx
index 454bea3f446..28a3ff2ea49 100644
--- a/packages/manager/src/OAuth/OAuthCallback.tsx
+++ b/packages/manager/src/OAuth/OAuthCallback.tsx
@@ -1,11 +1,14 @@
import * as Sentry from '@sentry/react';
import { useNavigate } from '@tanstack/react-router';
+import { useSearch } from '@tanstack/react-router';
import React from 'react';
import { SplashScreen } from 'src/components/SplashScreen';
import { clearStorageAndRedirectToLogout, handleOAuthCallback } from './oauth';
+import type { LinkProps } from '@tanstack/react-router';
+
/**
* Login will redirect back to Cloud Manager with a URL like:
* 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';
*/
export const OAuthCallback = () => {
const navigate = useNavigate();
- const authenticate = async () => {
- try {
- const { returnTo } = await handleOAuthCallback({
- params: location.search,
- });
-
- navigate({ to: returnTo });
- } catch (error) {
- // eslint-disable-next-line no-console
- console.error(error);
- Sentry.captureException(error);
- clearStorageAndRedirectToLogout();
- }
- };
+ const search = useSearch({
+ from: '/oauth/callback',
+ });
+
+ const hasStartedAuth = React.useRef(false);
+ const isAuthenticating = React.useRef(false);
React.useEffect(() => {
+ // Prevent running if already started or currently running
+ if (hasStartedAuth.current || isAuthenticating.current) {
+ return;
+ }
+
+ hasStartedAuth.current = true;
+ isAuthenticating.current = true;
+
+ const authenticate = async () => {
+ try {
+ const { returnTo } = await handleOAuthCallback({
+ params: search,
+ });
+
+ // None of these paths are valid return destinations
+ const invalidReturnToPaths: LinkProps['to'][] = [
+ '/logout',
+ '/admin/callback',
+ '/oauth/callback',
+ '/cancel',
+ ];
+
+ const isInvalidReturnTo =
+ !returnTo || invalidReturnToPaths.some((path) => returnTo === path);
+
+ if (isInvalidReturnTo) {
+ navigate({ to: '/' });
+ return;
+ }
+
+ navigate({ to: returnTo });
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(error);
+ Sentry.captureException(error);
+ clearStorageAndRedirectToLogout();
+ } finally {
+ isAuthenticating.current = false;
+ }
+ };
+
authenticate();
- }, []);
+ }, [navigate, search]);
return ;
};
diff --git a/packages/manager/src/OAuth/oauth.ts b/packages/manager/src/OAuth/oauth.ts
index 85b08c4618b..3d111dbecbe 100644
--- a/packages/manager/src/OAuth/oauth.ts
+++ b/packages/manager/src/OAuth/oauth.ts
@@ -187,10 +187,13 @@ export async function redirectToLogin() {
* @returns Some information about the new session because authentication was successfull
*/
export async function handleOAuthCallback(options: AuthCallbackOptions) {
+ const paramsObject =
+ typeof options.params === 'string'
+ ? getQueryParamsFromQueryString(options.params)
+ : options.params;
+
const { data: params, error: parseParamsError } = await tryCatch(
- OAuthCallbackParamsSchema.validate(
- getQueryParamsFromQueryString(options.params)
- )
+ OAuthCallbackParamsSchema.validate(paramsObject)
);
if (parseParamsError) {
@@ -296,7 +299,9 @@ export async function handleLoginAsCustomerCallback(
) {
const { data: params, error } = await tryCatch(
LoginAsCustomerCallbackParamsSchema.validate(
- getQueryParamsFromQueryString(options.params)
+ typeof options.params === 'string'
+ ? getQueryParamsFromQueryString(options.params)
+ : options.params
)
);
diff --git a/packages/manager/src/OAuth/types.ts b/packages/manager/src/OAuth/types.ts
index 6dd3e5d14a4..e0b5b0e3fe1 100644
--- a/packages/manager/src/OAuth/types.ts
+++ b/packages/manager/src/OAuth/types.ts
@@ -61,7 +61,7 @@ export interface AuthCallbackOptions {
/**
* The raw search or has params sent by the login server
*/
- params: string;
+ params: Record | string;
}
/**
diff --git a/packages/manager/src/features/IAM/hooks/useIsIAMEnabled.ts b/packages/manager/src/features/IAM/hooks/useIsIAMEnabled.ts
index 30024a756b7..8d5fc172f1e 100644
--- a/packages/manager/src/features/IAM/hooks/useIsIAMEnabled.ts
+++ b/packages/manager/src/features/IAM/hooks/useIsIAMEnabled.ts
@@ -50,12 +50,12 @@ export const checkIAMEnabled = async (
flags: FlagSet,
profile: Profile | undefined
): Promise => {
- if (!flags?.iam?.enabled) {
+ if (!flags?.iam?.enabled || !profile) {
return false;
}
try {
- if (profile?.username) {
+ if (profile.username) {
// For restricted users ONLY, get permissions
const permissions = await queryClient.ensureQueryData(
queryOptions(iamQueries.user(profile.username)._ctx.accountPermissions)
diff --git a/packages/manager/src/routes/auth/index.ts b/packages/manager/src/routes/auth/index.ts
index 8b91f930a0d..43ad01f2c6f 100644
--- a/packages/manager/src/routes/auth/index.ts
+++ b/packages/manager/src/routes/auth/index.ts
@@ -7,6 +7,12 @@ import { OAuthCallback } from 'src/OAuth/OAuthCallback';
import { rootRoute } from '../root';
+interface OAuthCallbackSearch {
+ code?: string;
+ returnTo?: string;
+ state?: string;
+}
+
interface CancelLandingSearch {
survey_link?: string;
}
@@ -34,6 +40,7 @@ const oauthCallbackRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'oauth/callback',
component: OAuthCallback,
+ validateSearch: (search: OAuthCallbackSearch) => search,
});
export {