Skip to content

Commit

Permalink
AUT-2042 Change Spring default logout behaviour to ensure http method…
Browse files Browse the repository at this point in the history
… is not changed during redirect and send request content with form parameters when POST log out is used
  • Loading branch information
Marten332 committed Dec 13, 2024
1 parent 7d4ecdd commit 25cbd57
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import ee.ria.govsso.client.authentication.ExampleClientUser;
import ee.ria.govsso.client.configuration.ExampleClientSessionProperties;
import ee.ria.govsso.client.govsso.configuration.GovssoProperties;
import ee.ria.govsso.client.govsso.configuration.authentication.GovssoAuthentication;
import ee.ria.govsso.client.govsso.oauth2.GovssoSessionUtil;
import ee.ria.govsso.client.util.AccessTokenUtil;
import ee.ria.govsso.client.util.DemoResponseUtil;
import ee.ria.govsso.client.util.LogoutUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -23,6 +26,7 @@

import static ee.ria.govsso.client.govsso.configuration.condition.OnGovssoCondition.GOVSSO_PROFILE;
import static ee.ria.govsso.client.tara.configuration.condition.OnTaraCondition.TARA_PROFILE;
import static org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME;

@Slf4j
@Controller
Expand All @@ -42,6 +46,8 @@ public class ClientController {
private String applicationIntroLong;
@Value("${example-client.messages.info-service}")
private String applicationInfoService;
@Value("${govsso.post-logout-redirect-uri}")
private String postLogoutRedirectUri;

@GetMapping(value = LOGIN_VIEW_MAPPING, produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView clientLoginView(
Expand All @@ -65,7 +71,7 @@ public ModelAndView clientLoginView(
}

@GetMapping(value = DASHBOARD_MAPPING, produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView dashboard(@AuthenticationPrincipal OidcUser oidcUser, ExampleClientUser exampleClientUser, Authentication authentication) {
public ModelAndView dashboard(@AuthenticationPrincipal OidcUser oidcUser, ExampleClientUser exampleClientUser, Authentication authentication, HttpServletRequest request) {
ModelAndView model = new ModelAndView("dashboard");
model.addObject("application_logo", applicationLogo);
model.addObject("authentication_provider", getAuthenticationProvider());
Expand All @@ -79,6 +85,14 @@ public ModelAndView dashboard(@AuthenticationPrincipal OidcUser oidcUser, Exampl
if (AccessTokenUtil.isJwtAccessToken(accessToken)) {
model.addObject("access_token", accessToken);
}
String locale = LogoutUtil.getUiLocale(request);
if (locale != null) {
model.addObject("ui_locales", locale);
}
String postLogoutRedirectUri = LogoutUtil.postLogoutRedirectUri(request, this.postLogoutRedirectUri);
if (postLogoutRedirectUri != null) {
model.addObject("post_logout_redirect_uri", postLogoutRedirectUri);
}
}

log.info("Showing dashboard for subject='{}'", oidcUser.getSubject());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public SecurityFilterChain filterChain(
.defaultSuccessUrl("/dashboard")
.failureHandler(getAuthFailureHandler()))
.logout(logoutConfigurer -> {
logoutConfigurer.logoutUrl("/oauth/logout");
logoutConfigurer.logoutRequestMatcher(new AntPathRequestMatcher("/oauth/logout"));
/*
Using custom handlers to pass ui_locales parameter to GovSSO logout flow.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
package ee.ria.govsso.client.govsso.oauth2;

import ee.ria.govsso.client.util.LogoutUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Collections;

import static ee.ria.govsso.client.govsso.oauth2.GovssoLocalePassingLogoutHandler.UI_LOCALES_PARAMETER;

Expand All @@ -27,10 +30,30 @@
public class GovssoClientInitiatedLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private final ClientRegistrationRepository clientRegistrationRepository;
private final String postLogoutRedirectUri;
private final DefaultRedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

public GovssoClientInitiatedLogoutSuccessHandler(ClientRegistrationRepository clientRegistrationRepository, String postLogoutRedirectUri) {
this.clientRegistrationRepository = clientRegistrationRepository;
this.postLogoutRedirectUri = postLogoutRedirectUri;
this.redirectStrategy.setStatusCode(HttpStatus.TEMPORARY_REDIRECT);
}

@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
handle(request, response, authentication);
}

@Override
protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
String targetUrl = determineTargetUrl(request, response, authentication);
if (response.isCommitted()) {
this.logger.debug(LogMessage.format("Did not redirect to %s since response already committed.", targetUrl));
return;
}

this.redirectStrategy.sendRedirect(request, response, targetUrl);
}

@Override
Expand All @@ -42,7 +65,7 @@ protected String determineTargetUrl(HttpServletRequest request, HttpServletRespo
URI endSessionEndpoint = endSessionEndpoint(clientRegistration);
if (endSessionEndpoint != null) {
String idToken = idToken(authentication);
String postLogoutRedirectUri = postLogoutRedirectUri(request);
String postLogoutRedirectUri = LogoutUtil.postLogoutRedirectUri(request, this.postLogoutRedirectUri);
targetUrl = endpointUri(request, endSessionEndpoint, idToken, postLogoutRedirectUri);
}
}
Expand All @@ -64,33 +87,21 @@ private String idToken(Authentication authentication) {
return ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue();
}

private String postLogoutRedirectUri(HttpServletRequest request) {
if (postLogoutRedirectUri == null) {
return null;
}
UriComponents uriComponents = UriComponentsBuilder
.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.getContextPath())
.replaceQuery(null)
.fragment(null)
.build();
return UriComponentsBuilder.fromUriString(postLogoutRedirectUri)
.buildAndExpand(Collections.singletonMap("baseUrl", uriComponents.toUriString()))
.toUriString();
}

private String endpointUri(HttpServletRequest request, URI endSessionEndpoint, String idToken, String postLogoutRedirectUri) {
UriComponentsBuilder builder = UriComponentsBuilder.fromUri(endSessionEndpoint);

String locale = (String) request.getAttribute(UI_LOCALES_PARAMETER);

builder.queryParam("id_token_hint", idToken);
if (StringUtils.isNotEmpty(locale)) {
builder.queryParam(UI_LOCALES_PARAMETER, locale);
}
if (postLogoutRedirectUri != null) {
builder.queryParam("post_logout_redirect_uri", postLogoutRedirectUri);
if (request.getMethod().equals(HttpMethod.GET.name())) {
builder.queryParam("id_token_hint", idToken);
if (StringUtils.isNotEmpty(locale)) {
builder.queryParam(UI_LOCALES_PARAMETER, locale);
}
if (postLogoutRedirectUri != null) {
builder.queryParam("post_logout_redirect_uri", postLogoutRedirectUri);
}
}

return builder.encode(StandardCharsets.UTF_8)
.build()
.toUriString();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ee.ria.govsso.client.govsso.oauth2;

import ee.ria.govsso.client.util.LogoutUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
Expand All @@ -24,12 +25,9 @@ public class GovssoLocalePassingLogoutHandler implements LogoutHandler {

@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
HttpSession session = request.getSession(false);
if (session == null) {
return;
}
if (session.getAttribute(LOCALE_SESSION_ATTRIBUTE_NAME) instanceof Locale locale) {
request.setAttribute(UI_LOCALES_PARAMETER, locale.getLanguage());
String locale = LogoutUtil.getUiLocale(request);
if (locale != null) {
request.setAttribute(UI_LOCALES_PARAMETER, locale);
}
}
}
43 changes: 43 additions & 0 deletions src/main/java/ee/ria/govsso/client/util/LogoutUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package ee.ria.govsso.client.util;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.experimental.UtilityClass;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

import java.util.Collections;
import java.util.Locale;

import static org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME;

@UtilityClass
public class LogoutUtil {

public String postLogoutRedirectUri(HttpServletRequest request, String postLogoutRedirectUri) {
if (postLogoutRedirectUri == null) {
return null;
}
UriComponents uriComponents = UriComponentsBuilder
.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.getContextPath())
.replaceQuery(null)
.fragment(null)
.build();
return UriComponentsBuilder.fromUriString(postLogoutRedirectUri)
.buildAndExpand(Collections.singletonMap("baseUrl", uriComponents.toUriString()))
.toUriString();
}

public String getUiLocale(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return null;
} else if (session.getAttribute(LOCALE_SESSION_ATTRIBUTE_NAME) instanceof Locale locale) {
return locale.getLanguage();
} else {
return null;
}
}
}
1 change: 1 addition & 0 deletions src/main/resources/static/scripts/govsso-session-update.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ function updateGovSsoSession() {
const rows = [];

$('#id_token').text(responseBody.id_token);
$('#id_token_hint').text(responseBody.id_token);
$('#access_token').text(responseBody.access_token);
$('#refresh_token').text(responseBody.refresh_token);

Expand Down
16 changes: 12 additions & 4 deletions src/main/resources/templates/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,18 @@
width="30"/>&nbsp;<span
th:text="${application_title}"/>
</a>
<ul class=" navbar-nav px-3">
<li class="nav-item text-nowrap">
<form method="post" th:action="@{/oauth/logout}" id="logoutForm">
<input class="btn btn-outline-secondary" name="logout_button" type="submit" value="Log out"/>
<ul class="navbar-nav navbar-expand px-3">
<li class="nav-item px-1 text-nowrap">
<form method="post" th:action="@{/oauth/logout}" id="logoutFormPost">
<input type="hidden" th:id="id_token_hint" name="id_token_hint" th:value="${id_token}">
<input th:if="${ui_locales}" type="hidden" th:id="ui_locales" name="ui_locales" th:value="${ui_locales}">
<input th:if="${post_logout_redirect_uri}" type="hidden" th:id="post_logout_redirect_uri" name="post_logout_redirect_uri" th:value="${post_logout_redirect_uri}">
<input class="btn btn-outline-secondary" name="logout_button" type="submit" value="Log out (POST)"/>
</form>
</li>
<li class="nav-item px-1 text-nowrap">
<form method="get" th:action="@{/oauth/logout}" id="logoutFormGet">
<input class="btn btn-outline-secondary" name="logout_button" type="submit" value="Log out (GET)"/>
</form>
</li>
</ul>
Expand Down
23 changes: 20 additions & 3 deletions src/test/java/ee/ria/govsso/client/GovssoAuthenticationTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ee.ria.govsso.client;

import io.restassured.filter.cookie.CookieFilter;
import io.restassured.http.ContentType;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import lombok.SneakyThrows;
Expand All @@ -14,6 +15,7 @@
import java.nio.charset.StandardCharsets;

import static ee.ria.govsso.client.UrlMatcher.url;
import static ee.ria.govsso.client.configuration.CookieConfiguration.COOKIE_NAME_XSRF_TOKEN;
import static ee.ria.govsso.client.controller.ClientController.DASHBOARD_MAPPING;
import static io.restassured.RestAssured.given;
import static java.util.Objects.requireNonNull;
Expand All @@ -30,7 +32,7 @@ public void applicationStartup() {

@Test
@SneakyThrows
public void authentication() {
public void authenticationAndLogout() {
String code = "randomly-generated-code";
CookieFilter cookieFilter = new CookieFilter();
ExtractableResponse<Response> startAuthenticationResponse = given()
Expand Down Expand Up @@ -74,14 +76,29 @@ public void authentication() {
.port(equalTo(port))
.path(equalTo("/dashboard")));

given()
Response response = given()
.filter(cookieFilter)
.when()
.get(DASHBOARD_MAPPING)
.then()
.statusCode(200)
.body(containsString("id=\"access_token\">eyJhbGciOiJSUzI1NiIsImtpZCI6IjJiMDZiMjNmLTI2MDMtNGMxYy05OWU2LWRmOTVjYjRlMzAwMSIsInR5cCI6IkpXVCJ9.eyJhY3IiOiJoaWdoIiwiYW1yIjpbIm1JRCJdLCJhdWQiOlsiaHR0cHM6Ly90ZXN0Il0sImJpcnRoZGF0ZSI6IjE5NjEtMDctMTIiLCJjbGllbnRfaWQiOiJjbGllbnQtYSIsImV4cCI6MTcxMDgzMzk2MywiZXh0Ijp7ImFjciI6ImhpZ2giLCJhbXIiOlsibUlEIl0sImJpcnRoZGF0ZSI6IjE5NjEtMDctMTIiLCJmYW1pbHlfbmFtZSI6IlBlcmVrb25uYW5pbWkzIiwiZ2l2ZW5fbmFtZSI6IkVlc25pbWkzIn0sImZhbWlseV9uYW1lIjoiUGVyZWtvbm5hbmltaTMiLCJnaXZlbl9uYW1lIjoiRWVzbmltaTMiLCJpYXQiOjE3MTA4MzM5NjIsImlzcyI6Imh0dHBzOi8vaW5wcm94eS5sb2NhbGhvc3Q6MTM0NDMvIiwianRpIjoiYWFjMmM4ZDEtYTNiMy00MmM1LWEzYzQtZTc4ZGIyZTc1NWNmIiwic2NwIjpbIm9wZW5pZCJdLCJzdWIiOiJJc2lrdWtvb2QzIn0.ZLVYuMPTrz-D8XIfc_V1DnAwfBGDD02IjUIKNPstwKmN3WcWPFL1utjDtbo3oGPoQvWEZYBfnpXOdAFYYcnBax7Aj4cUW1uamz0rKGInOE_-0o66Go9bMqJ5sA9mJn5EYS293SYsfDaFLz_P598FNohAIlovJj2CgYRQI7JPHkIBGKDKYGprQ-QywB13qEamosDGII1DH_RtCwWcqn5QEHzbsbuoARNXZ28G4vLpihuCKl-aHUDnms5vTsZRaeiR6YyAxJYkJdUG7FKE6c5ocLmp29aN19jIANpoiDLsGVATuoqFns0VwnVaXugMpAMvgscb29hItvoQlrwyKlbrPwRRHpdBP4L74kMxL5u8yTjVgTlnySKtc7YmJfXdpBUcRedsdTu4qsApzPkLASr0x7hSiclHYUtR1s9mDhuZH38_gsa43cVhOsayoeH-Fdr8hGvqTCihVlsFdWgd0fLfXYRqXDz9lPLpphMdJty1iQ1DSG5jSVaoaT-e1JSHNXCH1I21AomxWp5cvrEbK9VmaAeT6lelReVADeTJg1pBBUx_mJVxnh_Js0LrJrtxHRV-OWNo4kCBVYUjtJFsjvPovKR8dSGt1KzuCLKehKo5JNM4wiM4-hXMiwLkjE-qOYazMapGmqrXU-ijV3lOr0DROnB8fBWLl3j2FoHiQ9a0nbY"))
.body(containsString("id=\"refresh_token\">XF-G7eKbiuZ0eaUaZY7WZsz70Jmm0Tro6AiTyeQcULU.Xh4GpFqcJ-vatFArYknlH6dbY8FfnxaC3xf-uzLHhPY"));
.body(containsString("id=\"refresh_token\">XF-G7eKbiuZ0eaUaZY7WZsz70Jmm0Tro6AiTyeQcULU.Xh4GpFqcJ-vatFArYknlH6dbY8FfnxaC3xf-uzLHhPY"))
.extract().response();

String xorCsrfToken = response.htmlPath().getString("html.head.meta[2].@content");
String rawCsrfToken= response.getCookie("__Host-XSRF-TOKEN");

given()
.filter(cookieFilter)
.when()
.cookie(COOKIE_NAME_XSRF_TOKEN, rawCsrfToken)
.header(HttpHeaders.CONTENT_TYPE, ContentType.URLENC)
.formParam("_csrf", xorCsrfToken)
.post("/oauth/logout")
.then()
.statusCode(307)
.header("Location", "https://inproxy.localhost:13442/oauth2/sessions/logout");
}

private static String getQueryParam(UriComponents locationComponents, String paramName) {
Expand Down

0 comments on commit 25cbd57

Please sign in to comment.