Skip to content

Commit 44033cd

Browse files
committed
Make Internal Logout URI Configurable
Closes gh-14609
1 parent e18ec48 commit 44033cd

File tree

8 files changed

+288
-32
lines changed

8 files changed

+288
-32
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandler.java

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.IOException;
2020
import java.util.ArrayList;
2121
import java.util.Collection;
22+
import java.util.HashMap;
2223
import java.util.Map;
2324

2425
import jakarta.servlet.http.HttpServletRequest;
@@ -37,10 +38,12 @@
3738
import org.springframework.security.oauth2.core.OAuth2Error;
3839
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
3940
import org.springframework.security.web.authentication.logout.LogoutHandler;
41+
import org.springframework.security.web.util.UrlUtils;
4042
import org.springframework.util.Assert;
4143
import org.springframework.web.client.RestClientException;
4244
import org.springframework.web.client.RestOperations;
4345
import org.springframework.web.client.RestTemplate;
46+
import org.springframework.web.util.UriComponents;
4447
import org.springframework.web.util.UriComponentsBuilder;
4548

4649
/**
@@ -61,7 +64,7 @@ final class OidcBackChannelLogoutHandler implements LogoutHandler {
6164

6265
private RestOperations restOperations = new RestTemplate();
6366

64-
private String logoutEndpointName = "/logout";
67+
private String logoutUri = "{baseScheme}://localhost{basePort}/logout";
6568

6669
private String sessionCookieName = "JSESSIONID";
6770

@@ -112,12 +115,32 @@ private void eachLogout(HttpServletRequest request, OidcSessionInformation sessi
112115
}
113116

114117
String computeLogoutEndpoint(HttpServletRequest request) {
115-
String url = request.getRequestURL().toString();
116-
return UriComponentsBuilder.fromHttpUrl(url)
117-
.host("localhost")
118-
.replacePath(this.logoutEndpointName)
119-
.build()
120-
.toUriString();
118+
// @formatter:off
119+
UriComponents uriComponents = UriComponentsBuilder
120+
.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
121+
.replacePath(request.getContextPath())
122+
.replaceQuery(null)
123+
.fragment(null)
124+
.build();
125+
126+
Map<String, String> uriVariables = new HashMap<>();
127+
String scheme = uriComponents.getScheme();
128+
uriVariables.put("baseScheme", (scheme != null) ? scheme : "");
129+
uriVariables.put("baseUrl", uriComponents.toUriString());
130+
131+
String host = uriComponents.getHost();
132+
uriVariables.put("baseHost", (host != null) ? host : "");
133+
134+
String path = uriComponents.getPath();
135+
uriVariables.put("basePath", (path != null) ? path : "");
136+
137+
int port = uriComponents.getPort();
138+
uriVariables.put("basePort", (port == -1) ? "" : ":" + port);
139+
140+
return UriComponentsBuilder.fromUriString(this.logoutUri)
141+
.buildAndExpand(uriVariables)
142+
.toUriString();
143+
// @formatter:on
121144
}
122145

123146
private OAuth2Error oauth2Error(Collection<String> errors) {
@@ -164,7 +187,7 @@ void setRestOperations(RestOperations restOperations) {
164187
*/
165188
void setLogoutUri(String logoutUri) {
166189
Assert.hasText(logoutUri, "logoutUri cannot be empty");
167-
this.logoutEndpointName = logoutUri;
190+
this.logoutUri = logoutUri;
168191
}
169192

170193
/**

config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurer.java

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
1818

1919
import java.util.function.Consumer;
20+
import java.util.function.Function;
2021

2122
import org.springframework.security.authentication.AuthenticationManager;
2223
import org.springframework.security.authentication.ProviderManager;
@@ -123,7 +124,7 @@ public final class BackChannelLogoutConfigurer {
123124
private final AuthenticationManager authenticationManager = new ProviderManager(
124125
new OidcBackChannelLogoutAuthenticationProvider());
125126

126-
private LogoutHandler logoutHandler;
127+
private Function<B, LogoutHandler> logoutHandler = this::logoutHandler;
127128

128129
private AuthenticationConverter authenticationConverter(B http) {
129130
if (this.authenticationConverter == null) {
@@ -139,18 +140,54 @@ private AuthenticationManager authenticationManager() {
139140
}
140141

141142
private LogoutHandler logoutHandler(B http) {
142-
if (this.logoutHandler == null) {
143+
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
144+
logoutHandler.setSessionRegistry(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
145+
return logoutHandler;
146+
}
147+
148+
/**
149+
* Use this endpoint when invoking a back-channel logout.
150+
*
151+
* <p>
152+
* The resulting {@link LogoutHandler} will {@code POST} the session cookie and
153+
* CSRF token to this endpoint to invalidate the corresponding end-user session.
154+
*
155+
* <p>
156+
* Supports URI templates like {@code {baseUrl}}, {@code {baseScheme}}, and
157+
* {@code {basePort}}.
158+
*
159+
* <p>
160+
* By default, the URI is set to
161+
* {@code {baseScheme}://localhost{basePort}/logout}, meaning that the scheme and
162+
* port of the original back-channel request is preserved, while the host and
163+
* endpoint are changed.
164+
*
165+
* <p>
166+
* If you are using Spring Security for the logout endpoint, the path part of this
167+
* URI should match the value configured there.
168+
*
169+
* <p>
170+
* Otherwise, this is handy in the event that your server configuration means that
171+
* the scheme, server name, or port in the {@code Host} header are different from
172+
* how you would address the same server internally.
173+
* @param logoutUri the URI to request logout on the back-channel
174+
* @return the {@link BackChannelLogoutConfigurer} for further customizations
175+
* @since 6.2.4
176+
*/
177+
public BackChannelLogoutConfigurer logoutUri(String logoutUri) {
178+
this.logoutHandler = (http) -> {
143179
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
144180
logoutHandler.setSessionRegistry(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
145-
this.logoutHandler = logoutHandler;
146-
}
147-
return this.logoutHandler;
181+
logoutHandler.setLogoutUri(logoutUri);
182+
return logoutHandler;
183+
};
184+
return this;
148185
}
149186

150187
void configure(B http) {
151188
OidcBackChannelLogoutFilter filter = new OidcBackChannelLogoutFilter(authenticationConverter(http),
152189
authenticationManager());
153-
filter.setLogoutHandler(logoutHandler(http));
190+
filter.setLogoutHandler(this.logoutHandler.apply(http));
154191
http.addFilterBefore(filter, CsrfFilter.class);
155192
}
156193

config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandler.java

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.nio.charset.StandardCharsets;
2020
import java.util.Collection;
21+
import java.util.HashMap;
2122
import java.util.Map;
2223
import java.util.concurrent.atomic.AtomicInteger;
2324

@@ -30,6 +31,7 @@
3031
import org.springframework.core.io.buffer.DataBuffer;
3132
import org.springframework.http.HttpHeaders;
3233
import org.springframework.http.ResponseEntity;
34+
import org.springframework.http.server.reactive.ServerHttpRequest;
3335
import org.springframework.http.server.reactive.ServerHttpResponse;
3436
import org.springframework.security.core.Authentication;
3537
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
@@ -42,6 +44,7 @@
4244
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
4345
import org.springframework.util.Assert;
4446
import org.springframework.web.reactive.function.client.WebClient;
47+
import org.springframework.web.util.UriComponents;
4548
import org.springframework.web.util.UriComponentsBuilder;
4649

4750
/**
@@ -62,7 +65,7 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
6265

6366
private WebClient web = WebClient.create();
6467

65-
private String logoutEndpointName = "/logout";
68+
private String logoutUri = "{baseScheme}://localhost{basePort}/logout";
6669

6770
private String sessionCookieName = "SESSION";
6871

@@ -108,17 +111,36 @@ private Mono<ResponseEntity<Void>> eachLogout(WebFilterExchange exchange, OidcSe
108111
for (Map.Entry<String, String> credential : session.getAuthorities().entrySet()) {
109112
headers.add(credential.getKey(), credential.getValue());
110113
}
111-
String logout = computeLogoutEndpoint(exchange);
114+
String logout = computeLogoutEndpoint(exchange.getExchange().getRequest());
112115
return this.web.post().uri(logout).headers((h) -> h.putAll(headers)).retrieve().toBodilessEntity();
113116
}
114117

115-
String computeLogoutEndpoint(WebFilterExchange exchange) {
116-
String url = exchange.getExchange().getRequest().getURI().toString();
117-
return UriComponentsBuilder.fromHttpUrl(url)
118-
.host("localhost")
119-
.replacePath(this.logoutEndpointName)
120-
.build()
121-
.toUriString();
118+
String computeLogoutEndpoint(ServerHttpRequest request) {
119+
// @formatter:off
120+
UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI())
121+
.replacePath(request.getPath().contextPath().value())
122+
.replaceQuery(null)
123+
.fragment(null)
124+
.build();
125+
126+
Map<String, String> uriVariables = new HashMap<>();
127+
String scheme = uriComponents.getScheme();
128+
uriVariables.put("baseScheme", (scheme != null) ? scheme : "");
129+
uriVariables.put("baseUrl", uriComponents.toUriString());
130+
131+
String host = uriComponents.getHost();
132+
uriVariables.put("baseHost", (host != null) ? host : "");
133+
134+
String path = uriComponents.getPath();
135+
uriVariables.put("basePath", (path != null) ? path : "");
136+
137+
int port = uriComponents.getPort();
138+
uriVariables.put("basePort", (port == -1) ? "" : ":" + port);
139+
140+
return UriComponentsBuilder.fromUriString(this.logoutUri)
141+
.buildAndExpand(uriVariables)
142+
.toUriString();
143+
// @formatter:on
122144
}
123145

124146
private OAuth2Error oauth2Error(Collection<?> errors) {
@@ -168,7 +190,7 @@ void setWebClient(WebClient web) {
168190
*/
169191
void setLogoutUri(String logoutUri) {
170192
Assert.hasText(logoutUri, "logoutUri cannot be empty");
171-
this.logoutEndpointName = logoutUri;
193+
this.logoutUri = logoutUri;
172194
}
173195

174196
/**

config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
5959
import org.springframework.security.authorization.ReactiveAuthorizationManager;
6060
import org.springframework.security.config.Customizer;
61+
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer;
6162
import org.springframework.security.core.Authentication;
6263
import org.springframework.security.core.GrantedAuthority;
6364
import org.springframework.security.core.authority.AuthorityUtils;
@@ -110,6 +111,7 @@
110111
import org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint;
111112
import org.springframework.security.oauth2.server.resource.web.server.authentication.ServerBearerTokenAuthenticationConverter;
112113
import org.springframework.security.web.PortMapper;
114+
import org.springframework.security.web.authentication.logout.LogoutHandler;
113115
import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor;
114116
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
115117
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
@@ -5074,7 +5076,7 @@ public final class BackChannelLogoutConfigurer {
50745076

50755077
private final ReactiveAuthenticationManager authenticationManager = new OidcBackChannelLogoutReactiveAuthenticationManager();
50765078

5077-
private ServerLogoutHandler logoutHandler;
5079+
private Supplier<ServerLogoutHandler> logoutHandler = this::logoutHandler;
50785080

50795081
private ServerAuthenticationConverter authenticationConverter() {
50805082
if (this.authenticationConverter == null) {
@@ -5089,18 +5091,56 @@ private ReactiveAuthenticationManager authenticationManager() {
50895091
}
50905092

50915093
private ServerLogoutHandler logoutHandler() {
5092-
if (this.logoutHandler == null) {
5094+
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
5095+
logoutHandler.setSessionRegistry(OidcLogoutSpec.this.getSessionRegistry());
5096+
return logoutHandler;
5097+
}
5098+
5099+
/**
5100+
* Use this endpoint when invoking a back-channel logout.
5101+
*
5102+
* <p>
5103+
* The resulting {@link LogoutHandler} will {@code POST} the session cookie
5104+
* and CSRF token to this endpoint to invalidate the corresponding end-user
5105+
* session.
5106+
*
5107+
* <p>
5108+
* Supports URI templates like {@code {baseUrl}}, {@code {baseScheme}}, and
5109+
* {@code {basePort}}.
5110+
*
5111+
* <p>
5112+
* By default, the URI is set to
5113+
* {@code {baseScheme}://localhost{basePort}/logout}, meaning that the scheme
5114+
* and port of the original back-channel request is preserved, while the host
5115+
* and endpoint are changed.
5116+
*
5117+
* <p>
5118+
* If you are using Spring Security for the logout endpoint, the path part of
5119+
* this URI should match the value configured there.
5120+
*
5121+
* <p>
5122+
* Otherwise, this is handy in the event that your server configuration means
5123+
* that the scheme, server name, or port in the {@code Host} header are
5124+
* different from how you would address the same server internally.
5125+
* @param logoutUri the URI to request logout on the back-channel
5126+
* @return the {@link OidcLogoutConfigurer.BackChannelLogoutConfigurer} for
5127+
* further customizations
5128+
* @since 6.2.4
5129+
*/
5130+
public BackChannelLogoutConfigurer logoutUri(String logoutUri) {
5131+
this.logoutHandler = () -> {
50935132
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
50945133
logoutHandler.setSessionRegistry(OidcLogoutSpec.this.getSessionRegistry());
5095-
this.logoutHandler = logoutHandler;
5096-
}
5097-
return this.logoutHandler;
5134+
logoutHandler.setLogoutUri(logoutUri);
5135+
return logoutHandler;
5136+
};
5137+
return this;
50985138
}
50995139

51005140
void configure(ServerHttpSecurity http) {
51015141
OidcBackChannelLogoutWebFilter filter = new OidcBackChannelLogoutWebFilter(authenticationConverter(),
51025142
authenticationManager());
5103-
filter.setLogoutHandler(logoutHandler());
5143+
filter.setLogoutHandler(this.logoutHandler.get());
51045144
http.addFilterBefore(filter, SecurityWebFiltersOrder.CSRF);
51055145
}
51065146

config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandlerTests.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,28 @@ public void computeLogoutEndpointWhenDifferentHostnameThenLocalhost() {
3535
assertThat(endpoint).isEqualTo("http://localhost:8090/logout");
3636
}
3737

38+
@Test
39+
public void computeLogoutEndpointWhenUsingBaseUrlTemplateThenServerName() {
40+
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
41+
logoutHandler.setLogoutUri("{baseUrl}/logout");
42+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout");
43+
request.setServerName("host.docker.internal");
44+
request.setServerPort(8090);
45+
String endpoint = logoutHandler.computeLogoutEndpoint(request);
46+
assertThat(endpoint).isEqualTo("http://host.docker.internal:8090/logout");
47+
}
48+
49+
// gh-14609
50+
@Test
51+
public void computeLogoutEndpointWhenLogoutUriThenUses() {
52+
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
53+
logoutHandler.setLogoutUri("http://localhost:8090/logout");
54+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout");
55+
request.setScheme("https");
56+
request.setServerName("server-one.com");
57+
request.setServerPort(80);
58+
String endpoint = logoutHandler.computeLogoutEndpoint(request);
59+
assertThat(endpoint).isEqualTo("http://localhost:8090/logout");
60+
}
61+
3862
}

0 commit comments

Comments
 (0)