feat: Add logout for oidc (#2840)

* add oidc logout route, button in menu

* removed old comments

* use local storage to trigger signout in other tabs

* refactored to make use useEffect

* cleaned up comments

* fix remove event listener

* moved remove leftover storage variable to beginning of usemount

Co-authored-by: Michael Andersen <Michael.Andersen@rmp.uhn.ca>
This commit is contained in:
Michael Andersen 2022-10-06 22:40:16 -04:00 committed by GitHub
parent e401358887
commit 1d47f31738
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 119 additions and 46 deletions

View File

@ -148,6 +148,16 @@ function ViewerLayout({
},
];
if (appConfig.oidc) {
menuOptions.push({
title: t('Header:Logout'),
icon: 'power-off',
onClick: async () => {
navigate(`/logout?redirect_uri=${encodeURIComponent(window.location.href)}`);
}
});
}
/**
* Set body classes (tailwindcss) that don't allow vertical
* or horizontal overflow (no scrolling). Also guarantee window

View File

@ -4,5 +4,6 @@
"INVESTIGATIONAL USE ONLY": "INVESTIGATIONAL USE ONLY",
"Options": "Options",
"Preferences": "Preferences",
"Study list": "Study list"
}
"Study list": "Study list",
"Logout": "Logout"
}

View File

@ -0,0 +1,11 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 28"
aria-labelledby="title"
width="1em"
height="1em"
fill="currentColor"
>
<title id="title">Power Off</title>
<path d="M24 14c0 6.609-5.391 12-12 12s-12-5.391-12-12c0-3.797 1.75-7.297 4.797-9.578 0.891-0.672 2.141-0.5 2.797 0.391 0.672 0.875 0.484 2.141-0.391 2.797-2.031 1.531-3.203 3.859-3.203 6.391 0 4.406 3.594 8 8 8s8-3.594 8-8c0-2.531-1.172-4.859-3.203-6.391-0.875-0.656-1.062-1.922-0.391-2.797 0.656-0.891 1.922-1.062 2.797-0.391 3.047 2.281 4.797 5.781 4.797 9.578zM14 2v10c0 1.094-0.906 2-2 2s-2-0.906-2-2v-10c0-1.094 0.906-2 2-2s2 0.906 2 2z" />
</svg>

After

Width:  |  Height:  |  Size: 637 B

View File

@ -28,6 +28,7 @@ import logoOhifSmall from './../../assets/icons/logo-ohif-small.svg';
import magnifier from './../../assets/icons/magnifier.svg';
import notificationwarningDiamond from './../../assets/icons/notificationwarning-diamond.svg';
import pencil from './../../assets/icons/pencil.svg';
import powerOff from './../../assets/icons/power-off.svg';
import profile from './../../assets/icons/profile.svg';
import pushLeft from './../../assets/icons/push-left.svg';
import pushRight from './../../assets/icons/push-right.svg';
@ -97,6 +98,7 @@ const ICONS = {
exclamation: exclamation,
'notificationwarning-diamond': notificationwarningDiamond,
pencil: pencil,
'power-off': powerOff,
profile: profile,
'push-left': pushLeft,
'push-right': pushRight,

View File

@ -28,7 +28,6 @@ let commandsManager, extensionManager, servicesManager, hotkeysManager;
function App({ config, defaultExtensions, defaultModes }) {
const [init, setInit] = useState(null);
useEffect(() => {
const run = async () => {
appInit(config, defaultExtensions, defaultModes)

View File

@ -322,13 +322,13 @@ function WorkList({
seriesTableDataSource={
seriesInStudiesMap.has(studyInstanceUid)
? seriesInStudiesMap.get(studyInstanceUid).map(s => {
return {
description: s.description || '(empty)',
seriesNumber: s.seriesNumber || '',
modality: s.modality || '',
instances: s.numSeriesInstances || '',
};
})
return {
description: s.description || '(empty)',
seriesNumber: s.seriesNumber || '',
modality: s.modality || '',
instances: s.numSeriesInstances || '',
};
})
: []
}
>
@ -346,7 +346,7 @@ function WorkList({
<Link
key={i}
to={`${mode.routeName}?StudyInstanceUIDs=${studyInstanceUid}`}
// to={`${mode.routeName}/dicomweb?StudyInstanceUIDs=${studyInstanceUid}`}
// to={`${mode.routeName}/dicomweb?StudyInstanceUIDs=${studyInstanceUid}`}
>
<Button
rounded="full"
@ -354,7 +354,7 @@ function WorkList({
disabled={!isValidMode}
endIcon={<Icon name="launch-arrow" />} // launch-arrow | launch-info
className={classnames('font-bold', { 'ml-2': !isFirst })}
onClick={() => {}}
onClick={() => { }}
>
{t(`Modes:${mode.displayName}`)}
</Button>
@ -414,6 +414,17 @@ function WorkList({
},
];
if (appConfig.oidc) {
menuOptions.push({
icon: 'power-off',
title: t('Header:Logout'),
onClick: () => {
navigate(`/logout?redirect_uri=${encodeURIComponent(window.location.href)}`);
}
});
}
return (
<div
className={classnames('bg-black h-full', {

View File

@ -41,10 +41,7 @@ const initUserManager = (oidc, routerBasename) => {
const openIdConnectConfiguration = Object.assign({}, firstOpenIdClient, {
redirect_uri: _makeAbsoluteIfNecessary(redirect_uri, baseUri),
silent_redirect_uri: _makeAbsoluteIfNecessary(
silent_redirect_uri,
baseUri
),
silent_redirect_uri: _makeAbsoluteIfNecessary(silent_redirect_uri, baseUri),
post_logout_redirect_uri: _makeAbsoluteIfNecessary(
post_logout_redirect_uri,
baseUri
@ -52,6 +49,17 @@ const initUserManager = (oidc, routerBasename) => {
});
return getUserManagerForOpenIdConnectClient(openIdConnectConfiguration);
};
function LogoutComponent(props) {
const { userManager } = props;
localStorage.setItem('signoutEvent', 'true');
const location = useLocation();
const query = new URLSearchParams(location.search);
userManager.signoutRedirect({
post_logout_redirect_uri: query.get('redirect_uri'),
});
return null;
}
function LoginComponent(userManager) {
@ -60,9 +68,7 @@ function LoginComponent(userManager) {
const loginHint = queryParams.get('login_hint');
const targetLinkUri = queryParams.get('target_link_uri');
if (iss !== oidcAuthority) {
console.error(
'iss of /login does not match the oidc authority'
);
console.error('iss of /login does not match the oidc authority');
return null;
}
@ -96,22 +102,22 @@ function LoginComponent(userManager) {
}
function OpenIdConnectRoutes({
oidc,
routerBasename,
UserAuthenticationService
}) {
oidc,
routerBasename,
UserAuthenticationService,
}) {
const userManager = initUserManager(oidc, routerBasename);
const getAuthorizationHeader = () => {
const user = UserAuthenticationService.getUser();
return {
Authorization: `Bearer ${user.access_token}`
Authorization: `Bearer ${user.access_token}`,
};
}
};
const handleUnauthenticated = () => {
userManager.signinRedirect()
userManager.signinRedirect();
// return null because this is used in a react component
return null;
@ -119,23 +125,45 @@ function OpenIdConnectRoutes({
const navigate = useNavigate();
//for multi-tab logout
useEffect(() => {
localStorage.removeItem('signoutEvent');
const storageEventListener = event => {
const signOutEvent = localStorage.getItem('signoutEvent');
if (signOutEvent) {
navigate(
`/logout?redirect_uri=${encodeURIComponent(window.location.href)}`
);
}
};
window.addEventListener('storage', storageEventListener);
return () => {
window.removeEventListener('storage', storageEventListener);
};
}, []);
useEffect(() => {
UserAuthenticationService.set({ enabled: true });
UserAuthenticationService.setServiceImplementation({
getAuthorizationHeader,
handleUnauthenticated
handleUnauthenticated,
});
}, [])
}, []);
const oidcAuthority = oidc[0].authority;
const location = useLocation();
const { pathname, search } = location;
const redirect_uri = new URL(userManager.settings._redirect_uri).pathname//.replace(routerBasename,'')
const silent_refresh_uri = new URL(userManager.settings._silent_redirect_uri).pathname//.replace(routerBasename,'')
const post_logout_redirect_uri = new URL(userManager.settings._post_logout_redirect_uri).pathname//.replace(routerBasename,'');
const redirect_uri = new URL(userManager.settings._redirect_uri).pathname; //.replace(routerBasename,'')
const silent_refresh_uri = new URL(userManager.settings._silent_redirect_uri)
.pathname; //.replace(routerBasename,'')
const post_logout_redirect_uri = new URL(
userManager.settings._post_logout_redirect_uri
).pathname; //.replace(routerBasename,'');
// const pathnameRelative = pathname.replace(routerBasename,'');
@ -148,10 +176,7 @@ function OpenIdConnectRoutes({
return (
<Routes basename={routerBasename}>
<Route
path={silent_refresh_uri}
onEnter={window.location.reload}
/>
<Route path={silent_refresh_uri} onEnter={window.location.reload} />
<Route
path={post_logout_redirect_uri}
element={
@ -167,22 +192,36 @@ function OpenIdConnectRoutes({
/>
<Route
path={redirect_uri}
element={<CallbackPage userManager={userManager} onRedirectSuccess={(user) => {
const { pathname, search = '' } = JSON.parse(
sessionStorage.getItem('ohif-redirect-to')
);
element={
<CallbackPage
userManager={userManager}
onRedirectSuccess={user => {
const { pathname, search = '' } = JSON.parse(
sessionStorage.getItem('ohif-redirect-to')
);
UserAuthenticationService.setUser(user);
UserAuthenticationService.setUser(user);
navigate({
pathname,
search
})
}}/>}
navigate({
pathname,
search,
});
}}
/>
}
/>
<Route
path="/login"
element={<LoginComponent userManager={userManager} oidcAuthority={oidcAuthority}/>}
element={
<LoginComponent
userManager={userManager}
oidcAuthority={oidcAuthority}
/>
}
/>
<Route
path="/logout"
element={<LogoutComponent userManager={userManager} />}
/>
</Routes>
);