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 4a552d2a..22f3ecb8 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 @@ -49,6 +49,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.http.*; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.OAuth2ClientContext; @@ -63,6 +64,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.HttpMessageConverterExtractor; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import org.springframework.web.context.request.RequestContextHolder; @@ -123,84 +125,121 @@ public SessionToken refresh(String refreshToken, String accessToken) { } /** - * Invokes the refresh endpoint and return a session token holding the updated tokens details. + * Invokes the refresh endpoint to get a new session token with updated token details. * - * @param refreshToken the refresh token. - * @param accessToken the access token. - * @param configuration the OAuth2Configuration. - * @return the SessionToken. + *

This method attempts to refresh the session by exchanging the provided refresh token for a + * new access token. If the refresh token is invalid or the request fails after several retries, + * the session is cleared, and the user is redirected to the login page. + * + * @param refreshToken the refresh token to use for obtaining new access and refresh tokens + * @param accessToken the current access token + * @param configuration the OAuth2Configuration containing client credentials and endpoint URI + * @return a SessionToken containing the new token details, or null if the refresh process + * failed */ protected SessionToken doRefresh( String refreshToken, String accessToken, OAuth2Configuration configuration) { SessionToken sessionToken = null; + int maxRetries = 3; + int attempt = 0; + boolean success = false; + // Setup HTTP headers and body for the request RestTemplate restTemplate = new RestTemplate(); 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()); requestBody.add("client_id", configuration.getClientId()); - 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); - } + while (attempt < maxRetries && !success) { + attempt++; + LOGGER.info("Attempting to refresh token, attempt {} of {}", attempt, maxRetries); - if (newToken != null && newToken.getValue() != null && !newToken.getValue().isEmpty()) { - // update the Authentication - OAuth2RefreshToken newRefreshToken = newToken.getRefreshToken(); - OAuth2RefreshToken refreshTokenToUse = - newRefreshToken != null - && newRefreshToken.getValue() != null - && !newRefreshToken.getValue().isEmpty() - ? newRefreshToken - : new DefaultOAuth2RefreshToken(refreshToken); - updateAuthToken(accessToken, newToken, refreshTokenToUse, configuration); - sessionToken = - sessionToken( - newToken.getValue(), - refreshTokenToUse.getValue(), - newToken.getExpiration()); - } else if (accessToken != null) { - // update the Authentication - sessionToken = sessionToken(accessToken, refreshToken, null); - } else { - // 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: {}. Redirecting to login.", - configuration.buildRefreshTokenURI("offline")); - doLogout(null); try { - getResponse() - .sendRedirect( - "../../openid/" - + configuration.getProvider().toLowerCase() - + "/login"); - } catch (IOException e) { - LOGGER.error("Error while sending redirect to login service. ", e); - throw new RuntimeException(e); + ResponseEntity response = + restTemplate.exchange( + configuration.buildRefreshTokenURI(), + HttpMethod.POST, + requestEntity, + OAuth2AccessToken.class); + + if (response.getStatusCode().is2xxSuccessful()) { + OAuth2AccessToken newToken = response.getBody(); + if (newToken != null + && newToken.getValue() != null + && !newToken.getValue().isEmpty()) { + // Process and update the new token details + OAuth2RefreshToken newRefreshToken = newToken.getRefreshToken(); + OAuth2RefreshToken refreshTokenToUse = + (newRefreshToken != null && newRefreshToken.getValue() != null) + ? newRefreshToken + : new DefaultOAuth2RefreshToken(refreshToken); + + updateAuthToken(accessToken, newToken, refreshTokenToUse, configuration); + sessionToken = + sessionToken( + newToken.getValue(), + refreshTokenToUse.getValue(), + newToken.getExpiration()); + + LOGGER.info("Token refreshed successfully on attempt {}", attempt); + success = true; + } else { + LOGGER.warn("Received empty or null token on attempt {}", attempt); + } + } else if (response.getStatusCode().is4xxClientError()) { + // For client errors (e.g., 400, 401, 403), do not retry. + LOGGER.error( + "Client error occurred: {}. Stopping further attempts.", + response.getStatusCode()); + break; + } else { + // For server errors (5xx), continue retrying + LOGGER.warn("Server error occurred: {}. Retrying...", response.getStatusCode()); + } + } catch (RestClientException ex) { + LOGGER.error("Attempt {}: Error refreshing token: {}", attempt, ex.getMessage()); + if (attempt == maxRetries) { + LOGGER.error("Max retries reached. Unable to refresh token."); + } } } + + // Handle unsuccessful refresh + if (!success) { + handleRefreshFailure(accessToken, refreshToken, configuration); + } return sessionToken; } + /** + * Handles the refresh failure by clearing the session, logging out remotely, and redirecting to + * login. + * + * @param accessToken the current access token + * @param refreshToken the current refresh token + * @param configuration the OAuth2Configuration with endpoint details + */ + private void handleRefreshFailure( + String accessToken, String refreshToken, OAuth2Configuration configuration) { + LOGGER.info( + "Unable to refresh token after max retries. Clearing session and redirecting to login."); + doLogout(null); + + try { + String redirectUrl = + "../../openid/" + configuration.getProvider().toLowerCase() + "/login"; + getResponse().sendRedirect(redirectUrl); + } catch (IOException e) { + LOGGER.error("Error while sending redirect to login service: ", e); + throw new RuntimeException("Failed to redirect to login", e); + } + } + private static HttpHeaders getHttpHeaders( String accessToken, OAuth2Configuration configuration) { HttpHeaders headers = new HttpHeaders(); @@ -240,23 +279,26 @@ private void updateAuthToken( if (LOGGER.isDebugEnabled()) LOGGER.info("Updating the cache and the SecurityContext with new Auth details"); - TokenDetails details = getTokenDetails(authentication); - String idToken = details.getIdToken(); - cache().removeEntry(oldToken); - PreAuthenticatedAuthenticationToken updated = - new PreAuthenticatedAuthenticationToken( - authentication.getPrincipal(), - authentication.getCredentials(), - authentication.getAuthorities()); - DefaultOAuth2AccessToken accessToken = new DefaultOAuth2AccessToken(newToken); - if (refreshToken != null) { - accessToken.setRefreshToken(refreshToken); + if (authentication != null && !(authentication instanceof AnonymousAuthenticationToken)) { + TokenDetails details = getTokenDetails(authentication); + String idToken = details.getIdToken(); + cache().removeEntry(oldToken); + PreAuthenticatedAuthenticationToken updated = + new PreAuthenticatedAuthenticationToken( + authentication.getPrincipal(), + authentication.getCredentials(), + authentication.getAuthorities()); + DefaultOAuth2AccessToken accessToken = new DefaultOAuth2AccessToken(newToken); + if (refreshToken != null) { + accessToken.setRefreshToken(refreshToken); + } + if (LOGGER.isDebugEnabled()) + LOGGER.debug( + "Creating new details. AccessToken: {} IdToken: {}", accessToken, idToken); + updated.setDetails(new TokenDetails(accessToken, idToken, conf.getBeanName())); + cache().putCacheEntry(newToken.getValue(), updated); + SecurityContextHolder.getContext().setAuthentication(updated); } - if (LOGGER.isDebugEnabled()) - LOGGER.debug("Creating new details. AccessToken: {} IdToken: {}", accessToken, idToken); - updated.setDetails(new TokenDetails(accessToken, idToken, conf.getBeanName())); - cache().putCacheEntry(newToken.getValue(), updated); - SecurityContextHolder.getContext().setAuthentication(updated); } private OAuth2AccessToken retrieveAccessToken(String accessToken) {