Skip to content

Commit 968ebb1

Browse files
committed
baseUrl placeholder for OidcLogoutSuccessHandlers
Fixes gh-7842
1 parent 283e451 commit 968ebb1

File tree

4 files changed

+187
-37
lines changed

4 files changed

+187
-37
lines changed

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandler.java

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818

1919
import java.net.URI;
2020
import java.nio.charset.StandardCharsets;
21+
import java.util.Collections;
2122
import javax.servlet.http.HttpServletRequest;
2223
import javax.servlet.http.HttpServletResponse;
2324

@@ -27,7 +28,9 @@
2728
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
2829
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
2930
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
31+
import org.springframework.security.web.util.UrlUtils;
3032
import org.springframework.util.Assert;
33+
import org.springframework.web.util.UriComponents;
3134
import org.springframework.web.util.UriComponentsBuilder;
3235

3336
/**
@@ -41,7 +44,7 @@
4144
public final class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
4245
private final ClientRegistrationRepository clientRegistrationRepository;
4346

44-
private URI postLogoutRedirectUri;
47+
private String postLogoutRedirectUri;
4548

4649
public OidcClientInitiatedLogoutSuccessHandler(ClientRegistrationRepository clientRegistrationRepository) {
4750
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
@@ -54,9 +57,14 @@ protected String determineTargetUrl(HttpServletRequest request,
5457
String targetUrl = null;
5558
URI endSessionEndpoint;
5659
if (authentication instanceof OAuth2AuthenticationToken && authentication.getPrincipal() instanceof OidcUser) {
57-
endSessionEndpoint = this.endSessionEndpoint((OAuth2AuthenticationToken) authentication);
60+
String registrationId = ((OAuth2AuthenticationToken) authentication).getAuthorizedClientRegistrationId();
61+
ClientRegistration clientRegistration = this.clientRegistrationRepository
62+
.findByRegistrationId(registrationId);
63+
endSessionEndpoint = this.endSessionEndpoint(clientRegistration);
5864
if (endSessionEndpoint != null) {
59-
targetUrl = endpointUri(endSessionEndpoint, authentication);
65+
String idToken = idToken(authentication);
66+
URI postLogoutRedirectUri = postLogoutRedirectUri(request);
67+
targetUrl = endpointUri(endSessionEndpoint, idToken, postLogoutRedirectUri);
6068
}
6169
}
6270
if (targetUrl == null) {
@@ -66,13 +74,11 @@ protected String determineTargetUrl(HttpServletRequest request,
6674
return targetUrl;
6775
}
6876

69-
private URI endSessionEndpoint(OAuth2AuthenticationToken token) {
70-
String registrationId = token.getAuthorizedClientRegistrationId();
71-
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
72-
77+
private URI endSessionEndpoint(ClientRegistration clientRegistration) {
7378
URI result = null;
7479
if (clientRegistration != null) {
75-
Object endSessionEndpoint = clientRegistration.getProviderDetails().getConfigurationMetadata().get("end_session_endpoint");
80+
Object endSessionEndpoint = clientRegistration.getProviderDetails().getConfigurationMetadata()
81+
.get("end_session_endpoint");
7682
if (endSessionEndpoint != null) {
7783
result = URI.create(endSessionEndpoint.toString());
7884
}
@@ -81,25 +87,62 @@ private URI endSessionEndpoint(OAuth2AuthenticationToken token) {
8187
return result;
8288
}
8389

84-
private String endpointUri(URI endSessionEndpoint, Authentication authentication) {
85-
UriComponentsBuilder builder = UriComponentsBuilder.fromUri(endSessionEndpoint);
86-
builder.queryParam("id_token_hint", idToken(authentication));
87-
if (this.postLogoutRedirectUri != null) {
88-
builder.queryParam("post_logout_redirect_uri", this.postLogoutRedirectUri);
90+
private String idToken(Authentication authentication) {
91+
return ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue();
92+
}
93+
94+
private URI postLogoutRedirectUri(HttpServletRequest request) {
95+
if (this.postLogoutRedirectUri == null) {
96+
return null;
8997
}
90-
return builder.encode(StandardCharsets.UTF_8).build().toUriString();
98+
UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
99+
.replacePath(request.getContextPath())
100+
.replaceQuery(null)
101+
.fragment(null)
102+
.build();
103+
return UriComponentsBuilder.fromUriString(this.postLogoutRedirectUri)
104+
.buildAndExpand(Collections.singletonMap("baseUrl", uriComponents.toUriString()))
105+
.toUri();
91106
}
92107

93-
private String idToken(Authentication authentication) {
94-
return ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue();
108+
109+
private String endpointUri(URI endSessionEndpoint, String idToken, URI postLogoutRedirectUri) {
110+
UriComponentsBuilder builder = UriComponentsBuilder.fromUri(endSessionEndpoint);
111+
builder.queryParam("id_token_hint", idToken);
112+
if (postLogoutRedirectUri != null) {
113+
builder.queryParam("post_logout_redirect_uri", postLogoutRedirectUri);
114+
}
115+
return builder.encode(StandardCharsets.UTF_8).build().toUriString();
95116
}
96117

97118
/**
98119
* Set the post logout redirect uri to use
99120
*
100121
* @param postLogoutRedirectUri - A valid URL to which the OP should redirect after logging out the user
122+
* @deprecated {@link #setPostLogoutRedirectUri(String)}
101123
*/
124+
@Deprecated
102125
public void setPostLogoutRedirectUri(URI postLogoutRedirectUri) {
126+
Assert.notNull(postLogoutRedirectUri, "postLogoutRedirectUri cannot be null");
127+
this.postLogoutRedirectUri = postLogoutRedirectUri.toASCIIString();
128+
}
129+
130+
/**
131+
* Set the post logout redirect uri template to use. Supports the {@code "{baseUrl}"}
132+
* placeholder, for example:
133+
*
134+
* <pre>
135+
* handler.setPostLogoutRedirectUriTemplate("{baseUrl}");
136+
* </pre>
137+
*
138+
* will make so that {@code post_logout_redirect_uri} will be set to the base url for the client
139+
* application.
140+
*
141+
* @param postLogoutRedirectUri - A template for creating the {@code post_logout_redirect_uri}
142+
* query parameter
143+
* @since 5.3
144+
*/
145+
public void setPostLogoutRedirectUri(String postLogoutRedirectUri) {
103146
Assert.notNull(postLogoutRedirectUri, "postLogoutRedirectUri cannot be null");
104147
this.postLogoutRedirectUri = postLogoutRedirectUri;
105148
}

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandler.java

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,9 +18,11 @@
1818

1919
import java.net.URI;
2020
import java.nio.charset.StandardCharsets;
21+
import java.util.Collections;
2122

2223
import reactor.core.publisher.Mono;
2324

25+
import org.springframework.http.server.reactive.ServerHttpRequest;
2426
import org.springframework.security.core.Authentication;
2527
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
2628
import org.springframework.security.oauth2.client.registration.ClientRegistration;
@@ -32,6 +34,7 @@
3234
import org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler;
3335
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
3436
import org.springframework.util.Assert;
37+
import org.springframework.web.util.UriComponents;
3538
import org.springframework.web.util.UriComponentsBuilder;
3639

3740
/**
@@ -50,7 +53,7 @@ public class OidcClientInitiatedServerLogoutSuccessHandler
5053
= new RedirectServerLogoutSuccessHandler();
5154
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
5255

53-
private URI postLogoutRedirectUri;
56+
private String postLogoutRedirectUri;
5457

5558
/**
5659
* Constructs an {@link OidcClientInitiatedServerLogoutSuccessHandler} with the provided parameters
@@ -74,28 +77,40 @@ public Mono<Void> onLogoutSuccess(WebFilterExchange exchange, Authentication aut
7477
.filter(OAuth2AuthenticationToken.class::isInstance)
7578
.filter(token -> authentication.getPrincipal() instanceof OidcUser)
7679
.map(OAuth2AuthenticationToken.class::cast)
77-
.flatMap(this::endSessionEndpoint)
78-
.map(endSessionEndpoint -> endpointUri(endSessionEndpoint, authentication))
80+
.map(OAuth2AuthenticationToken::getAuthorizedClientRegistrationId)
81+
.flatMap(this.clientRegistrationRepository::findByRegistrationId)
82+
.flatMap(clientRegistration -> {
83+
URI endSessionEndpoint = endSessionEndpoint(clientRegistration);
84+
if (endSessionEndpoint == null) {
85+
return Mono.empty();
86+
}
87+
String idToken = idToken(authentication);
88+
URI postLogoutRedirectUri = postLogoutRedirectUri(exchange.getExchange().getRequest());
89+
return Mono.just(endpointUri(endSessionEndpoint, idToken, postLogoutRedirectUri));
90+
})
7991
.switchIfEmpty(this.serverLogoutSuccessHandler
8092
.onLogoutSuccess(exchange, authentication).then(Mono.empty()))
8193
.flatMap(endpointUri -> this.redirectStrategy.sendRedirect(exchange.getExchange(), endpointUri));
8294
}
8395

84-
private Mono<URI> endSessionEndpoint(OAuth2AuthenticationToken token) {
85-
String registrationId = token.getAuthorizedClientRegistrationId();
86-
return this.clientRegistrationRepository.findByRegistrationId(registrationId)
87-
.map(ClientRegistration::getProviderDetails)
88-
.map(ClientRegistration.ProviderDetails::getConfigurationMetadata)
89-
.flatMap(configurationMetadata -> Mono.justOrEmpty(configurationMetadata.get("end_session_endpoint")))
90-
.map(Object::toString)
91-
.map(URI::create);
96+
private URI endSessionEndpoint(ClientRegistration clientRegistration) {
97+
URI result = null;
98+
if (clientRegistration != null) {
99+
Object endSessionEndpoint = clientRegistration.getProviderDetails().getConfigurationMetadata()
100+
.get("end_session_endpoint");
101+
if (endSessionEndpoint != null) {
102+
result = URI.create(endSessionEndpoint.toString());
103+
}
104+
}
105+
106+
return result;
92107
}
93108

94-
private URI endpointUri(URI endSessionEndpoint, Authentication authentication) {
109+
private URI endpointUri(URI endSessionEndpoint, String idToken, URI postLogoutRedirectUri) {
95110
UriComponentsBuilder builder = UriComponentsBuilder.fromUri(endSessionEndpoint);
96-
builder.queryParam("id_token_hint", idToken(authentication));
97-
if (this.postLogoutRedirectUri != null) {
98-
builder.queryParam("post_logout_redirect_uri", this.postLogoutRedirectUri);
111+
builder.queryParam("id_token_hint", idToken);
112+
if (postLogoutRedirectUri != null) {
113+
builder.queryParam("post_logout_redirect_uri", postLogoutRedirectUri);
99114
}
100115
return builder.encode(StandardCharsets.UTF_8).build().toUri();
101116
}
@@ -104,13 +119,49 @@ private String idToken(Authentication authentication) {
104119
return ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue();
105120
}
106121

122+
private URI postLogoutRedirectUri(ServerHttpRequest request) {
123+
if (this.postLogoutRedirectUri == null) {
124+
return null;
125+
}
126+
UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI())
127+
.replacePath(request.getPath().contextPath().value())
128+
.replaceQuery(null)
129+
.fragment(null)
130+
.build();
131+
return UriComponentsBuilder.fromUriString(this.postLogoutRedirectUri)
132+
.buildAndExpand(Collections.singletonMap("baseUrl", uriComponents.toUriString()))
133+
.toUri();
134+
}
135+
107136
/**
108137
* Set the post logout redirect uri to use
109138
*
110139
* @param postLogoutRedirectUri - A valid URL to which the OP should redirect after logging out the user
140+
* @deprecated {@link #setPostLogoutRedirectUri(String)}
111141
*/
142+
@Deprecated
112143
public void setPostLogoutRedirectUri(URI postLogoutRedirectUri) {
113144
Assert.notNull(postLogoutRedirectUri, "postLogoutRedirectUri cannot be empty");
145+
this.postLogoutRedirectUri = postLogoutRedirectUri.toASCIIString();
146+
}
147+
148+
/**
149+
* Set the post logout redirect uri template to use. Supports the {@code "{baseUrl}"}
150+
* placeholder, for example:
151+
*
152+
* <pre>
153+
* handler.setPostLogoutRedirectUriTemplate("{baseUrl}");
154+
* </pre>
155+
*
156+
* will make so that {@code post_logout_redirect_uri} will be set to the base url for the client
157+
* application.
158+
*
159+
* @param postLogoutRedirectUri - A template for creating the {@code post_logout_redirect_uri}
160+
* query parameter
161+
* @since 5.3
162+
*/
163+
public void setPostLogoutRedirectUri(String postLogoutRedirectUri) {
164+
Assert.notNull(postLogoutRedirectUri, "postLogoutRedirectUri cannot be null");
114165
this.postLogoutRedirectUri = postLogoutRedirectUri;
115166
}
116167

oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandlerTests.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -145,9 +145,35 @@ public void logoutWhenUsingPostLogoutRedirectUriThenIncludesItInRedirect()
145145
"post_logout_redirect_uri=https://postlogout?encodedparam%3Dvalue");
146146
}
147147

148+
@Test
149+
public void logoutWhenUsingPostLogoutRedirectUriTemplateThenBuildsItForRedirect()
150+
throws IOException, ServletException {
151+
152+
OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(
153+
TestOidcUsers.create(),
154+
AuthorityUtils.NO_AUTHORITIES,
155+
this.registration.getRegistrationId());
156+
this.handler.setPostLogoutRedirectUri("{baseUrl}");
157+
this.request.setScheme("https");
158+
this.request.setServerPort(443);
159+
this.request.setServerName("rp.example.org");
160+
this.request.setUserPrincipal(token);
161+
this.handler.onLogoutSuccess(this.request, this.response, token);
162+
163+
assertThat(this.response.getRedirectedUrl()).isEqualTo("https://endpoint?" +
164+
"id_token_hint=id-token&" +
165+
"post_logout_redirect_uri=https://rp.example.org");
166+
}
167+
148168
@Test
149169
public void setPostLogoutRedirectUriWhenGivenNullThenThrowsException() {
150-
assertThatThrownBy(() -> this.handler.setPostLogoutRedirectUri(null))
170+
assertThatThrownBy(() -> this.handler.setPostLogoutRedirectUri((URI) null))
171+
.isInstanceOf(IllegalArgumentException.class);
172+
}
173+
174+
@Test
175+
public void setPostLogoutRedirectUriTemplateWhenGivenNullThenThrowsException() {
176+
assertThatThrownBy(() -> this.handler.setPostLogoutRedirectUri((String) null))
151177
.isInstanceOf(IllegalArgumentException.class);
152178
}
153179
}

oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandlerTests.java

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,8 +16,10 @@
1616

1717
package org.springframework.security.oauth2.client.oidc.web.server.logout;
1818

19+
import java.io.IOException;
1920
import java.net.URI;
2021
import java.util.Collections;
22+
import javax.servlet.ServletException;
2123

2224
import org.junit.Before;
2325
import org.junit.Test;
@@ -154,9 +156,37 @@ public void logoutWhenUsingPostLogoutRedirectUriThenIncludesItInRedirect() {
154156
"post_logout_redirect_uri=https://postlogout?encodedparam%3Dvalue");
155157
}
156158

159+
@Test
160+
public void logoutWhenUsingPostLogoutRedirectUriTemplateThenBuildsItForRedirect()
161+
throws IOException, ServletException {
162+
163+
OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(
164+
TestOidcUsers.create(),
165+
AuthorityUtils.NO_AUTHORITIES,
166+
this.registration.getRegistrationId());
167+
when(this.exchange.getPrincipal()).thenReturn(Mono.just(token));
168+
MockServerHttpRequest request = MockServerHttpRequest.get("https://rp.example.org/").build();
169+
when(this.exchange.getRequest()).thenReturn(request);
170+
WebFilterExchange f = new WebFilterExchange(exchange, this.chain);
171+
172+
this.handler.setPostLogoutRedirectUri("{baseUrl}");
173+
this.handler.onLogoutSuccess(f, token).block();
174+
175+
assertThat(redirectedUrl(this.exchange))
176+
.isEqualTo("https://endpoint?" +
177+
"id_token_hint=id-token&" +
178+
"post_logout_redirect_uri=https://rp.example.org");
179+
}
180+
157181
@Test
158182
public void setPostLogoutRedirectUriWhenGivenNullThenThrowsException() {
159-
assertThatThrownBy(() -> this.handler.setPostLogoutRedirectUri(null))
183+
assertThatThrownBy(() -> this.handler.setPostLogoutRedirectUri((URI) null))
184+
.isInstanceOf(IllegalArgumentException.class);
185+
}
186+
187+
@Test
188+
public void setPostLogoutRedirectUriTemplateWhenGivenNullThenThrowsException() {
189+
assertThatThrownBy(() -> this.handler.setPostLogoutRedirectUri((String) null))
160190
.isInstanceOf(IllegalArgumentException.class);
161191
}
162192

0 commit comments

Comments
 (0)