mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-08 15:04:29 +00:00
feat(ui): support /logout path to perform logout from api redirect (#20040)
This commit is contained in:
parent
9331a526ea
commit
30a8d64959
@ -349,7 +349,7 @@ public class AuthenticationCodeFlowHandler {
|
|||||||
if (session != null) {
|
if (session != null) {
|
||||||
LOG.debug("Invalidating the session for logout");
|
LOG.debug("Invalidating the session for logout");
|
||||||
session.invalidate();
|
session.invalidate();
|
||||||
httpServletResponse.sendRedirect(serverUrl);
|
httpServletResponse.sendRedirect(serverUrl + "/logout");
|
||||||
} else {
|
} else {
|
||||||
LOG.error("No session store available for this web context");
|
LOG.error("No session store available for this web context");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import AppContainer from '../AppContainer/AppContainer';
|
|||||||
import Loader from '../common/Loader/Loader';
|
import Loader from '../common/Loader/Loader';
|
||||||
import { UnAuthenticatedAppRouter } from './UnAuthenticatedAppRouter';
|
import { UnAuthenticatedAppRouter } from './UnAuthenticatedAppRouter';
|
||||||
|
|
||||||
|
import { LogoutPage } from '../../pages/LogoutPage/LogoutPage';
|
||||||
import SamlCallback from '../../pages/SamlCallback';
|
import SamlCallback from '../../pages/SamlCallback';
|
||||||
|
|
||||||
const AppRouter = () => {
|
const AppRouter = () => {
|
||||||
@ -86,6 +87,7 @@ const AppRouter = () => {
|
|||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact component={PageNotFound} path={ROUTES.NOT_FOUND} />
|
<Route exact component={PageNotFound} path={ROUTES.NOT_FOUND} />
|
||||||
|
<Route exact component={LogoutPage} path={ROUTES.LOGOUT} />
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
component={AccessNotAllowedPage}
|
component={AccessNotAllowedPage}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import { useHistory } from 'react-router-dom';
|
|||||||
import { ROUTES } from '../../../constants/constants';
|
import { ROUTES } from '../../../constants/constants';
|
||||||
import { useApplicationStore } from '../../../hooks/useApplicationStore';
|
import { useApplicationStore } from '../../../hooks/useApplicationStore';
|
||||||
import { logoutUser, renewToken } from '../../../rest/LoginAPI';
|
import { logoutUser, renewToken } from '../../../rest/LoginAPI';
|
||||||
|
import TokenService from '../../../utils/Auth/TokenService/TokenServiceUtil';
|
||||||
import { setOidcToken } from '../../../utils/LocalStorageUtils';
|
import { setOidcToken } from '../../../utils/LocalStorageUtils';
|
||||||
|
|
||||||
export const GenericAuthenticator = forwardRef(
|
export const GenericAuthenticator = forwardRef(
|
||||||
@ -37,6 +38,7 @@ export const GenericAuthenticator = forwardRef(
|
|||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await logoutUser();
|
await logoutUser();
|
||||||
|
|
||||||
|
TokenService.getInstance().clearRefreshInProgress();
|
||||||
history.push(ROUTES.SIGNIN);
|
history.push(ROUTES.SIGNIN);
|
||||||
setOidcToken('');
|
setOidcToken('');
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
|
|||||||
@ -36,6 +36,7 @@ import { showErrorToast } from '../../../utils/ToastUtils';
|
|||||||
import { ROUTES } from '../../../constants/constants';
|
import { ROUTES } from '../../../constants/constants';
|
||||||
import { useApplicationStore } from '../../../hooks/useApplicationStore';
|
import { useApplicationStore } from '../../../hooks/useApplicationStore';
|
||||||
import { AccessTokenResponse, refreshSAMLToken } from '../../../rest/auth-API';
|
import { AccessTokenResponse, refreshSAMLToken } from '../../../rest/auth-API';
|
||||||
|
import TokenService from '../../../utils/Auth/TokenService/TokenServiceUtil';
|
||||||
import {
|
import {
|
||||||
getOidcToken,
|
getOidcToken,
|
||||||
getRefreshToken,
|
getRefreshToken,
|
||||||
@ -76,24 +77,19 @@ const SamlAuthenticator = forwardRef<AuthenticatorRef, Props>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = async () => {
|
||||||
const token = getOidcToken();
|
const token = getOidcToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
postSamlLogout()
|
try {
|
||||||
.then(() => {
|
await postSamlLogout();
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
try {
|
onLogoutSuccess();
|
||||||
onLogoutSuccess();
|
TokenService.getInstance().clearRefreshInProgress();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// TODO: Handle error on logout failure
|
// TODO: Handle error on logout failure
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('Error while logging out', err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -167,6 +167,7 @@ export const AuthProvider = ({
|
|||||||
resetWebAnalyticSession();
|
resetWebAnalyticSession();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handler to perform logout within application
|
||||||
const onLogoutHandler = useCallback(() => {
|
const onLogoutHandler = useCallback(() => {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
@ -392,6 +393,7 @@ export const AuthProvider = ({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Callback to cleanup session related info upon successful logout
|
||||||
const handleSuccessfulLogout = () => {
|
const handleSuccessfulLogout = () => {
|
||||||
resetUserDetails();
|
resetUserDetails();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -39,6 +39,7 @@ import {
|
|||||||
import { resetWebAnalyticSession } from '../../../utils/WebAnalyticsUtils';
|
import { resetWebAnalyticSession } from '../../../utils/WebAnalyticsUtils';
|
||||||
|
|
||||||
import { toLower } from 'lodash';
|
import { toLower } from 'lodash';
|
||||||
|
import TokenService from '../../../utils/Auth/TokenService/TokenServiceUtil';
|
||||||
import { extractDetailsFromToken } from '../../../utils/AuthProvider.util';
|
import { extractDetailsFromToken } from '../../../utils/AuthProvider.util';
|
||||||
import {
|
import {
|
||||||
getOidcToken,
|
getOidcToken,
|
||||||
@ -181,6 +182,8 @@ const BasicAuthProvider = ({
|
|||||||
try {
|
try {
|
||||||
await logoutUser({ token, refreshToken });
|
await logoutUser({ token, refreshToken });
|
||||||
setOidcToken('');
|
setOidcToken('');
|
||||||
|
setRefreshToken('');
|
||||||
|
TokenService.getInstance().clearRefreshInProgress();
|
||||||
history.push(ROUTES.SIGNIN);
|
history.push(ROUTES.SIGNIN);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showErrorToast(error as AxiosError);
|
showErrorToast(error as AxiosError);
|
||||||
|
|||||||
@ -131,6 +131,7 @@ export const ROUTES = {
|
|||||||
NOT_FOUND: '/404',
|
NOT_FOUND: '/404',
|
||||||
FORBIDDEN: '/403',
|
FORBIDDEN: '/403',
|
||||||
UNAUTHORISED: '/unauthorised',
|
UNAUTHORISED: '/unauthorised',
|
||||||
|
LOGOUT: '/logout',
|
||||||
MY_DATA: '/my-data',
|
MY_DATA: '/my-data',
|
||||||
TOUR: '/tour',
|
TOUR: '/tour',
|
||||||
REPORTS: '/reports',
|
REPORTS: '/reports',
|
||||||
|
|||||||
@ -97,6 +97,9 @@ export const useApplicationStore = create<ApplicationStore>()((set, get) => ({
|
|||||||
onLoginHandler: () => {
|
onLoginHandler: () => {
|
||||||
// This is a placeholder function that will be replaced by the actual function
|
// This is a placeholder function that will be replaced by the actual function
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Handler to perform logout within application
|
||||||
|
*/
|
||||||
onLogoutHandler: () => {
|
onLogoutHandler: () => {
|
||||||
// This is a placeholder function that will be replaced by the actual function
|
// This is a placeholder function that will be replaced by the actual function
|
||||||
},
|
},
|
||||||
@ -110,11 +113,6 @@ export const useApplicationStore = create<ApplicationStore>()((set, get) => ({
|
|||||||
updateAxiosInterceptors: () => {
|
updateAxiosInterceptors: () => {
|
||||||
// This is a placeholder function that will be replaced by the actual function
|
// This is a placeholder function that will be replaced by the actual function
|
||||||
},
|
},
|
||||||
trySilentSignIn: (forceLogout?: boolean) => {
|
|
||||||
if (forceLogout) {
|
|
||||||
// This is a placeholder function that will be replaced by the actual function
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateCurrentUser: (user) => {
|
updateCurrentUser: (user) => {
|
||||||
set({ currentUser: user });
|
set({ currentUser: user });
|
||||||
},
|
},
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Collate.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { useApplicationStore } from '../../hooks/useApplicationStore';
|
||||||
|
import { LogoutPage } from './LogoutPage';
|
||||||
|
|
||||||
|
// Mock the useApplicationStore hook
|
||||||
|
jest.mock('../../hooks/useApplicationStore', () => ({
|
||||||
|
useApplicationStore: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('LogoutPage', () => {
|
||||||
|
const mockOnLogoutHandler = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset mock before each test
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Setup mock implementation
|
||||||
|
(useApplicationStore as unknown as jest.Mock).mockReturnValue({
|
||||||
|
onLogoutHandler: mockOnLogoutHandler,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onLogoutHandler on mount', () => {
|
||||||
|
render(<LogoutPage />);
|
||||||
|
|
||||||
|
expect(mockOnLogoutHandler).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render Loader component with fullScreen prop', () => {
|
||||||
|
const { container } = render(<LogoutPage />);
|
||||||
|
const loaderElement = container.firstChild;
|
||||||
|
|
||||||
|
expect(loaderElement).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Collate.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import Loader from '../../components/common/Loader/Loader';
|
||||||
|
import { useApplicationStore } from '../../hooks/useApplicationStore';
|
||||||
|
|
||||||
|
export const LogoutPage = () => {
|
||||||
|
const { onLogoutHandler } = useApplicationStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onLogoutHandler();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <Loader fullScreen />;
|
||||||
|
};
|
||||||
@ -127,6 +127,7 @@ class TokenService {
|
|||||||
// Clear the refresh flag (used after refresh is complete)
|
// Clear the refresh flag (used after refresh is complete)
|
||||||
clearRefreshInProgress() {
|
clearRefreshInProgress() {
|
||||||
localStorage.removeItem(REFRESH_IN_PROGRESS_KEY);
|
localStorage.removeItem(REFRESH_IN_PROGRESS_KEY);
|
||||||
|
localStorage.removeItem(REFRESHED_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a refresh is already in progress (used by other tabs)
|
// Check if a refresh is already in progress (used by other tabs)
|
||||||
|
|||||||
@ -311,6 +311,7 @@ export const isProtectedRoute = (pathname: string) => {
|
|||||||
ROUTES.HOME,
|
ROUTES.HOME,
|
||||||
ROUTES.AUTH_CALLBACK,
|
ROUTES.AUTH_CALLBACK,
|
||||||
ROUTES.NOT_FOUND,
|
ROUTES.NOT_FOUND,
|
||||||
|
ROUTES.LOGOUT,
|
||||||
].indexOf(pathname) === -1
|
].indexOf(pathname) === -1
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user