Skip to content

Commit 2870e35

Browse files
committed
AUT-2042 Change Spring default logout behaviour to ensure http method is not changed during redirect and send request content with form parameters when POST log out is used
1 parent 6526bf7 commit 2870e35

File tree

8 files changed

+132
-40
lines changed

8 files changed

+132
-40
lines changed

src/main/java/ee/ria/govsso/client/controller/ClientController.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
import ee.ria.govsso.client.authentication.ExampleClientUser;
44
import ee.ria.govsso.client.configuration.ExampleClientSessionProperties;
5+
import ee.ria.govsso.client.govsso.configuration.GovssoProperties;
56
import ee.ria.govsso.client.govsso.configuration.authentication.GovssoAuthentication;
67
import ee.ria.govsso.client.govsso.oauth2.GovssoSessionUtil;
78
import ee.ria.govsso.client.util.AccessTokenUtil;
89
import ee.ria.govsso.client.util.DemoResponseUtil;
10+
import ee.ria.govsso.client.util.LogoutUtil;
11+
import jakarta.servlet.http.HttpServletRequest;
912
import lombok.RequiredArgsConstructor;
1013
import lombok.extern.slf4j.Slf4j;
1114
import org.springframework.beans.factory.annotation.Value;
@@ -23,6 +26,7 @@
2326

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

2731
@Slf4j
2832
@Controller
@@ -42,6 +46,8 @@ public class ClientController {
4246
private String applicationIntroLong;
4347
@Value("${example-client.messages.info-service}")
4448
private String applicationInfoService;
49+
@Value("${govsso.post-logout-redirect-uri}")
50+
private String postLogoutRedirectUri;
4551

4652
@GetMapping(value = LOGIN_VIEW_MAPPING, produces = MediaType.TEXT_HTML_VALUE)
4753
public ModelAndView clientLoginView(
@@ -65,7 +71,7 @@ public ModelAndView clientLoginView(
6571
}
6672

6773
@GetMapping(value = DASHBOARD_MAPPING, produces = MediaType.TEXT_HTML_VALUE)
68-
public ModelAndView dashboard(@AuthenticationPrincipal OidcUser oidcUser, ExampleClientUser exampleClientUser, Authentication authentication) {
74+
public ModelAndView dashboard(@AuthenticationPrincipal OidcUser oidcUser, ExampleClientUser exampleClientUser, Authentication authentication, HttpServletRequest request) {
6975
ModelAndView model = new ModelAndView("dashboard");
7076
model.addObject("application_logo", applicationLogo);
7177
model.addObject("authentication_provider", getAuthenticationProvider());
@@ -79,6 +85,14 @@ public ModelAndView dashboard(@AuthenticationPrincipal OidcUser oidcUser, Exampl
7985
if (AccessTokenUtil.isJwtAccessToken(accessToken)) {
8086
model.addObject("access_token", accessToken);
8187
}
88+
String locale = LogoutUtil.getUiLocale(request);
89+
if (locale != null) {
90+
model.addObject("ui_locales", locale);
91+
}
92+
String postLogoutRedirectUri = LogoutUtil.postLogoutRedirectUri(request, this.postLogoutRedirectUri);
93+
if (postLogoutRedirectUri != null) {
94+
model.addObject("post_logout_redirect_uri", postLogoutRedirectUri);
95+
}
8296
}
8397

8498
log.info("Showing dashboard for subject='{}'", oidcUser.getSubject());

src/main/java/ee/ria/govsso/client/govsso/configuration/GovssoSecurityConfiguration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public SecurityFilterChain filterChain(
120120
.defaultSuccessUrl("/dashboard")
121121
.failureHandler(getAuthFailureHandler()))
122122
.logout(logoutConfigurer -> {
123-
logoutConfigurer.logoutUrl("/oauth/logout");
123+
logoutConfigurer.logoutRequestMatcher(new AntPathRequestMatcher("/oauth/logout"));
124124
/*
125125
Using custom handlers to pass ui_locales parameter to GovSSO logout flow.
126126
*/

src/main/java/ee/ria/govsso/client/govsso/oauth2/GovssoClientInitiatedLogoutSuccessHandler.java

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
package ee.ria.govsso.client.govsso.oauth2;
22

3+
import ee.ria.govsso.client.util.LogoutUtil;
34
import jakarta.servlet.http.HttpServletRequest;
45
import jakarta.servlet.http.HttpServletResponse;
56
import org.apache.commons.lang3.StringUtils;
7+
import org.springframework.core.log.LogMessage;
8+
import org.springframework.http.HttpMethod;
9+
import org.springframework.http.HttpStatus;
610
import org.springframework.security.core.Authentication;
711
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
812
import org.springframework.security.oauth2.client.registration.ClientRegistration;
913
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
1014
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
15+
import org.springframework.security.web.DefaultRedirectStrategy;
1116
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
12-
import org.springframework.security.web.util.UrlUtils;
13-
import org.springframework.web.util.UriComponents;
1417
import org.springframework.web.util.UriComponentsBuilder;
1518

19+
import java.io.IOException;
1620
import java.net.URI;
1721
import java.nio.charset.StandardCharsets;
18-
import java.util.Collections;
1922

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

@@ -27,10 +30,30 @@
2730
public class GovssoClientInitiatedLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
2831
private final ClientRegistrationRepository clientRegistrationRepository;
2932
private final String postLogoutRedirectUri;
33+
private final DefaultRedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
3034

3135
public GovssoClientInitiatedLogoutSuccessHandler(ClientRegistrationRepository clientRegistrationRepository, String postLogoutRedirectUri) {
3236
this.clientRegistrationRepository = clientRegistrationRepository;
3337
this.postLogoutRedirectUri = postLogoutRedirectUri;
38+
this.redirectStrategy.setStatusCode(HttpStatus.TEMPORARY_REDIRECT);
39+
}
40+
41+
@Override
42+
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
43+
throws IOException {
44+
handle(request, response, authentication);
45+
}
46+
47+
@Override
48+
protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
49+
throws IOException {
50+
String targetUrl = determineTargetUrl(request, response, authentication);
51+
if (response.isCommitted()) {
52+
this.logger.debug(LogMessage.format("Did not redirect to %s since response already committed.", targetUrl));
53+
return;
54+
}
55+
56+
this.redirectStrategy.sendRedirect(request, response, targetUrl);
3457
}
3558

3659
@Override
@@ -42,7 +65,7 @@ protected String determineTargetUrl(HttpServletRequest request, HttpServletRespo
4265
URI endSessionEndpoint = endSessionEndpoint(clientRegistration);
4366
if (endSessionEndpoint != null) {
4467
String idToken = idToken(authentication);
45-
String postLogoutRedirectUri = postLogoutRedirectUri(request);
68+
String postLogoutRedirectUri = LogoutUtil.postLogoutRedirectUri(request, this.postLogoutRedirectUri);
4669
targetUrl = endpointUri(request, endSessionEndpoint, idToken, postLogoutRedirectUri);
4770
}
4871
}
@@ -64,33 +87,21 @@ private String idToken(Authentication authentication) {
6487
return ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue();
6588
}
6689

67-
private String postLogoutRedirectUri(HttpServletRequest request) {
68-
if (postLogoutRedirectUri == null) {
69-
return null;
70-
}
71-
UriComponents uriComponents = UriComponentsBuilder
72-
.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
73-
.replacePath(request.getContextPath())
74-
.replaceQuery(null)
75-
.fragment(null)
76-
.build();
77-
return UriComponentsBuilder.fromUriString(postLogoutRedirectUri)
78-
.buildAndExpand(Collections.singletonMap("baseUrl", uriComponents.toUriString()))
79-
.toUriString();
80-
}
81-
8290
private String endpointUri(HttpServletRequest request, URI endSessionEndpoint, String idToken, String postLogoutRedirectUri) {
8391
UriComponentsBuilder builder = UriComponentsBuilder.fromUri(endSessionEndpoint);
8492

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

87-
builder.queryParam("id_token_hint", idToken);
88-
if (StringUtils.isNotEmpty(locale)) {
89-
builder.queryParam(UI_LOCALES_PARAMETER, locale);
90-
}
91-
if (postLogoutRedirectUri != null) {
92-
builder.queryParam("post_logout_redirect_uri", postLogoutRedirectUri);
95+
if (request.getMethod().equals(HttpMethod.GET.name())) {
96+
builder.queryParam("id_token_hint", idToken);
97+
if (StringUtils.isNotEmpty(locale)) {
98+
builder.queryParam(UI_LOCALES_PARAMETER, locale);
99+
}
100+
if (postLogoutRedirectUri != null) {
101+
builder.queryParam("post_logout_redirect_uri", postLogoutRedirectUri);
102+
}
93103
}
104+
94105
return builder.encode(StandardCharsets.UTF_8)
95106
.build()
96107
.toUriString();

src/main/java/ee/ria/govsso/client/govsso/oauth2/GovssoLocalePassingLogoutHandler.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package ee.ria.govsso.client.govsso.oauth2;
22

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

2526
@Override
2627
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
27-
HttpSession session = request.getSession(false);
28-
if (session == null) {
29-
return;
30-
}
31-
if (session.getAttribute(LOCALE_SESSION_ATTRIBUTE_NAME) instanceof Locale locale) {
32-
request.setAttribute(UI_LOCALES_PARAMETER, locale.getLanguage());
28+
String locale = LogoutUtil.getUiLocale(request);
29+
if (locale != null) {
30+
request.setAttribute(UI_LOCALES_PARAMETER, locale);
3331
}
3432
}
3533
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package ee.ria.govsso.client.util;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import jakarta.servlet.http.HttpSession;
5+
import lombok.experimental.UtilityClass;
6+
import org.springframework.security.web.util.UrlUtils;
7+
import org.springframework.web.util.UriComponents;
8+
import org.springframework.web.util.UriComponentsBuilder;
9+
10+
import java.util.Collections;
11+
import java.util.Locale;
12+
13+
import static org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME;
14+
15+
@UtilityClass
16+
public class LogoutUtil {
17+
18+
public String postLogoutRedirectUri(HttpServletRequest request, String postLogoutRedirectUri) {
19+
if (postLogoutRedirectUri == null) {
20+
return null;
21+
}
22+
UriComponents uriComponents = UriComponentsBuilder
23+
.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
24+
.replacePath(request.getContextPath())
25+
.replaceQuery(null)
26+
.fragment(null)
27+
.build();
28+
return UriComponentsBuilder.fromUriString(postLogoutRedirectUri)
29+
.buildAndExpand(Collections.singletonMap("baseUrl", uriComponents.toUriString()))
30+
.toUriString();
31+
}
32+
33+
public String getUiLocale(HttpServletRequest request) {
34+
HttpSession session = request.getSession(false);
35+
if (session == null) {
36+
return null;
37+
} else if (session.getAttribute(LOCALE_SESSION_ATTRIBUTE_NAME) instanceof Locale locale) {
38+
return locale.getLanguage();
39+
} else {
40+
return null;
41+
}
42+
}
43+
}

src/main/resources/static/scripts/govsso-session-update.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ function updateGovSsoSession() {
5353
const rows = [];
5454

5555
$('#id_token').text(responseBody.id_token);
56+
$('#id_token_hint').text(responseBody.id_token);
5657
$('#access_token').text(responseBody.access_token);
5758
$('#refresh_token').text(responseBody.refresh_token);
5859

src/main/resources/templates/dashboard.html

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,18 @@
2020
width="30"/>&nbsp;<span
2121
th:text="${application_title}"/>
2222
</a>
23-
<ul class=" navbar-nav px-3">
24-
<li class="nav-item text-nowrap">
25-
<form method="post" th:action="@{/oauth/logout}" id="logoutForm">
26-
<input class="btn btn-outline-secondary" name="logout_button" type="submit" value="Log out"/>
23+
<ul class="navbar-nav navbar-expand px-3">
24+
<li class="nav-item px-1 text-nowrap">
25+
<form method="post" th:action="@{/oauth/logout}" id="logoutFormPost">
26+
<input type="hidden" th:id="id_token_hint" name="id_token_hint" th:value="${id_token}">
27+
<input th:if="${ui_locales}" type="hidden" th:id="ui_locales" name="ui_locales" th:value="${ui_locales}">
28+
<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}">
29+
<input class="btn btn-outline-secondary" name="logout_button" type="submit" value="Log out (POST)"/>
30+
</form>
31+
</li>
32+
<li class="nav-item px-1 text-nowrap">
33+
<form method="get" th:action="@{/oauth/logout}" id="logoutFormGet">
34+
<input class="btn btn-outline-secondary" name="logout_button" type="submit" value="Log out (GET)"/>
2735
</form>
2836
</li>
2937
</ul>

src/test/java/ee/ria/govsso/client/GovssoAuthenticationTest.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ee.ria.govsso.client;
22

33
import io.restassured.filter.cookie.CookieFilter;
4+
import io.restassured.http.ContentType;
45
import io.restassured.response.ExtractableResponse;
56
import io.restassured.response.Response;
67
import lombok.SneakyThrows;
@@ -15,6 +16,7 @@
1516
import java.nio.charset.StandardCharsets;
1617

1718
import static ee.ria.govsso.client.UrlMatcher.url;
19+
import static ee.ria.govsso.client.configuration.CookieConfiguration.COOKIE_NAME_XSRF_TOKEN;
1820
import static ee.ria.govsso.client.controller.ClientController.DASHBOARD_MAPPING;
1921
import static io.restassured.RestAssured.given;
2022
import static java.util.Objects.requireNonNull;
@@ -32,7 +34,7 @@ public void applicationStartup() {
3234
@Test
3335
@SneakyThrows
3436
@Disabled
35-
public void authentication() {
37+
public void authenticationAndLogout() {
3638
String code = "randomly-generated-code";
3739
CookieFilter cookieFilter = new CookieFilter();
3840
ExtractableResponse<Response> startAuthenticationResponse = given()
@@ -76,14 +78,29 @@ public void authentication() {
7678
.port(equalTo(port))
7779
.path(equalTo("/dashboard")));
7880

79-
given()
81+
Response response = given()
8082
.filter(cookieFilter)
8183
.when()
8284
.get(DASHBOARD_MAPPING)
8385
.then()
8486
.statusCode(200)
8587
.body(containsString("id=\"access_token\">eyJhbGciOiJSUzI1NiIsImtpZCI6IjJiMDZiMjNmLTI2MDMtNGMxYy05OWU2LWRmOTVjYjRlMzAwMSIsInR5cCI6IkpXVCJ9.eyJhY3IiOiJoaWdoIiwiYW1yIjpbIm1JRCJdLCJhdWQiOlsiaHR0cHM6Ly90ZXN0Il0sImJpcnRoZGF0ZSI6IjE5NjEtMDctMTIiLCJjbGllbnRfaWQiOiJjbGllbnQtYSIsImV4cCI6MTcxMDgzMzk2MywiZXh0Ijp7ImFjciI6ImhpZ2giLCJhbXIiOlsibUlEIl0sImJpcnRoZGF0ZSI6IjE5NjEtMDctMTIiLCJmYW1pbHlfbmFtZSI6IlBlcmVrb25uYW5pbWkzIiwiZ2l2ZW5fbmFtZSI6IkVlc25pbWkzIn0sImZhbWlseV9uYW1lIjoiUGVyZWtvbm5hbmltaTMiLCJnaXZlbl9uYW1lIjoiRWVzbmltaTMiLCJpYXQiOjE3MTA4MzM5NjIsImlzcyI6Imh0dHBzOi8vaW5wcm94eS5sb2NhbGhvc3Q6MTM0NDMvIiwianRpIjoiYWFjMmM4ZDEtYTNiMy00MmM1LWEzYzQtZTc4ZGIyZTc1NWNmIiwic2NwIjpbIm9wZW5pZCJdLCJzdWIiOiJJc2lrdWtvb2QzIn0.ZLVYuMPTrz-D8XIfc_V1DnAwfBGDD02IjUIKNPstwKmN3WcWPFL1utjDtbo3oGPoQvWEZYBfnpXOdAFYYcnBax7Aj4cUW1uamz0rKGInOE_-0o66Go9bMqJ5sA9mJn5EYS293SYsfDaFLz_P598FNohAIlovJj2CgYRQI7JPHkIBGKDKYGprQ-QywB13qEamosDGII1DH_RtCwWcqn5QEHzbsbuoARNXZ28G4vLpihuCKl-aHUDnms5vTsZRaeiR6YyAxJYkJdUG7FKE6c5ocLmp29aN19jIANpoiDLsGVATuoqFns0VwnVaXugMpAMvgscb29hItvoQlrwyKlbrPwRRHpdBP4L74kMxL5u8yTjVgTlnySKtc7YmJfXdpBUcRedsdTu4qsApzPkLASr0x7hSiclHYUtR1s9mDhuZH38_gsa43cVhOsayoeH-Fdr8hGvqTCihVlsFdWgd0fLfXYRqXDz9lPLpphMdJty1iQ1DSG5jSVaoaT-e1JSHNXCH1I21AomxWp5cvrEbK9VmaAeT6lelReVADeTJg1pBBUx_mJVxnh_Js0LrJrtxHRV-OWNo4kCBVYUjtJFsjvPovKR8dSGt1KzuCLKehKo5JNM4wiM4-hXMiwLkjE-qOYazMapGmqrXU-ijV3lOr0DROnB8fBWLl3j2FoHiQ9a0nbY"))
86-
.body(containsString("id=\"refresh_token\">XF-G7eKbiuZ0eaUaZY7WZsz70Jmm0Tro6AiTyeQcULU.Xh4GpFqcJ-vatFArYknlH6dbY8FfnxaC3xf-uzLHhPY"));
88+
.body(containsString("id=\"refresh_token\">XF-G7eKbiuZ0eaUaZY7WZsz70Jmm0Tro6AiTyeQcULU.Xh4GpFqcJ-vatFArYknlH6dbY8FfnxaC3xf-uzLHhPY"))
89+
.extract().response();
90+
91+
String xorCsrfToken = response.htmlPath().getString("html.head.meta[2].@content");
92+
String rawCsrfToken= response.getCookie("__Host-XSRF-TOKEN");
93+
94+
given()
95+
.filter(cookieFilter)
96+
.when()
97+
.cookie(COOKIE_NAME_XSRF_TOKEN, rawCsrfToken)
98+
.header(HttpHeaders.CONTENT_TYPE, ContentType.URLENC)
99+
.formParam("_csrf", xorCsrfToken)
100+
.post("/oauth/logout")
101+
.then()
102+
.statusCode(307)
103+
.header("Location", "https://inproxy.localhost:13442/oauth2/sessions/logout");
87104
}
88105

89106
private static String getQueryParam(UriComponents locationComponents, String paramName) {

0 commit comments

Comments
 (0)