Skip to content

Commit 894035d

Browse files
authored
End keycloak session if user is not authorized (#334)
1 parent 62f49c1 commit 894035d

File tree

6 files changed

+111
-41
lines changed

6 files changed

+111
-41
lines changed

src/main/java/org/mskcc/oncokb/curation/security/CustomOAuthSuccessHandler.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
7777
SecurityContextHolder.getContext().setAuthentication(authenticationWithAuthorities);
7878
super.onAuthenticationSuccess(request, response, authenticationWithAuthorities);
7979
} else {
80-
request.getSession().invalidate();
81-
SecurityContextHolder.clearContext();
82-
redirectStrategy.sendRedirect(request, response, "/logout");
80+
redirectStrategy.sendRedirect(request, response, "/logout?unauthorized=true");
8381
}
8482

8583
clearAuthenticationAttributes(request);

src/main/java/org/mskcc/oncokb/curation/security/SecurityUtils.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
import org.springframework.security.core.context.SecurityContext;
1010
import org.springframework.security.core.context.SecurityContextHolder;
1111
import org.springframework.security.core.userdetails.UserDetails;
12+
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
13+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
1214
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
15+
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
1316
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
1417
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
1518

@@ -117,4 +120,20 @@ private static Collection<String> getRolesFromClaims(Map<String, Object> claims)
117120
private static List<GrantedAuthority> mapRolesToGrantedAuthorities(Collection<String> roles) {
118121
return roles.stream().filter(role -> role.startsWith("ROLE_")).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
119122
}
123+
124+
public static String getKeycloakLogoutURL(ClientRegistration clientRegistration) {
125+
StringBuilder logoutUrl = new StringBuilder();
126+
127+
// Get keycloak logout endpoint (host/auth/realms/<my_realm>/protocol/openid-connect/logout)
128+
logoutUrl.append(clientRegistration.getProviderDetails().getConfigurationMetadata().get("end_session_endpoint").toString());
129+
130+
// Get id token
131+
OAuth2AuthenticationToken auth = (OAuth2AuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
132+
OidcUser oAuth2User = (OidcUser) auth.getPrincipal();
133+
String idTokenHint = oAuth2User.getIdToken().getTokenValue();
134+
135+
logoutUrl.append("?id_token_hint=").append(idTokenHint);
136+
137+
return logoutUrl.toString();
138+
}
120139
}

src/main/java/org/mskcc/oncokb/curation/web/rest/LogoutResource.java

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.Map;
44
import javax.servlet.http.HttpServletRequest;
5+
import org.mskcc.oncokb.curation.security.SecurityUtils;
56
import org.springframework.http.HttpHeaders;
67
import org.springframework.http.ResponseEntity;
78
import org.springframework.security.core.context.SecurityContextHolder;
@@ -33,22 +34,9 @@ public LogoutResource(ClientRegistrationRepository registrations) {
3334
*/
3435
@PostMapping("/api/logout")
3536
public ResponseEntity<?> logout(HttpServletRequest request) {
36-
StringBuilder logoutUrl = new StringBuilder();
37-
38-
// Get keycloak logout endpoint (host/auth/realms/<my_realm>/protocol/openid-connect/logout)
39-
logoutUrl.append(this.registration.getProviderDetails().getConfigurationMetadata().get("end_session_endpoint").toString());
40-
41-
String originUrl = request.getHeader(HttpHeaders.ORIGIN);
42-
43-
OAuth2AuthenticationToken auth = (OAuth2AuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
44-
OidcUser oAuth2User = (OidcUser) auth.getPrincipal();
45-
String idTokenHint = oAuth2User.getIdToken().getTokenValue();
46-
47-
// After logout redirect to home page
48-
logoutUrl.append("?post_logout_redirect_uri=").append(originUrl);
49-
logoutUrl.append("&id_token_hint=").append(idTokenHint);
50-
37+
String logoutUrl = SecurityUtils.getKeycloakLogoutURL(this.registration);
5138
request.getSession().invalidate();
52-
return ResponseEntity.ok().body(Map.of("logoutUrl", logoutUrl.toString()));
39+
SecurityContextHolder.clearContext();
40+
return ResponseEntity.ok().body(Map.of("logoutUrl", logoutUrl));
5341
}
5442
}

src/main/webapp/app/config/constants/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,3 +420,7 @@ export const DEFAULT_TOAST_ERROR_OPTIONS: ToastOptions = {
420420
draggable: false,
421421
closeOnClick: false,
422422
};
423+
424+
export const KEYCLOAK_LOGOUT_REDIRECT_PARAM = 'post_logout_redirect_uri';
425+
export const KEYCLOAK_SESSION_TERMINATED_PARAM = 'session_terminated';
426+
export const KEYCLOAK_UNAUTHORIZED_PARAM = 'unauthorized';
Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,92 @@
1-
import React from 'react';
2-
import { componentInject } from 'app/shared/util/typed-inject';
1+
import React, { useEffect } from 'react';
2+
import { connect } from 'app/shared/util/typed-inject';
33
import { IRootStore } from 'app/stores';
4-
import { observer } from 'mobx-react';
5-
import { Row } from 'reactstrap';
4+
import { Link, RouteComponentProps } from 'react-router-dom';
5+
import {
6+
KEYCLOAK_LOGOUT_REDIRECT_PARAM,
7+
KEYCLOAK_SESSION_TERMINATED_PARAM,
8+
KEYCLOAK_UNAUTHORIZED_PARAM,
9+
PAGE_ROUTE,
10+
} from 'app/config/constants/constants';
11+
import { setUrlParams } from 'app/shared/util/url-utils';
612

7-
export interface ILogoutProps extends StoreProps {
13+
function getKeycloakLogoutUrl(baseLogoutUrl: string) {
14+
const redirectUrl = setUrlParams(window.location.href, { [KEYCLOAK_SESSION_TERMINATED_PARAM]: true });
15+
// Encoding URI will allow us to include multiple search params in redirect uri param
16+
const keyCloakLogoutUrl = setUrlParams(baseLogoutUrl, { [KEYCLOAK_LOGOUT_REDIRECT_PARAM]: redirectUrl });
17+
return keyCloakLogoutUrl;
18+
}
19+
20+
export interface ILogoutProps extends StoreProps, RouteComponentProps {
821
logoutUrl: string;
922
}
1023

11-
class Logout extends React.Component<ILogoutProps> {
12-
constructor(props: ILogoutProps) {
13-
super(props);
14-
}
24+
export const Logout = (props: ILogoutProps) => {
25+
const searchParams = new URLSearchParams(props.location.search);
1526

16-
componentDidMount() {
17-
this.props.logout();
18-
}
27+
const sessionTerminated = !!searchParams.get(KEYCLOAK_SESSION_TERMINATED_PARAM);
28+
const unauthorizedAccess = !!searchParams.get(KEYCLOAK_UNAUTHORIZED_PARAM);
29+
30+
useEffect(() => {
31+
if (unauthorizedAccess && !sessionTerminated) {
32+
props.logout();
33+
} else if (!sessionTerminated) {
34+
props.logout();
35+
}
36+
}, [unauthorizedAccess, sessionTerminated]);
1937

20-
render() {
21-
if (this.props.logoutUrl) {
22-
window.location.href = this.props.logoutUrl;
38+
useEffect(() => {
39+
if (props.logoutUrl) {
40+
if (!sessionTerminated || unauthorizedAccess) {
41+
window.location.href = getKeycloakLogoutUrl(props.logoutUrl);
42+
}
2343
}
44+
}, [props.logoutUrl, sessionTerminated, unauthorizedAccess]);
2445

25-
return (
26-
<Row className="justify-content-center">
27-
<h4>Logged out.</h4>
28-
</Row>
29-
);
46+
if (!sessionTerminated) {
47+
return <></>;
3048
}
31-
}
49+
50+
if (unauthorizedAccess) {
51+
return <></>;
52+
}
53+
54+
const unauthorizedAccessContent = (
55+
<>
56+
<h3 className="card-title mb-4">Logged out!</h3>
57+
<div className="card-text alert alert-danger" role="alert">
58+
You are not authorized to access this website. If you think this is an error, please reach out to OncoKB Dev team.
59+
</div>
60+
</>
61+
);
62+
63+
const loggedOutContent = (
64+
<>
65+
<h3 className="card-title mb-4">Logged out successfully!</h3>
66+
</>
67+
);
68+
69+
return (
70+
<div className="d-flex justify-content-center">
71+
<div className="card" style={{ width: '40rem' }}>
72+
<div className="card-body">
73+
{unauthorizedAccess ? unauthorizedAccessContent : loggedOutContent}
74+
<p className="card-text">Please click on &apos;Sign in&apos; to be directed to the login page.</p>
75+
<Link to={PAGE_ROUTE.LOGIN} className="btn btn-primary">
76+
Sign in
77+
</Link>
78+
</div>
79+
</div>
80+
</div>
81+
);
82+
};
3283

3384
const mapStoreToProps = (storeState: IRootStore) => ({
85+
isAuthenticated: storeState.authStore.isAuthenticated,
3486
logoutUrl: storeState.authStore.logoutUrl,
3587
logout: storeState.authStore.logout,
3688
});
3789

3890
type StoreProps = ReturnType<typeof mapStoreToProps>;
3991

40-
export default componentInject(mapStoreToProps)(observer(Logout));
92+
export default connect(mapStoreToProps)(Logout);

src/main/webapp/app/shared/util/url-utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,12 @@ export const replaceUrlParams = (url: string, ...params: (string | number)[]) =>
2323

2424
return url;
2525
};
26+
27+
export const setUrlParams = (urlString: string, params: Record<string, any>) => {
28+
const url = new URL(urlString);
29+
for (const [key, value] of Object.entries(params)) {
30+
// Set will override the param if it already exists in the URL
31+
url.searchParams.set(key, value);
32+
}
33+
return url.toString();
34+
};

0 commit comments

Comments
 (0)