Skip to content
66 changes: 51 additions & 15 deletions packages/manager/src/OAuth/OAuthCallback.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is one potential issue:

here we were using windows.location which means it would get evaluated (and return a value) before router initiation.

});

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);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using references to avoid triggering the callback more than needed and end up with stale values


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',
];
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the fix to not redirect to one of these routes in order to never be stuck in a logout loop


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 <SplashScreen />;
};
13 changes: 9 additions & 4 deletions packages/manager/src/OAuth/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
)
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just added some typeguard here to satisfy the new callback strict route types


Expand Down
2 changes: 1 addition & 1 deletion packages/manager/src/OAuth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export interface AuthCallbackOptions {
/**
* The raw search or has params sent by the login server
*/
params: string;
params: Record<string, unknown> | string;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/manager/src/features/IAM/hooks/useIsIAMEnabled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ export const checkIAMEnabled = async (
flags: FlagSet,
profile: Profile | undefined
): Promise<boolean> => {
if (!flags?.iam?.enabled) {
if (!flags?.iam?.enabled || !profile) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

not directly related to this PR. just a follow up to #13037

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)
Expand Down
7 changes: 7 additions & 0 deletions packages/manager/src/routes/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -34,6 +40,7 @@ const oauthCallbackRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'oauth/callback',
component: OAuthCallback,
validateSearch: (search: OAuthCallbackSearch) => search,
});

export {
Expand Down