diff --git a/src/modules/rest/api/src/main/java/it/geosolutions/geostore/services/rest/SessionServiceDelegate.java b/src/modules/rest/api/src/main/java/it/geosolutions/geostore/services/rest/SessionServiceDelegate.java index 36517990..de622b22 100644 --- a/src/modules/rest/api/src/main/java/it/geosolutions/geostore/services/rest/SessionServiceDelegate.java +++ b/src/modules/rest/api/src/main/java/it/geosolutions/geostore/services/rest/SessionServiceDelegate.java @@ -43,7 +43,7 @@ public interface SessionServiceDelegate { /** * Do the logout. * - * @param accessToken the current session token. + * @param sessionId the current session token. */ - void doLogout(String accessToken); + void doLogout(String sessionId); } diff --git a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/impl/SessionServiceDelegateImpl.java b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/impl/SessionServiceDelegateImpl.java index a7a13e95..471a5de4 100644 --- a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/impl/SessionServiceDelegateImpl.java +++ b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/impl/SessionServiceDelegateImpl.java @@ -38,8 +38,8 @@ public SessionToken refresh(String refreshToken, String accessToken) { } @Override - public void doLogout(String accessToken) { - userSessionService.removeSession(accessToken); + public void doLogout(String sessionId) { + userSessionService.removeSession(sessionId); } /** diff --git a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/IdPConfiguration.java b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/IdPConfiguration.java index 6c6238d4..f7e76dd9 100644 --- a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/IdPConfiguration.java +++ b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/IdPConfiguration.java @@ -39,7 +39,7 @@ public void setEnabled(boolean enabled) { } /** - * @return true if the logged in user should be created on the db if not present. False + * @return true if the logged-in user should be created on the db if not present. False * otherwise. */ public boolean isAutoCreateUser() { diff --git a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/TokenAuthenticationCache.java b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/TokenAuthenticationCache.java index a600adbe..f82d0ea3 100644 --- a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/TokenAuthenticationCache.java +++ b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/TokenAuthenticationCache.java @@ -89,21 +89,24 @@ protected void revokeAuthIfRefreshExpired(Authentication authentication) { ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken; OAuth2Configuration configuration = (OAuth2Configuration) context.getBean(tokenDetails.getProvider()); - if (expiring.getExpiration().after(new Date())) { - OAuth2Configuration.Endpoint revokeEndpoint = - configuration.buildRevokeEndpoint(expiring.getValue()); - if (revokeEndpoint != null) { - RestTemplate template = new RestTemplate(); - ResponseEntity responseEntity = - template.exchange( - revokeEndpoint.getUrl(), - revokeEndpoint.getMethod(), - null, - String.class); - if (responseEntity.getStatusCode().value() != 200) { - LOGGER.error( - "Error while revoking authorization. Error is: " - + responseEntity.getBody()); + if (configuration != null && configuration.isEnabled()) { + if (expiring.getExpiration().after(new Date())) { + OAuth2Configuration.Endpoint revokeEndpoint = + configuration.buildRevokeEndpoint( + expiring.getValue(), accessToken.getValue(), configuration); + if (revokeEndpoint != null) { + RestTemplate template = new RestTemplate(); + ResponseEntity responseEntity = + template.exchange( + revokeEndpoint.getUrl(), + revokeEndpoint.getMethod(), + null, + String.class); + if (responseEntity.getStatusCode().value() != 200) { + LOGGER.error( + "Error while revoking authorization. Error is: {}", + responseEntity.getBody()); + } } } } @@ -123,7 +126,7 @@ public Authentication get(String accessToken) { /** * Put an Authentication instance identified by an accessToken value. If the passed - * Authentication instance those not have a refresh token and we have an old one that has, the + * Authentication instance does not have a refresh token, and we have an old one that has, the * refresh Token is set to the new instance. * * @param accessToken the access token identifying the instance to update diff --git a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/keycloak/KeycloakSessionServiceDelegate.java b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/keycloak/KeycloakSessionServiceDelegate.java index e3137046..bb3582bc 100644 --- a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/keycloak/KeycloakSessionServiceDelegate.java +++ b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/keycloak/KeycloakSessionServiceDelegate.java @@ -135,7 +135,7 @@ private SessionToken sessionToken(String accessToken, String refreshToken, Date } @Override - public void doLogout(String accessToken) { + public void doLogout(String sessionId) { HttpServletRequest request = OAuth2Utils.getRequest(); HttpServletResponse response = OAuth2Utils.getResponse(); KeyCloakHelper helper = GeoStoreContext.bean(KeyCloakHelper.class); @@ -167,7 +167,7 @@ public void doLogout(String accessToken) { AdapterTokenStore tokenStore = factory.createAdapterTokenStore(deployment, getRequest(), getResponse()); if (tokenStore != null) tokenStore.logout(); - internalLogout(accessToken, request, response); + internalLogout(sessionId, request, response); } private void internalLogout( diff --git a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/DiscoveryClient.java b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/DiscoveryClient.java index 5d42e1cf..93989edd 100644 --- a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/DiscoveryClient.java +++ b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/DiscoveryClient.java @@ -92,6 +92,7 @@ private void setLocation(String location) { public void autofill(OAuth2Configuration conf) { if (location != null) { Map response = restTemplate.getForObject(this.location, Map.class); + assert response != null; Optional.ofNullable(response.get(getAuthorizationEndpointAttrName())) .ifPresent(uri -> conf.setAuthorizationUri((String) uri)); Optional.ofNullable(response.get(getTokenEndpointAttrName())) diff --git a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/OAuth2Configuration.java b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/OAuth2Configuration.java index 2ecd5490..aeee55fa 100644 --- a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/OAuth2Configuration.java +++ b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/OAuth2Configuration.java @@ -32,7 +32,10 @@ import java.util.Collections; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -55,6 +58,7 @@ public class OAuth2Configuration extends IdPConfiguration { protected String authorizationUri; protected String checkTokenEndpointUrl; protected String logoutUri; + protected boolean globalLogoutEnabled = false; protected String scopes; protected String idTokenUri; protected String discoveryUrl; @@ -119,7 +123,7 @@ public String buildLoginUri(String accessType, String... additionalScopes) { if (accessType != null) loginUri.append("&").append("access_type=").append(accessType); String finalUrl = loginUri.toString(); if (LOGGER.isDebugEnabled()) - LOGGER.info("Going to request authorization to this endpoint " + finalUrl); + LOGGER.info("Going to request authorization to this endpoint {}", finalUrl); return finalUrl; } @@ -236,6 +240,20 @@ public void setLogoutUri(String logoutUri) { this.logoutUri = logoutUri; } + /** @return */ + public boolean isGlobalLogoutEnabled() { + return globalLogoutEnabled; + } + + /** + * Set th + * + * @param globalLogoutEnabled + */ + public void setGlobalLogoutEnabled(boolean globalLogoutEnabled) { + this.globalLogoutEnabled = globalLogoutEnabled; + } + /** * Get the configured scopes as a String. * @@ -331,38 +349,85 @@ protected String appendParameters(MultiValueMap params, String u return builder.build().toUriString(); } + protected static void getLogoutRequestParams( + String token, String clientId, MultiValueMap params) { + params.put("token", Collections.singletonList(token)); + if (clientId != null && !clientId.isEmpty()) { + params.put("client_id", Collections.singletonList(clientId)); + } + } + /** * Build the revoke endpoint. * * @param token the access_token to revoke. * @return the revoke endpoint. */ - public Endpoint buildRevokeEndpoint(String token) { + public Endpoint buildRevokeEndpoint( + String token, String accessToken, OAuth2Configuration configuration) { Endpoint result = null; if (revokeEndpoint != null) { - MultiValueMap params = new LinkedMultiValueMap<>(); - params.put("token", Collections.singletonList(token)); - result = new Endpoint(HttpMethod.POST, appendParameters(params, revokeEndpoint)); + HttpHeaders headers = getHttpHeaders(accessToken, configuration); + + MultiValueMap bodyParams = new LinkedMultiValueMap<>(); + bodyParams.add("token", token); + bodyParams.add("client_id", clientId); + + HttpEntity> requestEntity = + new HttpEntity<>(bodyParams, headers); + + result = new Endpoint(HttpMethod.POST, revokeEndpoint); + result.setRequestEntity(requestEntity); } return result; } + private static HttpHeaders getHttpHeaders( + String accessToken, OAuth2Configuration configuration) { + HttpHeaders headers = getHeaders(accessToken, configuration); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + return headers; + } + /** * Build the logout endpoint. * * @param token the current access_token. * @return the logout endpoint. */ - public Endpoint buildLogoutEndpoint(String token) { + public Endpoint buildLogoutEndpoint( + String token, String accessToken, OAuth2Configuration configuration) { Endpoint result = null; if (logoutUri != null) { + HttpHeaders headers = getHeaders(accessToken, configuration); + MultiValueMap params = new LinkedMultiValueMap<>(); - params.put("token", Collections.singletonList(token)); + getLogoutRequestParams(token, clientId, params); + + HttpEntity> requestEntity = + new HttpEntity<>(null, headers); + result = new Endpoint(HttpMethod.GET, appendParameters(params, logoutUri)); + result.setRequestEntity(requestEntity); } return result; } + private static HttpHeaders getHeaders(String accessToken, OAuth2Configuration configuration) { + HttpHeaders headers = new HttpHeaders(); + if (configuration != null + && configuration.clientId != null + && configuration.clientSecret != null) + headers.setBasicAuth( + configuration.clientId, + configuration + .clientSecret); // Set client ID and client secret for authentication + else if (accessToken != null) { + headers.set("Authorization", "Bearer " + accessToken); + } + return headers; + } + /** @return true if redirect to authorization is active always. False otherwise. */ public boolean isEnableRedirectEntryPoint() { return enableRedirectEntryPoint; @@ -383,7 +448,7 @@ public void setEnableRedirectEntryPoint(boolean enableRedirectEntryPoint) { * @return the principal key. */ public String getPrincipalKey() { - if (principalKey == null || "".equals(principalKey)) return "email"; + if (principalKey == null || principalKey.isEmpty()) return "email"; return principalKey; } @@ -428,13 +493,15 @@ public void setGroupsClaim(String groupsClaim) { this.groupsClaim = groupsClaim; } - /** Class the represent and endpoint with a HTTP method. */ + /** Class the representing and endpoint with a HTTP method. */ public static class Endpoint { private String url; private HttpMethod method; + private HttpEntity requestEntity; + public Endpoint(HttpMethod method, String url) { this.method = method; this.url = url; @@ -467,5 +534,15 @@ public HttpMethod getMethod() { public void setMethod(HttpMethod method) { this.method = method; } + + /** @return */ + public HttpEntity getRequestEntity() { + return requestEntity; + } + + /** @param requestEntity */ + public void setRequestEntity(HttpEntity requestEntity) { + this.requestEntity = requestEntity; + } } } diff --git a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/OAuth2SessionServiceDelegate.java b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/OAuth2SessionServiceDelegate.java index 2a6629fa..2e83f821 100644 --- a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/OAuth2SessionServiceDelegate.java +++ b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/OAuth2SessionServiceDelegate.java @@ -39,19 +39,15 @@ import it.geosolutions.geostore.services.rest.security.TokenAuthenticationCache; import it.geosolutions.geostore.services.rest.utils.GeoStoreContext; import java.io.IOException; -import java.util.Arrays; import java.util.Date; +import java.util.Map; +import java.util.Optional; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.http.client.ClientHttpRequest; -import org.springframework.http.converter.FormHttpMessageConverter; +import org.springframework.http.*; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.OAuth2ClientContext; @@ -64,8 +60,8 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.HttpMessageConverterExtractor; -import org.springframework.web.client.RequestCallback; import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.request.RequestContextHolder; /** Abstract implementation of an OAuth2 SessionServiceDelegate. */ public abstract class OAuth2SessionServiceDelegate implements SessionServiceDelegate { @@ -76,7 +72,7 @@ public abstract class OAuth2SessionServiceDelegate implements SessionServiceDele /** * @param restSessionService the session service to which register this delegate. - * @param delegateName this delegate name eg. google or github etc... + * @param delegateName this delegate name eg. google or GitHub etc... */ public OAuth2SessionServiceDelegate( RESTSessionService restSessionService, String delegateName, UserService userService) { @@ -98,12 +94,21 @@ public SessionToken refresh(String refreshToken, String accessToken) { Date fiveMinutesFromNow = fiveMinutesFromNow(); SessionToken sessionToken = null; OAuth2Configuration configuration = configuration(); - if ((expiresIn == null || fiveMinutesFromNow.after(expiresIn)) && refreshToken != null) { - if (LOGGER.isDebugEnabled()) LOGGER.info("Going to refresh the token."); - sessionToken = doRefresh(refreshToken, accessToken, configuration); + if (configuration != null && configuration.isEnabled()) { + if ((expiresIn == null || fiveMinutesFromNow.after(expiresIn)) + && refreshToken != null) { + if (LOGGER.isDebugEnabled()) LOGGER.info("Going to refresh the token."); + try { + sessionToken = doRefresh(refreshToken, accessToken, configuration); + if (sessionToken == null) + sessionToken = + sessionToken( + accessToken, refreshToken, currentToken.getExpiration()); + } catch (NullPointerException npe) { + LOGGER.error("Current configuration wasn't correctly initialized."); + } + } } - if (sessionToken == null) - sessionToken = sessionToken(accessToken, refreshToken, currentToken.getExpiration()); return sessionToken; } @@ -118,30 +123,48 @@ public SessionToken refresh(String refreshToken, String accessToken) { protected SessionToken doRefresh( String refreshToken, String accessToken, OAuth2Configuration configuration) { SessionToken sessionToken = null; - MultiValueMap form = new LinkedMultiValueMap<>(); - form.add("grant_type", "refresh_token"); - form.add("refresh_token", refreshToken); - form.add("client_secret", configuration.getClientSecret()); + RestTemplate restTemplate = new RestTemplate(); - HttpHeaders headers = new HttpHeaders(); - OAuth2AccessToken newToken = - restTemplate.execute( - configuration.buildRefreshTokenURI(), - HttpMethod.POST, - new RefreshTokenRequestCallback(form, headers), - tokenExtractor()); + HttpHeaders headers = getHttpHeaders(accessToken, configuration); + + MultiValueMap requestBody = new LinkedMultiValueMap<>(); + requestBody.add("grant_type", "refresh_token"); + requestBody.add("refresh_token", refreshToken); + requestBody.add("client_secret", configuration.getClientSecret()); + + HttpEntity> requestEntity = + new HttpEntity<>(requestBody, headers); + + OAuth2AccessToken newToken = null; + try { + newToken = + restTemplate + .exchange( + configuration + .buildRefreshTokenURI(), // Use exchange method for POST + // request + HttpMethod.POST, + requestEntity, // Include request body + OAuth2AccessToken.class) + .getBody(); + } catch (Exception ex) { + LOGGER.error("Error trying to obtain a refresh token.", ex); + } + if (newToken != null && newToken.getValue() != null) { - String refreshed = newToken.getValue(); // update the Authentication updateAuthToken(accessToken, newToken, refreshToken, configuration); - sessionToken = sessionToken(refreshed, refreshToken, newToken.getExpiration()); + sessionToken = + sessionToken(newToken.getValue(), refreshToken, newToken.getExpiration()); + } else if (accessToken != null) { + // update the Authentication + sessionToken = sessionToken(accessToken, refreshToken, null); } else { - // the refresh token was invalid. lets clear the session and send a remote logout. - // then redirects to the login entry point. + // the refresh token was invalid. let's clear the session and send a remote logout. + // then redirect to the login entry point. LOGGER.info( - "Unable to refresh the token. The following request was performed: " - + configuration.buildRefreshTokenURI("offline") - + ". Redirecting to login."); + "Unable to refresh the token. The following request was performed: {}. Redirecting to login.", + configuration.buildRefreshTokenURI("offline")); doLogout(null); try { getResponse() @@ -157,9 +180,26 @@ protected SessionToken doRefresh( return sessionToken; } + private static HttpHeaders getHttpHeaders( + String accessToken, OAuth2Configuration configuration) { + HttpHeaders headers = new HttpHeaders(); + if (configuration != null + && configuration.clientId != null + && configuration.clientSecret != null) + headers.setBasicAuth( + configuration.clientId, + configuration + .clientSecret); // Set client ID and client secret for authentication + else if (accessToken != null) { + headers.set("Authorization", "Bearer " + accessToken); + } + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); // Set content type + return headers; + } + private SessionToken sessionToken(String accessToken, String refreshToken, Date expires) { SessionToken sessionToken = new SessionToken(); - sessionToken.setExpires(Long.valueOf(expires.getTime())); + if (expires != null) sessionToken.setExpires(expires.getTime()); sessionToken.setAccessToken(accessToken); sessionToken.setRefreshToken(refreshToken); sessionToken.setTokenType("bearer"); @@ -215,40 +255,85 @@ private OAuth2AccessToken retrieveAccessToken(String accessToken) { result = details.getAccessToken(); } if (result == null) { - OAuth2ClientContext context = - GeoStoreContext.bean(OAuth2RestTemplate.class).getOAuth2ClientContext(); - if (context != null) result = context.getAccessToken(); + OAuth2RestTemplate oAuth2RestTemplate = restTemplate(); + if (oAuth2RestTemplate != null) { + OAuth2ClientContext context = oAuth2RestTemplate.getOAuth2ClientContext(); + if (context != null) result = context.getAccessToken(); + } } if (result == null) result = new DefaultOAuth2AccessToken(accessToken); return result; } @Override - public void doLogout(String accessToken) { + public void doLogout(String sessionId) { HttpServletRequest request = getRequest(); HttpServletResponse response = getResponse(); OAuth2RestTemplate restTemplate = restTemplate(); - if (accessToken == null) - accessToken = OAuth2Utils.getParameterValue(ACCESS_TOKEN_PARAM, request); - TokenAuthenticationCache cache = cache(); - Authentication authentication = cache.get(accessToken); - OAuth2AccessToken token = null; - TokenDetails tokenDetails = getTokenDetails(authentication); - if (tokenDetails != null) token = tokenDetails.getAccessToken(); - cache.removeEntry(accessToken); - if (token == null) token = restTemplate.getOAuth2ClientContext().getAccessToken(); - if (token != null) { - OAuth2Configuration configuration = configuration(); - doLogoutInternal(token, configuration); - clearSession(restTemplate, request); - } else { - if (LOGGER.isDebugEnabled()) - LOGGER.info("Unable to retrieve access token. Remote logout was not executed."); + + String token = null; + String accessToken = null; + if (sessionId != null) { + TokenAuthenticationCache cache = cache(); + Authentication authentication = cache.get(sessionId); + TokenDetails tokenDetails = getTokenDetails(authentication); + if (tokenDetails != null) { + token = tokenDetails.getIdToken(); + accessToken = tokenDetails.getAccessToken().getValue(); + } + cache.removeEntry(sessionId); + } + + if (token == null) { + if (restTemplate.getOAuth2ClientContext().getAccessToken() != null) { + token = + restTemplate + .getOAuth2ClientContext() + .getAccessToken() + .getRefreshToken() + .getValue(); + } + if (token == null) { + token = OAuth2Utils.getParameterValue(REFRESH_TOKEN_PARAM, request); + } + if (token == null) { + token = + (String) + RequestContextHolder.getRequestAttributes() + .getAttribute(REFRESH_TOKEN_PARAM, 0); + } + } + + if (accessToken == null) { + if (restTemplate.getOAuth2ClientContext().getAccessToken() != null) { + accessToken = restTemplate.getOAuth2ClientContext().getAccessToken().getValue(); + } + if (accessToken == null) { + accessToken = OAuth2Utils.getParameterValue(ACCESS_TOKEN_PARAM, request); + } + if (accessToken == null) { + accessToken = + (String) + RequestContextHolder.getRequestAttributes() + .getAttribute(ACCESS_TOKEN_PARAM, 0); + } + } + + OAuth2Configuration configuration = configuration(); + if (configuration != null && configuration.isEnabled()) { + if (token != null && accessToken != null) { + if (configuration.isGlobalLogoutEnabled()) + doLogoutInternal(token, configuration, accessToken); + if (configuration.getRevokeEndpoint() != null) clearSession(restTemplate, request); + } else { + if (LOGGER.isDebugEnabled()) + LOGGER.info("Unable to retrieve access token. Remote logout was not executed."); + } + if (response != null) clearCookies(request, response); } - if (request != null && response != null) clearCookies(request, response); } - // clears any state Spring OAuth2 object might preserve. + // clears any state a Spring OAuth2 object might preserve. private void clearSession(OAuth2RestTemplate restTemplate, HttpServletRequest request) { final AccessTokenRequest accessTokenRequest = restTemplate.getOAuth2ClientContext().getAccessTokenRequest(); @@ -274,52 +359,64 @@ private void clearSession(OAuth2RestTemplate restTemplate, HttpServletRequest re * @param token the access token. * @param configuration the OAuth2Configuration */ - protected void doLogoutInternal(OAuth2AccessToken token, OAuth2Configuration configuration) { - String tokenValue = - token.getRefreshToken() != null - ? token.getRefreshToken().getValue() - : token.getValue(); + protected void doLogoutInternal( + Object token, OAuth2Configuration configuration, String accessToken) { + String tokenValue = null; + if (token instanceof OAuth2AccessToken) { + tokenValue = + ((OAuth2AccessToken) token).getRefreshToken() != null + ? ((OAuth2AccessToken) token).getRefreshToken().getValue() + : ((OAuth2AccessToken) token).getValue(); + } else if (token instanceof String) { + tokenValue = (String) token; + } if (configuration.getRevokeEndpoint() != null && tokenValue != null) { if (LOGGER.isDebugEnabled()) LOGGER.info("Performing remote logout"); - callRevokeEndpoint(tokenValue, configuration.getRevokeEndpoint()); - callRemoteLogout(token.getValue(), configuration.getLogoutUri()); + callRevokeEndpoint(tokenValue, accessToken); + callRemoteLogout(tokenValue, accessToken); } } - protected void callRevokeEndpoint(String token, String revokeEndpointUrl) { + protected void callRevokeEndpoint(String token, String accessToken) { OAuth2Configuration configuration = configuration(); - OAuth2Configuration.Endpoint revokeEndpoint = configuration.buildRevokeEndpoint(token); - if (revokeEndpoint != null) { - RestTemplate template = new RestTemplate(); - ResponseEntity responseEntity = - template.exchange( - revokeEndpoint.getUrl(), - revokeEndpoint.getMethod(), - null, - String.class); - if (responseEntity.getStatusCode().value() != 200) { - LOGGER.error( - "Error while revoking authorization. Error is: " - + responseEntity.getBody()); + if (configuration != null && configuration.isEnabled()) { + OAuth2Configuration.Endpoint revokeEndpoint = + configuration.buildRevokeEndpoint(token, accessToken, configuration); + if (revokeEndpoint != null) { + RestTemplate template = new RestTemplate(); + try { + ResponseEntity responseEntity = + template.exchange( + revokeEndpoint.getUrl(), + revokeEndpoint.getMethod(), + revokeEndpoint.getRequestEntity(), + String.class); + if (responseEntity.getStatusCode().value() != 200) { + logRevokeErrors(responseEntity.getBody()); + } + } catch (Exception e) { + logRevokeErrors(e); + } } } } - protected void callRemoteLogout(String token, String logoutUri) { + protected void callRemoteLogout(String token, String accessToken) { OAuth2Configuration configuration = configuration(); - OAuth2Configuration.Endpoint logoutEndpoint = configuration.buildLogoutEndpoint(token); - if (logoutEndpoint != null) { - RestTemplate template = new RestTemplate(); - ResponseEntity responseEntity = - template.exchange( - logoutEndpoint.getUrl(), - logoutEndpoint.getMethod(), - null, - String.class); - if (responseEntity.getStatusCode().value() != 200) { - LOGGER.error( - "Error while revoking authorization. Error is: " - + responseEntity.getBody()); + if (configuration != null && configuration.isEnabled()) { + OAuth2Configuration.Endpoint logoutEndpoint = + configuration.buildLogoutEndpoint(token, accessToken, configuration); + if (logoutEndpoint != null) { + RestTemplate template = new RestTemplate(); + ResponseEntity responseEntity = + template.exchange( + logoutEndpoint.getUrl(), + logoutEndpoint.getMethod(), + logoutEndpoint.getRequestEntity(), + String.class); + if (responseEntity.getStatusCode().value() != 200) { + logRevokeErrors(responseEntity.getBody()); + } } } } @@ -353,16 +450,20 @@ private TokenAuthenticationCache cache() { * * @return the OAuth2Configuration. */ - protected abstract OAuth2Configuration configuration(); - - /** - * Get an OAuth2Configuration by bean name. - * - * @param configBeanName the bean name. - * @return the config bean. - */ - protected OAuth2Configuration configuration(String configBeanName) { - return GeoStoreContext.bean(configBeanName, OAuth2Configuration.class); + protected OAuth2Configuration configuration() { + Map configurations = + GeoStoreContext.beans(OAuth2Configuration.class); + if (configurations != null) { + Optional enabledConfig = + configurations.values().stream() + .filter(OAuth2Configuration::isEnabled) + .findFirst(); + + if (enabledConfig.isPresent()) { + return enabledConfig.get(); + } + } + return null; } protected HttpMessageConverterExtractor tokenExtractor() { @@ -404,30 +505,7 @@ public String getUserName(String sessionId, boolean refresh, boolean autorefresh return null; } - /** A RequestCallback used when performing a refresh token request. */ - protected class RefreshTokenRequestCallback implements RequestCallback { - - private final MultiValueMap form; - - private final HttpHeaders headers; - - private RefreshTokenRequestCallback( - MultiValueMap form, HttpHeaders headers) { - this.form = form; - this.headers = headers; - } - - public void doWithRequest(ClientHttpRequest request) throws IOException { - request.getHeaders().putAll(this.headers); - request.getHeaders() - .setAccept( - Arrays.asList( - MediaType.APPLICATION_JSON, - MediaType.TEXT_XML, - MediaType.TEXT_PLAIN, - MediaType.APPLICATION_FORM_URLENCODED)); - new FormHttpMessageConverter() - .write(this.form, MediaType.APPLICATION_FORM_URLENCODED, request); - } + private static void logRevokeErrors(Object cause) { + LOGGER.error("Error while revoking authorization. Error is: {}", cause); } } diff --git a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/Oauth2LoginService.java b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/Oauth2LoginService.java index ba9c42e5..f610ce70 100644 --- a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/Oauth2LoginService.java +++ b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/Oauth2LoginService.java @@ -50,10 +50,10 @@ protected Response.ResponseBuilder getCallbackResponseBuilder( String token, String refreshToken, String provider) { Response.ResponseBuilder result = new ResponseBuilderImpl(); IdPConfiguration configuration = configuration(provider); - LOGGER.info("Callback Provider: " + provider); - LOGGER.debug("Token: " + token); - LOGGER.debug("Redirect uri: " + configuration.getRedirectUri()); - LOGGER.debug("Internal redirect uri: " + configuration.getInternalRedirectUri()); + LOGGER.info("Callback Provider: {}", provider); + LOGGER.debug("Token: {}", token); + LOGGER.debug("Redirect uri: {}", configuration.getRedirectUri()); + LOGGER.debug("Internal redirect uri: {}", configuration.getInternalRedirectUri()); if (token != null) { LOGGER.info("AccessToken found"); SessionToken sessionToken = new SessionToken(); @@ -61,12 +61,10 @@ protected Response.ResponseBuilder getCallbackResponseBuilder( result = result.status(302) .location(new URI(configuration.getInternalRedirectUri())); - if (token != null) { - LOGGER.debug("AccessToken: " + token); - sessionToken.setAccessToken(token); - } + LOGGER.debug("AccessToken: {}", token); + sessionToken.setAccessToken(token); if (refreshToken != null) { - LOGGER.debug("RefreshToken: " + refreshToken); + LOGGER.debug("RefreshToken: {}", refreshToken); sessionToken.setRefreshToken(refreshToken); } sessionToken.setTokenType(BEARER_TYPE); diff --git a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/google/GoogleSessionServiceDelegate.java b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/google/GoogleSessionServiceDelegate.java index 5bc17437..63e2e238 100644 --- a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/google/GoogleSessionServiceDelegate.java +++ b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/google/GoogleSessionServiceDelegate.java @@ -27,11 +27,8 @@ */ package it.geosolutions.geostore.services.rest.security.oauth2.google; -import static it.geosolutions.geostore.services.rest.security.oauth2.google.OAuthGoogleSecurityConfiguration.CONF_BEAN_NAME; - import it.geosolutions.geostore.services.UserService; import it.geosolutions.geostore.services.rest.RESTSessionService; -import it.geosolutions.geostore.services.rest.security.oauth2.OAuth2Configuration; import it.geosolutions.geostore.services.rest.security.oauth2.OAuth2SessionServiceDelegate; import it.geosolutions.geostore.services.rest.utils.GeoStoreContext; import org.springframework.security.oauth2.client.OAuth2RestTemplate; @@ -44,11 +41,6 @@ public GoogleSessionServiceDelegate( super(restSessionService, "google", userService); } - @Override - protected OAuth2Configuration configuration() { - return configuration(CONF_BEAN_NAME); - } - @Override protected OAuth2RestTemplate restTemplate() { return GeoStoreContext.bean("googleOpenIdRestTemplate", OAuth2RestTemplate.class); diff --git a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/openid_connect/OpenIdConnectConfiguration.java b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/openid_connect/OpenIdConnectConfiguration.java index cc531bfc..e86a4cd2 100644 --- a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/openid_connect/OpenIdConnectConfiguration.java +++ b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/openid_connect/OpenIdConnectConfiguration.java @@ -30,6 +30,8 @@ import it.geosolutions.geostore.services.rest.security.oauth2.OAuth2Configuration; import it.geosolutions.geostore.services.rest.security.oauth2.OAuth2Utils; import java.util.Collections; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -104,19 +106,30 @@ public void setUsePKCE(boolean usePKCE) { * @return the logout endpoint. */ @Override - public Endpoint buildLogoutEndpoint(String token) { + public Endpoint buildLogoutEndpoint( + String token, String accessToken, OAuth2Configuration configuration) { Endpoint result = null; String uri = getLogoutUri(); - String idToken = OAuth2Utils.getIdToken(); + String idToken = OAuth2Utils.getIdToken() != null ? OAuth2Utils.getIdToken() : accessToken; if (uri != null) { + HttpHeaders headers = new HttpHeaders(); + MultiValueMap params = new LinkedMultiValueMap<>(); - if (idToken != null) params.put("id_token_hint", Collections.singletonList(idToken)); + if (idToken != null) { + params.put("token_type_hint", Collections.singletonList("id_token")); + headers.set("Authorization", "Bearer " + idToken); + } if (StringUtils.hasText(getPostLogoutRedirectUri())) params.put( "post_logout_redirect_uri", Collections.singletonList(getPostLogoutRedirectUri())); - params.put("token", Collections.singletonList(token)); + getLogoutRequestParams(token, clientId, params); + + HttpEntity> requestEntity = + new HttpEntity<>(null, headers); + result = new Endpoint(HttpMethod.GET, appendParameters(params, uri)); + result.setRequestEntity(requestEntity); } return result; } diff --git a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/openid_connect/OpenIdConnectFilter.java b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/openid_connect/OpenIdConnectFilter.java index 2bbbd324..ed101082 100644 --- a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/openid_connect/OpenIdConnectFilter.java +++ b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/openid_connect/OpenIdConnectFilter.java @@ -91,7 +91,7 @@ protected String getPreAuthenticatedPrincipal( } // we must validate String token = null; - if (accessToken == null) { + if (accessToken != null) { token = accessToken.getValue(); } else { token = (String) req.getAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE); diff --git a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/openid_connect/OpenIdConnectLoginService.java b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/openid_connect/OpenIdConnectLoginService.java index d5965c03..07dde56d 100644 --- a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/openid_connect/OpenIdConnectLoginService.java +++ b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/openid_connect/OpenIdConnectLoginService.java @@ -36,12 +36,11 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.core.Response; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails; +import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; /** @@ -50,9 +49,6 @@ */ public class OpenIdConnectLoginService extends Oauth2LoginService { - private static final Logger LOGGER = - LogManager.getLogger(OpenIdConnectLoginService.class.getName()); - public OpenIdConnectLoginService(IdPLoginRest loginRest) { loginRest.registerService("oidc", this); } @@ -68,7 +64,10 @@ public Response doInternalRedirect( HttpServletRequest request, HttpServletResponse response, String provider) { String token = getAccessToken(); String refreshToken = getRefreshAccessToken(); - if (token == null && SecurityContextHolder.getContext() != null) { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (token == null + && SecurityContextHolder.getContext() != null + && requestAttributes != null) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.getDetails() != null @@ -77,31 +76,28 @@ public Response doInternalRedirect( OAuth2AccessToken accessTokenDetails = tokenDetails.getAccessToken(); if (accessTokenDetails != null) { token = accessTokenDetails.getValue(); - RequestContextHolder.getRequestAttributes() - .setAttribute(ACCESS_TOKEN_PARAM, accessTokenDetails, 0); - RequestContextHolder.getRequestAttributes() - .setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, token, 0); + requestAttributes.setAttribute(ACCESS_TOKEN_PARAM, accessTokenDetails, 0); + requestAttributes.setAttribute( + OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, token, 0); if (accessTokenDetails.getRefreshToken().getValue() != null) { refreshToken = accessTokenDetails.getRefreshToken().getValue(); - RequestContextHolder.getRequestAttributes() - .setAttribute( - REFRESH_TOKEN_PARAM, - accessTokenDetails.getRefreshToken().getValue(), - 0); + requestAttributes.setAttribute( + REFRESH_TOKEN_PARAM, + accessTokenDetails.getRefreshToken().getValue(), + 0); } } if (tokenDetails.getIdToken() != null) { - RequestContextHolder.getRequestAttributes() - .setAttribute(ID_TOKEN_PARAM, tokenDetails.getIdToken(), 0); - RequestContextHolder.getRequestAttributes() - .setAttribute( - OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, - tokenDetails.getIdToken(), - 0); + requestAttributes.setAttribute(ID_TOKEN_PARAM, tokenDetails.getIdToken(), 0); + requestAttributes.setAttribute( + OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, + tokenDetails.getIdToken(), + 0); } } } - RequestContextHolder.getRequestAttributes().setAttribute(PROVIDER_KEY, provider, 0); + assert requestAttributes != null; + requestAttributes.setAttribute(PROVIDER_KEY, provider, 0); return buildCallbackResponse(token, refreshToken, provider); } } diff --git a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/openid_connect/OpenIdConnectSessionServiceDelegate.java b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/openid_connect/OpenIdConnectSessionServiceDelegate.java index 542d4ab1..bdf93f7d 100644 --- a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/openid_connect/OpenIdConnectSessionServiceDelegate.java +++ b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/security/oauth2/openid_connect/OpenIdConnectSessionServiceDelegate.java @@ -27,11 +27,8 @@ */ package it.geosolutions.geostore.services.rest.security.oauth2.openid_connect; -import static it.geosolutions.geostore.services.rest.security.oauth2.openid_connect.OpenIdConnectSecurityConfiguration.CONF_BEAN_NAME; - import it.geosolutions.geostore.services.UserService; import it.geosolutions.geostore.services.rest.RESTSessionService; -import it.geosolutions.geostore.services.rest.security.oauth2.OAuth2Configuration; import it.geosolutions.geostore.services.rest.security.oauth2.OAuth2SessionServiceDelegate; import it.geosolutions.geostore.services.rest.utils.GeoStoreContext; import org.springframework.security.oauth2.client.OAuth2RestTemplate; @@ -44,11 +41,6 @@ public OpenIdConnectSessionServiceDelegate( super(restSessionService, "oidc", userService); } - @Override - protected OAuth2Configuration configuration() { - return configuration(CONF_BEAN_NAME); - } - @Override protected OAuth2RestTemplate restTemplate() { return GeoStoreContext.bean("oidcOpenIdRestTemplate", OAuth2RestTemplate.class); diff --git a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/utils/GeoStoreContext.java b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/utils/GeoStoreContext.java index 43747867..2f49303b 100644 --- a/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/utils/GeoStoreContext.java +++ b/src/modules/rest/impl/src/main/java/it/geosolutions/geostore/services/rest/utils/GeoStoreContext.java @@ -1,5 +1,6 @@ package it.geosolutions.geostore.services.rest.utils; +import java.util.Map; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.beans.BeansException; @@ -17,6 +18,16 @@ public class GeoStoreContext implements ApplicationContextAware { */ static ApplicationContext context; + public static Map beans(Class clazz) { + Map result = null; + try { + if (context != null) result = context.getBeansOfType(clazz); + } catch (Exception e) { + LOGGER.error("Error while retrieving the bean of type {}", clazz.getSimpleName(), e); + } + return result; + } + public static T bean(Class clazz) { T result = null; try { diff --git a/src/modules/rest/impl/src/test/java/it/geosolutions/geostore/rest/security/oauth2/OAuth2SessionServiceTest.java b/src/modules/rest/impl/src/test/java/it/geosolutions/geostore/rest/security/oauth2/OAuth2SessionServiceTest.java index 29b5c117..d9a37609 100644 --- a/src/modules/rest/impl/src/test/java/it/geosolutions/geostore/rest/security/oauth2/OAuth2SessionServiceTest.java +++ b/src/modules/rest/impl/src/test/java/it/geosolutions/geostore/rest/security/oauth2/OAuth2SessionServiceTest.java @@ -29,8 +29,7 @@ import static it.geosolutions.geostore.services.rest.SessionServiceDelegate.PROVIDER_KEY; import static it.geosolutions.geostore.services.rest.security.oauth2.OAuth2Utils.ACCESS_TOKEN_PARAM; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; +import static org.junit.Assert.*; import static org.mockito.Mockito.when; import it.geosolutions.geostore.services.rest.RESTSessionService; @@ -44,6 +43,7 @@ import it.geosolutions.geostore.services.rest.security.oauth2.google.OAuthGoogleSecurityConfiguration; import it.geosolutions.geostore.services.rest.utils.GeoStoreContext; import java.util.ArrayList; +import java.util.HashMap; import org.junit.Test; import org.mockito.MockedStatic; import org.mockito.Mockito; @@ -68,7 +68,9 @@ public class OAuth2SessionServiceTest { @Test public void testLogout() { GoogleOAuth2Configuration configuration = new GoogleOAuth2Configuration(); + configuration.setEnabled(true); configuration.setIdTokenUri("https://www.googleapis.com/oauth2/v3/certs"); + configuration.setRevokeEndpoint("http://google.foo"); PreAuthenticatedAuthenticationToken authenticationToken = new PreAuthenticatedAuthenticationToken("user", "", new ArrayList<>()); OAuth2AccessToken accessToken = new DefaultOAuth2AccessToken(ACCESS_TOKEN); @@ -85,7 +87,12 @@ public void testLogout() { new DefaultOAuth2ClientContext(), configuration); SecurityContextHolder.getContext().setAuthentication(authenticationToken); + HashMap configurations = new HashMap<>(); + configurations.put("googleOAuth2Config", configuration); try (MockedStatic utilities = Mockito.mockStatic(GeoStoreContext.class)) { + utilities + .when(() -> GeoStoreContext.beans(OAuth2Configuration.class)) + .thenReturn(configurations); utilities .when( () -> @@ -115,6 +122,7 @@ public void testLogout() { // start the test sessionService.removeSession(); assertEquals(response.getStatus(), HttpStatus.OK_200); + // if the end-session URI is null, the session won't be invalidated assertNull(SecurityContextHolder.getContext().getAuthentication()); assertNull(request.getUserPrincipal()); }