diff --git a/frontend/packages/console-shared/src/utils/__tests__/console-fetch.spec.ts b/frontend/packages/console-shared/src/utils/__tests__/console-fetch.spec.ts index abf3fda29be..527ee36306a 100644 --- a/frontend/packages/console-shared/src/utils/__tests__/console-fetch.spec.ts +++ b/frontend/packages/console-shared/src/utils/__tests__/console-fetch.spec.ts @@ -1,7 +1,7 @@ import { RetryError } from '@console/dynamic-plugin-sdk/src/utils/error/http-error'; import { - shouldLogout, unescapeGoUnicode, + isK8sUrl, validateStatus, } from '@console/shared/src/utils/console-fetch-utils'; import { coFetch } from '../console-fetch'; @@ -41,25 +41,25 @@ describe('consoleFetch', () => { headers.set('content-type', 'application/json'); it('logs out users who get a 401 from k8s', () => { - expect(shouldLogout('/api/kubernetes/api/v1/pods')).toEqual(true); + expect(isK8sUrl('/api/kubernetes/api/v1/pods')).toEqual(true); }); it('respects basePath and logs out users who get a 401 from k8s', () => { const originalBasePath = window.SERVER_FLAGS.basePath; window.SERVER_FLAGS.basePath = '/blah/'; - expect(shouldLogout('/blah/api/kubernetes/api/v1/pods')).toEqual(true); + expect(isK8sUrl('/blah/api/kubernetes/api/v1/pods')).toEqual(true); window.SERVER_FLAGS.basePath = originalBasePath; }); it('does not log out users who get a 401 from chargeback', () => { expect( - shouldLogout('/api/kubernetes/api/v1/namespaces/prd354/services/chargeback/proxy/api'), + isK8sUrl('/api/kubernetes/api/v1/namespaces/prd354/services/chargeback/proxy/api'), ).toEqual(false); }); it('does not log out users who get a 401 from graphs', () => { expect( - shouldLogout( + isK8sUrl( '/api/kubernetes/api/v1/proxy/namespaces/tectonic-system/services/prometheus:9090/api/v1/query?query=100%20-%20(sum(rate(node_cpu%7Bjob%3D%22node-exporter%22%2Cmode%3D%22idle%22%7D%5B2m%5D))%20%2F%20count(node_cpu%7Bjob%3D%22node-exporter%22%2C%20mode%3D%22idle%22%7D))%20*%20100', ), ).toEqual(false); diff --git a/frontend/packages/console-shared/src/utils/console-fetch-utils.ts b/frontend/packages/console-shared/src/utils/console-fetch-utils.ts index af13ba3ea0a..95f5777f8a0 100644 --- a/frontend/packages/console-shared/src/utils/console-fetch-utils.ts +++ b/frontend/packages/console-shared/src/utils/console-fetch-utils.ts @@ -112,8 +112,8 @@ export const applyConsoleHeaders = (url: string, options: RequestInit): RequestI return options; }; -// TODO: url can be url or path, but shouldLogout only handles paths -export const shouldLogout = (url: string): boolean => { +// TODO: url can be url or path, but isK8sUrl only handles paths +export const isK8sUrl = (url: string): boolean => { const k8sRegex = new RegExp(`^${window.SERVER_FLAGS.basePath}api/kubernetes/`); // 401 from k8s. show logout screen if (k8sRegex.test(url)) { @@ -151,7 +151,22 @@ export const validateStatus = async ( method: string, retry: boolean, ) => { + const isK8sRequest = isK8sUrl(url); if (response.ok || response.status === 304) { + // Reset redirect counter on successful k8s request + if (isK8sRequest) { + // We can't use regular import from outside this package, so a dynamic import is required + // This also breaks a nasty cycle - authSvc.logout calls coFetch (which calls validateStatus) + import('@console/internal/module/auth') + .then((m) => m.authSvc) + .then((authSvc) => { + authSvc.resetRedirectCount(); + }) + .catch((e) => { + // eslint-disable-next-line no-console + console.error('Error resetting redirect counter', e); + }); + } return response; } @@ -159,14 +174,14 @@ export const validateStatus = async ( throw new RetryError(); } - if (response.status === 401 && shouldLogout(url)) { + if (response.status === 401 && isK8sRequest) { const next = window.location.pathname + window.location.search + window.location.hash; // This also breaks a nasty cycle - authSvc.logout calls coFetch (which calls validateStatus) import('@console/internal/module/auth') .then((m) => m.authSvc) .then((authSvc) => { - authSvc.logout(next); + authSvc.handle401(next); }) .catch((e) => { // eslint-disable-next-line no-console diff --git a/frontend/public/module/auth.ts b/frontend/public/module/auth.ts index c112813def9..325bdaaacb3 100644 --- a/frontend/public/module/auth.ts +++ b/frontend/public/module/auth.ts @@ -26,6 +26,10 @@ const name = 'name'; const email = 'email'; const clearLocalStorageKeys = [userID, name, email]; +// Constants for redirect loop detection +const AUTH_REDIRECT_COUNT_KEY = 'auth-redirect-count'; +const MAX_AUTH_REDIRECTS = 3; + const setNext = (next: string) => { if (!next) { return; @@ -51,6 +55,39 @@ const clearLocalStorage = (keys: string[]) => { }); }; +// Helper functions for redirect counter +const getAuthRedirectCount = () => { + try { + const count = sessionStorage.getItem(AUTH_REDIRECT_COUNT_KEY); + return count ? parseInt(count, 10) : 0; + } catch (e) { + // eslint-disable-next-line no-console + console.error('Failed to get auth redirect count from sessionStorage', e); + return 0; + } +}; + +const incrementAuthRedirectCount = () => { + try { + const count = getAuthRedirectCount() + 1; + sessionStorage.setItem(AUTH_REDIRECT_COUNT_KEY, count.toString()); + return count; + } catch (e) { + // eslint-disable-next-line no-console + console.error('Failed to increment auth redirect count in sessionStorage', e); + return 0; + } +}; + +const resetAuthRedirectCount = () => { + try { + sessionStorage.removeItem(AUTH_REDIRECT_COUNT_KEY); + } catch (e) { + // eslint-disable-next-line no-console + console.error('Failed to reset auth redirect count in sessionStorage', e); + } +}; + export const authSvc = { userID: () => { const id = loginStateItem(userID); @@ -126,4 +163,37 @@ export const authSvc = { window.location.assign(loginURL); } }, + + // Handle 401 responses with redirect loop detection + handle401: (next) => { + const redirectCount = incrementAuthRedirectCount(); + + // If we've exceeded the max redirects, redirect to the error page + if (redirectCount > MAX_AUTH_REDIRECTS) { + // eslint-disable-next-line no-console + console.error( + `Authentication redirect loop detected (${redirectCount} consecutive 401 responses). Redirecting to error page.`, + ); + + // Build error page URL with query parameters + const errorURL = new URL(loginErrorURL || '/auth/error', window.location.origin); + errorURL.searchParams.set('error', 'redirect_loop_detected'); + errorURL.searchParams.set('error_type', 'auth'); + + // Avoid redirecting if we're already on the error page + if (![window.location.href, window.location.pathname].includes(loginErrorURL)) { + window.location.href = errorURL.toString(); + } + resetAuthRedirectCount(); + return; + } + + // Proceed with normal logout flow + authSvc.logout(next); + }, + + // Reset redirect counter (called on successful k8s requests) + resetRedirectCount: () => { + resetAuthRedirectCount(); + }, };