body = new LinkedMultiValueMap<>();
+ body.add("logout_token", token.getPrincipal().getTokenValue());
+ body.add("_spring_security_internal_logout", "true");
+ return this.web.post()
+ .uri(logout)
+ .headers((h) -> h.putAll(headers))
+ .body(BodyInserters.fromFormData(body))
+ .retrieve()
+ .toBodilessEntity();
}
- String computeLogoutEndpoint(ServerHttpRequest request) {
+ String computeLogoutEndpoint(ServerHttpRequest request, OidcBackChannelLogoutAuthentication token) {
// @formatter:off
UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI())
.replacePath(request.getPath().contextPath().value())
@@ -137,6 +150,9 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
int port = uriComponents.getPort();
uriVariables.put("basePort", (port == -1) ? "" : ":" + port);
+ String registrationId = token.getClientRegistration().getRegistrationId();
+ uriVariables.put("registrationId", registrationId);
+
return UriComponentsBuilder.fromUriString(this.logoutUri)
.buildAndExpand(uriVariables)
.toUriString();
@@ -161,34 +177,13 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
return response.writeWith(Flux.just(buffer));
}
- /**
- * Use this {@link OidcSessionRegistry} to identify sessions to invalidate. Note that
- * this class uses
- * {@link OidcSessionRegistry#removeSessionInformation(OidcLogoutToken)} to identify
- * sessions.
- * @param sessionRegistry the {@link OidcSessionRegistry} to use
- */
- void setSessionRegistry(ReactiveOidcSessionRegistry sessionRegistry) {
- Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
- this.sessionRegistry = sessionRegistry;
- }
-
- /**
- * Use this {@link WebClient} to perform the per-session back-channel logout
- * @param web the {@link WebClient} to use
- */
- void setWebClient(WebClient web) {
- Assert.notNull(web, "web cannot be null");
- this.web = web;
- }
-
/**
* Use this logout URI for performing per-session logout. Defaults to {@code /logout}
* since that is the default URI for
* {@link org.springframework.security.web.authentication.logout.LogoutFilter}.
* @param logoutUri the URI to use
*/
- void setLogoutUri(String logoutUri) {
+ public void setLogoutUri(String logoutUri) {
Assert.hasText(logoutUri, "logoutUri cannot be empty");
this.logoutUri = logoutUri;
}
@@ -200,7 +195,7 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
* Note that if you are using Spring Session, this likely needs to change to SESSION.
* @param sessionCookieName the cookie name to use
*/
- void setSessionCookieName(String sessionCookieName) {
+ public void setSessionCookieName(String sessionCookieName) {
Assert.hasText(sessionCookieName, "clientSessionCookieName cannot be empty");
this.sessionCookieName = sessionCookieName;
}
diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
index ebec42a6ef..b6000b7322 100644
--- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
+++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
@@ -58,7 +58,6 @@ import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.config.Customizer;
-import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
@@ -5529,8 +5528,12 @@ public class ServerHttpSecurity {
}
private ServerLogoutHandler logoutHandler() {
- OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
- logoutHandler.setSessionRegistry(OidcLogoutSpec.this.getSessionRegistry());
+ OidcBackChannelServerLogoutHandler logoutHandler = getBeanOrNull(
+ OidcBackChannelServerLogoutHandler.class);
+ if (logoutHandler != null) {
+ return logoutHandler;
+ }
+ logoutHandler = new OidcBackChannelServerLogoutHandler(OidcLogoutSpec.this.getSessionRegistry());
return logoutHandler;
}
@@ -5548,9 +5551,9 @@ public class ServerHttpSecurity {
*
*
* By default, the URI is set to
- * {@code {baseScheme}://localhost{basePort}/logout}, meaning that the scheme
- * and port of the original back-channel request is preserved, while the host
- * and endpoint are changed.
+ * {@code {baseUrl}/logout/connect/back-channel/{registrationId}}, meaning
+ * that the scheme and port of the original back-channel request is preserved,
+ * while the host and endpoint are changed.
*
*
* If you are using Spring Security for the logout endpoint, the path part of
@@ -5561,27 +5564,135 @@ public class ServerHttpSecurity {
* that the scheme, server name, or port in the {@code Host} header are
* different from how you would address the same server internally.
* @param logoutUri the URI to request logout on the back-channel
- * @return the {@link OidcLogoutConfigurer.BackChannelLogoutConfigurer} for
- * further customizations
+ * @return the {@link BackChannelLogoutConfigurer} for further customizations
* @since 6.2.4
*/
public BackChannelLogoutConfigurer logoutUri(String logoutUri) {
this.logoutHandler = () -> {
- OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
- logoutHandler.setSessionRegistry(OidcLogoutSpec.this.getSessionRegistry());
+ OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(
+ OidcLogoutSpec.this.getSessionRegistry());
logoutHandler.setLogoutUri(logoutUri);
return logoutHandler;
};
return this;
}
+ /**
+ * Configure what and how per-session logout will be performed.
+ *
+ *
+ * This overrides any value given to {@link #logoutUri(String)}
+ *
+ *
+ * By default, the resulting {@link LogoutHandler} will {@code POST} the
+ * session cookie and OIDC logout token back to the original back-channel
+ * logout endpoint.
+ *
+ *
+ * Using this method changes the underlying default that {@code POST}s the
+ * session cookie and CSRF token to your application's {@code /logout}
+ * endpoint. As such, it is recommended to call this instead of accepting the
+ * {@code /logout} default as this does not require any special CSRF
+ * configuration, even if you don't require other changes.
+ *
+ *
+ * For example, configuring Back-Channel Logout in the following way:
+ *
+ *
+ * http
+ * .oidcLogout((oidc) -> oidc
+ * .backChannel((backChannel) -> backChannel
+ * .logoutHandler(new OidcBackChannelServerLogoutHandler())
+ * )
+ * );
+ *
+ *
+ * will make so that the per-session logout invocation no longer requires
+ * special CSRF configurations.
+ *
+ *
+ * The default URI is
+ * {@code {baseUrl}/logout/connect/back-channel/{registrationId}}, which is
+ * simply an internal version of the same endpoint exposed to your
+ * Back-Channel services. You can use
+ * {@link OidcBackChannelServerLogoutHandler#setLogoutUri(String)} to alter
+ * the scheme, server name, or port in the {@code Host} header to accommodate
+ * how your application would address itself internally.
+ *
+ *
+ * For example, if the way your application would internally call itself is on
+ * a different scheme and port than incoming traffic, you can configure the
+ * endpoint in the following way:
+ *
+ *
+ * http
+ * .oidcLogout((oidc) -> oidc
+ * .backChannel((backChannel) -> backChannel
+ * .logoutUri("http://localhost:9000/logout/connect/back-channel/{registrationId}")
+ * )
+ * );
+ *
+ *
+ *
+ * You can also publish it as a {@code @Bean} as follows:
+ *
+ *
+ * @Bean
+ * OidcBackChannelServerLogoutHandler oidcLogoutHandler() {
+ * OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
+ * logoutHandler.setLogoutUri("http://localhost:9000/logout/connect/back-channel/{registrationId}");
+ * return logoutHandler;
+ * }
+ *
+ *
+ * to have the same effect.
+ * @param logoutHandler the {@link ServerLogoutHandler} to use each individual
+ * session
+ * @return {@link BackChannelLogoutConfigurer} for further customizations
+ * @since 6.4
+ */
+ public BackChannelLogoutConfigurer logoutHandler(ServerLogoutHandler logoutHandler) {
+ this.logoutHandler = () -> logoutHandler;
+ return this;
+ }
+
void configure(ServerHttpSecurity http) {
+ ServerLogoutHandler oidcLogout = this.logoutHandler.get();
+ ServerLogoutHandler sessionLogout = new SecurityContextServerLogoutHandler();
+ LogoutSpec logout = ServerHttpSecurity.this.logout;
+ if (logout != null) {
+ sessionLogout = new DelegatingServerLogoutHandler(logout.logoutHandlers);
+ }
OidcBackChannelLogoutWebFilter filter = new OidcBackChannelLogoutWebFilter(authenticationConverter(),
- authenticationManager());
- filter.setLogoutHandler(this.logoutHandler.get());
+ authenticationManager(), new EitherLogoutHandler(oidcLogout, sessionLogout));
http.addFilterBefore(filter, SecurityWebFiltersOrder.CSRF);
}
+ private static final class EitherLogoutHandler implements ServerLogoutHandler {
+
+ private final ServerLogoutHandler left;
+
+ private final ServerLogoutHandler right;
+
+ EitherLogoutHandler(ServerLogoutHandler left, ServerLogoutHandler right) {
+ this.left = left;
+ this.right = right;
+ }
+
+ @Override
+ public Mono logout(WebFilterExchange exchange, Authentication authentication) {
+ return exchange.getExchange().getFormData().flatMap((data) -> {
+ if (data.getFirst("_spring_security_internal_logout") == null) {
+ return this.left.logout(exchange, authentication);
+ }
+ else {
+ return this.right.logout(exchange, authentication);
+ }
+ });
+ }
+
+ }
+
}
}
diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDsl.kt
index f9fdd7dc4d..27532b4c01 100644
--- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDsl.kt
+++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDsl.kt
@@ -1,3 +1,4 @@
+
/*
* Copyright 2002-2023 the original author or authors.
*
@@ -72,4 +73,5 @@ class OidcLogoutDsl {
backChannel?.also { oidcLogout.backChannel(backChannel) }
}
}
+
}
diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcBackChannelLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcBackChannelLogoutDsl.kt
index efac77a566..f23b14d981 100644
--- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcBackChannelLogoutDsl.kt
+++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcBackChannelLogoutDsl.kt
@@ -18,6 +18,7 @@ package org.springframework.security.config.annotation.web.oauth2.login
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer
+import org.springframework.security.web.authentication.logout.LogoutHandler
/**
* A Kotlin DSL to configure the OIDC 1.0 Back-Channel configuration using
@@ -28,7 +29,26 @@ import org.springframework.security.config.annotation.web.configurers.oauth2.cli
*/
@OAuth2LoginSecurityMarker
class OidcBackChannelLogoutDsl {
+ private var _logoutUri: String? = null
+ private var _logoutHandler: LogoutHandler? = null
+
+ var logoutHandler: LogoutHandler?
+ get() = _logoutHandler
+ set(value) {
+ _logoutHandler = value
+ _logoutUri = null
+ }
+ var logoutUri: String?
+ get() = _logoutUri
+ set(value) {
+ _logoutUri = value
+ _logoutHandler = null
+ }
+
internal fun get(): (OidcLogoutConfigurer.BackChannelLogoutConfigurer) -> Unit {
- return { backChannel -> }
+ return { backChannel ->
+ logoutHandler?.also { backChannel.logoutHandler(logoutHandler) }
+ logoutUri?.also { backChannel.logoutUri(logoutUri) }
+ }
}
}
diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcBackChannelLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcBackChannelLogoutDsl.kt
index 5a245e5092..ba6b15da93 100644
--- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcBackChannelLogoutDsl.kt
+++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcBackChannelLogoutDsl.kt
@@ -16,6 +16,8 @@
package org.springframework.security.config.web.server
+import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler
+
/**
* A Kotlin DSL to configure [ServerHttpSecurity] OIDC 1.0 Back-Channel Logout support using idiomatic Kotlin code.
*
@@ -24,7 +26,26 @@ package org.springframework.security.config.web.server
*/
@ServerSecurityMarker
class ServerOidcBackChannelLogoutDsl {
+ private var _logoutUri: String? = null
+ private var _logoutHandler: ServerLogoutHandler? = null
+
+ var logoutHandler: ServerLogoutHandler?
+ get() = _logoutHandler
+ set(value) {
+ _logoutHandler = value
+ _logoutUri = null
+ }
+ var logoutUri: String?
+ get() = _logoutUri
+ set(value) {
+ _logoutUri = value
+ _logoutHandler = null
+ }
+
internal fun get(): (ServerHttpSecurity.OidcLogoutSpec.BackChannelLogoutConfigurer) -> Unit {
- return { backChannel -> }
+ return { backChannel ->
+ logoutHandler?.also { backChannel.logoutHandler(logoutHandler) }
+ logoutUri?.also { backChannel.logoutUri(logoutUri) }
+ }
}
}
diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDsl.kt
index 503a5b0c84..7c27e3e081 100644
--- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDsl.kt
+++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDsl.kt
@@ -47,7 +47,9 @@ class ServerOidcLogoutDsl {
* return http {
* oauth2Login { }
* oidcLogout {
- * backChannel { }
+ * backChannel {
+ * sessionLogout { }
+ * }
* }
* }
* }
diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandlerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandlerTests.java
index 71595ac7aa..9dc542a406 100644
--- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandlerTests.java
+++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandlerTests.java
@@ -19,44 +19,55 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.cl
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.security.oauth2.client.oidc.authentication.logout.TestOidcLogoutTokens;
+import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
+import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
+import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import static org.assertj.core.api.Assertions.assertThat;
public class OidcBackChannelLogoutHandlerTests {
+ private final OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
+
+ private final OidcBackChannelLogoutAuthentication token = new OidcBackChannelLogoutAuthentication(
+ TestOidcLogoutTokens.withSubject("issuer", "subject").build(),
+ TestClientRegistrations.clientRegistration().build());
+
// gh-14553
@Test
public void computeLogoutEndpointWhenDifferentHostnameThenLocalhost() {
- OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
+ OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(this.sessionRegistry);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout");
+ logoutHandler.setLogoutUri("{baseScheme}://localhost{basePort}/logout");
request.setServerName("host.docker.internal");
request.setServerPort(8090);
- String endpoint = logoutHandler.computeLogoutEndpoint(request);
- assertThat(endpoint).isEqualTo("http://localhost:8090/logout");
+ String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token);
+ assertThat(endpoint).startsWith("http://localhost:8090/logout");
}
@Test
public void computeLogoutEndpointWhenUsingBaseUrlTemplateThenServerName() {
- OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
+ OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(this.sessionRegistry);
logoutHandler.setLogoutUri("{baseUrl}/logout");
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout");
request.setServerName("host.docker.internal");
request.setServerPort(8090);
- String endpoint = logoutHandler.computeLogoutEndpoint(request);
- assertThat(endpoint).isEqualTo("http://host.docker.internal:8090/logout");
+ String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token);
+ assertThat(endpoint).startsWith("http://host.docker.internal:8090/logout");
}
// gh-14609
@Test
public void computeLogoutEndpointWhenLogoutUriThenUses() {
- OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
+ OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(this.sessionRegistry);
logoutHandler.setLogoutUri("http://localhost:8090/logout");
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout");
request.setScheme("https");
request.setServerName("server-one.com");
request.setServerPort(80);
- String endpoint = logoutHandler.computeLogoutEndpoint(request);
- assertThat(endpoint).isEqualTo("http://localhost:8090/logout");
+ String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token);
+ assertThat(endpoint).startsWith("http://localhost:8090/logout");
}
}
diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java
index 481dcced74..e1a6c357fa 100644
--- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java
+++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java
@@ -24,6 +24,7 @@ import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
@@ -91,6 +92,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.willThrow;
@@ -218,6 +220,40 @@ public class OidcLogoutConfigurerTests {
this.mvc.perform(get("/token/logout").session(one)).andExpect(status().isOk());
}
+ @Test
+ void logoutWhenSelfRemoteLogoutUriThenUses() throws Exception {
+ this.spring.register(WebServerConfig.class, OidcProviderConfig.class, SelfLogoutUriConfig.class).autowire();
+ String registrationId = this.clientRegistration.getRegistrationId();
+ MockHttpSession session = login();
+ String logoutToken = this.mvc.perform(get("/token/logout").session(session))
+ .andExpect(status().isOk())
+ .andReturn()
+ .getResponse()
+ .getContentAsString();
+ this.mvc
+ .perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
+ .param("logout_token", logoutToken))
+ .andExpect(status().isOk());
+ this.mvc.perform(get("/token/logout").session(session)).andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ void logoutWhenDifferentCookieNameThenUses() throws Exception {
+ this.spring.register(OidcProviderConfig.class, CookieConfig.class).autowire();
+ String registrationId = this.clientRegistration.getRegistrationId();
+ MockHttpSession session = login();
+ String logoutToken = this.mvc.perform(get("/token/logout").session(session))
+ .andExpect(status().isOk())
+ .andReturn()
+ .getResponse()
+ .getContentAsString();
+ this.mvc
+ .perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
+ .param("logout_token", logoutToken))
+ .andExpect(status().isOk());
+ this.mvc.perform(get("/token/logout").session(session)).andExpect(status().isUnauthorized());
+ }
+
@Test
void logoutWhenRemoteLogoutFailsThenReportsPartialLogout() throws Exception {
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, WithBrokenLogoutConfig.class).autowire();
@@ -355,6 +391,87 @@ public class OidcLogoutConfigurerTests {
}
+ @Configuration
+ @EnableWebSecurity
+ @Import(RegistrationConfig.class)
+ static class SelfLogoutUriConfig {
+
+ private final OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
+
+ @Bean
+ @Order(1)
+ SecurityFilterChain filters(HttpSecurity http) throws Exception {
+ // @formatter:off
+ http
+ .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
+ .oauth2Login((oauth2) -> oauth2.oidcSessionRegistry(this.sessionRegistry))
+ .oidcLogout((oidc) -> oidc
+ .backChannel(Customizer.withDefaults())
+ );
+ // @formatter:on
+
+ return http.build();
+ }
+
+ @Bean
+ OidcBackChannelLogoutHandler oidcLogoutHandler() {
+ return new OidcBackChannelLogoutHandler(this.sessionRegistry);
+ }
+
+ }
+
+ @Configuration
+ @EnableWebSecurity
+ @Import(RegistrationConfig.class)
+ static class CookieConfig {
+
+ private final MockWebServer server = new MockWebServer();
+
+ private final OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
+
+ @Bean
+ @Order(1)
+ SecurityFilterChain filters(HttpSecurity http) throws Exception {
+ // @formatter:off
+ http
+ .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
+ .oauth2Login((oauth2) -> oauth2.oidcSessionRegistry(this.sessionRegistry))
+ .oidcLogout((oidc) -> oidc
+ .backChannel(Customizer.withDefaults())
+ );
+ // @formatter:on
+
+ return http.build();
+ }
+
+ @Bean
+ OidcBackChannelLogoutHandler oidcLogoutHandler() {
+ OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(this.sessionRegistry);
+ logoutHandler.setSessionCookieName("SESSION");
+ return logoutHandler;
+ }
+
+ @Bean
+ MockWebServer web(ObjectProvider mvc) {
+ MockMvcDispatcher dispatcher = new MockMvcDispatcher(mvc);
+ dispatcher.setAssertion((rr) -> {
+ String cookie = rr.getHeaders().get("Cookie");
+ if (cookie == null) {
+ return;
+ }
+ assertThat(cookie).contains("SESSION").doesNotContain("JSESSIONID");
+ });
+ this.server.setDispatcher(dispatcher);
+ return this.server;
+ }
+
+ @PreDestroy
+ void shutdown() throws IOException {
+ this.server.shutdown();
+ }
+
+ }
+
@Configuration
@EnableWebSecurity
@Import(RegistrationConfig.class)
@@ -559,12 +676,15 @@ public class OidcLogoutConfigurerTests {
private MockMvc mvc;
+ private Consumer assertion = (rr) -> { };
+
MockMvcDispatcher(ObjectProvider mvc) {
this.mvcProvider = mvc;
}
@Override
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
+ this.assertion.accept(request);
this.mvc = this.mvcProvider.getObject();
String method = request.getMethod();
String path = request.getPath();
@@ -601,6 +721,10 @@ public class OidcLogoutConfigurerTests {
this.session.put(session.getId(), session);
}
+ void setAssertion(Consumer assertion) {
+ this.assertion = assertion;
+ }
+
private MockHttpSession session(RecordedRequest request) {
String cookieHeaderValue = request.getHeader("Cookie");
if (cookieHeaderValue == null) {
@@ -613,6 +737,10 @@ public class OidcLogoutConfigurerTests {
return this.session.computeIfAbsent(parts[1],
(k) -> new MockHttpSession(new MockServletContext(), parts[1]));
}
+ if ("SESSION".equals(parts[0])) {
+ return this.session.computeIfAbsent(parts[1],
+ (k) -> new MockHttpSession(new MockServletContext(), parts[1]));
+ }
}
return new MockHttpSession();
}
diff --git a/config/src/test/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandlerTests.java b/config/src/test/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandlerTests.java
index 37190608c1..a8494bdc90 100644
--- a/config/src/test/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandlerTests.java
+++ b/config/src/test/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandlerTests.java
@@ -19,6 +19,10 @@ package org.springframework.security.config.web.server;
import org.junit.jupiter.api.Test;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.security.oauth2.client.oidc.authentication.logout.TestOidcLogoutTokens;
+import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry;
+import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry;
+import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import static org.assertj.core.api.Assertions.assertThat;
@@ -27,36 +31,43 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
public class OidcBackChannelServerLogoutHandlerTests {
+ private final ReactiveOidcSessionRegistry sessionRegistry = new InMemoryReactiveOidcSessionRegistry();
+
+ private final OidcBackChannelLogoutAuthentication token = new OidcBackChannelLogoutAuthentication(
+ TestOidcLogoutTokens.withSubject("issuer", "subject").build(),
+ TestClientRegistrations.clientRegistration().build());
+
// gh-14553
@Test
public void computeLogoutEndpointWhenDifferentHostnameThenLocalhost() {
- OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
+ OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(this.sessionRegistry);
+ logoutHandler.setLogoutUri("{baseScheme}://localhost{basePort}/logout");
MockServerHttpRequest request = MockServerHttpRequest
.get("https://host.docker.internal:8090/back-channel/logout")
.build();
- String endpoint = logoutHandler.computeLogoutEndpoint(request);
- assertThat(endpoint).isEqualTo("https://localhost:8090/logout");
+ String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token);
+ assertThat(endpoint).startsWith("https://localhost:8090/logout");
}
@Test
public void computeLogoutEndpointWhenUsingBaseUrlTemplateThenServerName() {
- OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
+ OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(this.sessionRegistry);
logoutHandler.setLogoutUri("{baseUrl}/logout");
MockServerHttpRequest request = MockServerHttpRequest
.get("http://host.docker.internal:8090/back-channel/logout")
.build();
- String endpoint = logoutHandler.computeLogoutEndpoint(request);
- assertThat(endpoint).isEqualTo("http://host.docker.internal:8090/logout");
+ String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token);
+ assertThat(endpoint).startsWith("http://host.docker.internal:8090/logout");
}
// gh-14609
@Test
public void computeLogoutEndpointWhenLogoutUriThenUses() {
- OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
+ OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(this.sessionRegistry);
logoutHandler.setLogoutUri("http://localhost:8090/logout");
MockServerHttpRequest request = MockServerHttpRequest.get("https://server-one.com/back-channel/logout").build();
- String endpoint = logoutHandler.computeLogoutEndpoint(request);
- assertThat(endpoint).isEqualTo("http://localhost:8090/logout");
+ String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token);
+ assertThat(endpoint).startsWith("http://localhost:8090/logout");
}
}
diff --git a/config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java
index fed59255f0..63845b7fcf 100644
--- a/config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java
+++ b/config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java
@@ -25,6 +25,7 @@ import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.function.Consumer;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
@@ -96,6 +97,7 @@ import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.server.WebSession;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
@@ -268,6 +270,52 @@ public class OidcLogoutSpecTests {
this.test.get().uri("/token/logout").cookie("SESSION", one).exchange().expectStatus().isOk();
}
+ @Test
+ void logoutWhenSelfRemoteLogoutUriThenUses() {
+ this.spring.register(WebServerConfig.class, OidcProviderConfig.class, SelfLogoutUriConfig.class).autowire();
+ String registrationId = this.clientRegistration.getRegistrationId();
+ String sessionId = login();
+ String logoutToken = this.test.get()
+ .uri("/token/logout")
+ .cookie("SESSION", sessionId)
+ .exchange()
+ .expectStatus()
+ .isOk()
+ .returnResult(String.class)
+ .getResponseBody()
+ .blockFirst();
+ this.test.post()
+ .uri(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
+ .body(BodyInserters.fromFormData("logout_token", logoutToken))
+ .exchange()
+ .expectStatus()
+ .isOk();
+ this.test.get().uri("/token/logout").cookie("SESSION", sessionId).exchange().expectStatus().isUnauthorized();
+ }
+
+ @Test
+ void logoutWhenDifferentCookieNameThenUses() {
+ this.spring.register(OidcProviderConfig.class, CookieConfig.class).autowire();
+ String registrationId = this.clientRegistration.getRegistrationId();
+ String sessionId = login();
+ String logoutToken = this.test.get()
+ .uri("/token/logout")
+ .cookie("SESSION", sessionId)
+ .exchange()
+ .expectStatus()
+ .isOk()
+ .returnResult(String.class)
+ .getResponseBody()
+ .blockFirst();
+ this.test.post()
+ .uri(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
+ .body(BodyInserters.fromFormData("logout_token", logoutToken))
+ .exchange()
+ .expectStatus()
+ .isOk();
+ this.test.get().uri("/token/logout").cookie("SESSION", sessionId).exchange().expectStatus().isUnauthorized();
+ }
+
@Test
void logoutWhenRemoteLogoutFailsThenReportsPartialLogout() {
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, WithBrokenLogoutConfig.class).autowire();
@@ -444,6 +492,81 @@ public class OidcLogoutSpecTests {
}
+ @Configuration
+ @EnableWebFluxSecurity
+ @Import(RegistrationConfig.class)
+ static class SelfLogoutUriConfig {
+
+ @Bean
+ @Order(1)
+ SecurityWebFilterChain filters(ServerHttpSecurity http) throws Exception {
+ // @formatter:off
+ http
+ .authorizeExchange((authorize) -> authorize.anyExchange().authenticated())
+ .oauth2Login(Customizer.withDefaults())
+ .oidcLogout((oidc) -> oidc
+ .backChannel(Customizer.withDefaults())
+ );
+ // @formatter:on
+
+ return http.build();
+ }
+
+ }
+
+ @Configuration
+ @EnableWebFluxSecurity
+ @Import(RegistrationConfig.class)
+ static class CookieConfig {
+
+ private final ReactiveOidcSessionRegistry sessionRegistry = new InMemoryReactiveOidcSessionRegistry();
+
+ private final MockWebServer server = new MockWebServer();
+
+ @Bean
+ @Order(1)
+ SecurityWebFilterChain filters(ServerHttpSecurity http) throws Exception {
+ // @formatter:off
+ http
+ .authorizeExchange((authorize) -> authorize.anyExchange().authenticated())
+ .oauth2Login((oauth2) -> oauth2.oidcSessionRegistry(this.sessionRegistry))
+ .oidcLogout((oidc) -> oidc
+ .backChannel(Customizer.withDefaults())
+ );
+ // @formatter:on
+
+ return http.build();
+ }
+
+ @Bean
+ OidcBackChannelServerLogoutHandler oidcLogoutHandler() {
+ OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(
+ this.sessionRegistry);
+ logoutHandler.setSessionCookieName("JSESSIONID");
+ return logoutHandler;
+ }
+
+ @Bean
+ MockWebServer web(ObjectProvider web) {
+ WebTestClientDispatcher dispatcher = new WebTestClientDispatcher(web);
+ dispatcher.setAssertion((rr) -> {
+ String cookie = rr.getHeaders().get("Cookie");
+ if (cookie == null) {
+ return;
+ }
+ assertThat(cookie).contains("JSESSIONID");
+ });
+ this.server.setDispatcher(dispatcher);
+ return this.server;
+ }
+
+ @PreDestroy
+ void shutdown() throws IOException {
+ this.server.shutdown();
+ }
+
+ }
+
@Configuration
@EnableWebFluxSecurity
@Import(RegistrationConfig.class)
@@ -652,12 +775,15 @@ public class OidcLogoutSpecTests {
private WebTestClient web;
+ private Consumer assertion = (rr) -> { };
+
WebTestClientDispatcher(ObjectProvider web) {
this.webProvider = web;
}
@Override
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
+ this.assertion.accept(request);
this.web = this.webProvider.getObject();
String method = request.getMethod();
String path = request.getPath();
@@ -700,6 +826,10 @@ public class OidcLogoutSpecTests {
}
}
+ void setAssertion(Consumer assertion) {
+ this.assertion = assertion;
+ }
+
private String session(RecordedRequest request) {
String cookieHeaderValue = request.getHeader("Cookie");
if (cookieHeaderValue == null) {
@@ -711,6 +841,9 @@ public class OidcLogoutSpecTests {
if (SESSION_COOKIE_NAME.equals(parts[0])) {
return parts[1];
}
+ if ("JSESSIONID".equals(parts[0])) {
+ return parts[1];
+ }
}
return null;
}
diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDslTests.kt
index 468be4251b..2518991b8d 100644
--- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDslTests.kt
+++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDslTests.kt
@@ -23,13 +23,19 @@ import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcBackChannelLogoutHandler
+import org.springframework.security.config.annotation.web.oauth2.login.OidcBackChannelLogoutDsl
import org.springframework.security.config.test.SpringTestContext
import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry
+import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry
import org.springframework.security.oauth2.client.registration.ClientRegistration
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository
import org.springframework.security.oauth2.client.registration.TestClientRegistrations
import org.springframework.security.web.SecurityFilterChain
+import org.springframework.security.web.authentication.logout.LogoutHandler
+import org.springframework.test.util.ReflectionTestUtils
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.post
@@ -53,12 +59,23 @@ class OidcLogoutDslTests {
this.mockMvc.post("/logout/connect/back-channel/" + clientRegistration.registrationId) {
param("logout_token", "token")
}.andExpect { status { isBadRequest() } }
+ val chain: SecurityFilterChain = this.spring.context.getBean(SecurityFilterChain::class.java)
+ for (filter in chain.filters) {
+ if (filter.javaClass.simpleName.equals("OidcBackChannelLogoutFilter")) {
+ val logoutHandler = ReflectionTestUtils.getField(filter, "logoutHandler") as LogoutHandler
+ val backChannelLogoutHandler = ReflectionTestUtils.getField(logoutHandler, "left") as LogoutHandler
+ var cookieName = ReflectionTestUtils.getField(backChannelLogoutHandler, "sessionCookieName") as String
+ assert(cookieName.equals("SESSION"))
+ }
+ }
}
@Configuration
@EnableWebSecurity
open class ClientRepositoryConfig {
+ private val sessionRegistry = InMemoryOidcSessionRegistry()
+
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
@@ -73,6 +90,13 @@ class OidcLogoutDslTests {
return http.build()
}
+ @Bean
+ open fun oidcLogoutHandler(): OidcBackChannelLogoutHandler {
+ val logoutHandler = OidcBackChannelLogoutHandler(this.sessionRegistry)
+ logoutHandler.setSessionCookieName("SESSION");
+ return logoutHandler;
+ }
+
@Bean
open fun clientRegistration(): ClientRegistration {
return TestClientRegistrations.clientRegistration().build()
diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDslTests.kt
index a4b62b2dc0..4bc0ca993d 100644
--- a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDslTests.kt
+++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDslTests.kt
@@ -25,14 +25,18 @@ import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.test.SpringTestContext
import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry
import org.springframework.security.oauth2.client.registration.ClientRegistration
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
import org.springframework.security.oauth2.client.registration.TestClientRegistrations
+import org.springframework.security.web.authentication.logout.LogoutHandler
import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.test.util.ReflectionTestUtils
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.web.reactive.config.EnableWebFlux
import org.springframework.web.reactive.function.BodyInserters
+import org.springframework.web.server.WebFilter
/**
* Tests for [ServerOidcLogoutDsl]
@@ -63,6 +67,15 @@ class ServerOidcLogoutDslTests {
.body(BodyInserters.fromFormData("logout_token", "token"))
.exchange()
.expectStatus().isBadRequest
+ val chain: SecurityWebFilterChain = this.spring.context.getBean(SecurityWebFilterChain::class.java)
+ chain.webFilters.doOnNext({ filter: WebFilter ->
+ if (filter.javaClass.simpleName.equals("OidcBackChannelLogoutWebFilter")) {
+ val logoutHandler = ReflectionTestUtils.getField(filter, "logoutHandler") as LogoutHandler
+ val backChannelLogoutHandler = ReflectionTestUtils.getField(logoutHandler, "left") as LogoutHandler
+ var cookieName = ReflectionTestUtils.getField(backChannelLogoutHandler, "sessionCookieName") as String
+ assert(cookieName.equals("SESSION"))
+ }
+ })
}
@Configuration
@@ -70,6 +83,8 @@ class ServerOidcLogoutDslTests {
@EnableWebFluxSecurity
open class ClientRepositoryConfig {
+ private val sessionRegistry = InMemoryReactiveOidcSessionRegistry()
+
@Bean
open fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
@@ -83,6 +98,13 @@ class ServerOidcLogoutDslTests {
}
}
+ @Bean
+ open fun oidcLogoutHandler(): OidcBackChannelServerLogoutHandler {
+ val logoutHandler = OidcBackChannelServerLogoutHandler(this.sessionRegistry)
+ logoutHandler.setSessionCookieName("SESSION");
+ return logoutHandler;
+ }
+
@Bean
open fun clientRegistration(): ClientRegistration {
return TestClientRegistrations.clientRegistration().build()
diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login/logout.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login/logout.adoc
index c1545aeb74..15ce91c39c 100644
--- a/docs/modules/ROOT/pages/reactive/oauth2/login/logout.adoc
+++ b/docs/modules/ROOT/pages/reactive/oauth2/login/logout.adoc
@@ -137,6 +137,11 @@ Java::
+
[source,java,role="primary"]
----
+@Bean
+OidcBackChannelServerLogoutHandler oidcLogoutHandler() {
+ return new OidcBackChannelServerLogoutHandler();
+}
+
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Exception {
http
@@ -155,6 +160,11 @@ Kotlin::
+
[source,kotlin,role="secondary"]
----
+@Bean
+fun oidcLogoutHandler(): OidcBackChannelLogoutHandler {
+ return OidcBackChannelLogoutHandler()
+}
+
@Bean
open fun filterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http {
@@ -197,6 +207,80 @@ The overall flow for a Back-Channel logout is like this:
Remember that Spring Security's OIDC support is multi-tenant.
This means that it will only terminate sessions whose Client matches the `aud` claim in the Logout Token.
+=== Customizing the Session Logout Endpoint
+
+With `OidcBackChannelServerLogoutHandler` published, the session logout endpoint is `+{baseUrl}+/logout/connect/back-channel/+{registrationId}+`.
+
+If `OidcBackChannelServerLogoutHandler` is not wired, then the URL is `+{baseUrl}+/logout/connect/back-channel/+{registrationId}+`, which is not recommended since it requires passing a CSRF token, which can be challenging depending on the kind of repository your application uses.
+
+In the event that you need to customize the endpoint, you can provide the URL as follows:
+
+
+[tabs]
+======
+Java::
++
+[source=java,role="primary"]
+----
+http
+ // ...
+ .oidcLogout((oidc) -> oidc
+ .backChannel((backChannel) -> backChannel
+ .logoutUri("http://localhost:9000/logout/connect/back-channel/+{registrationId}+")
+ )
+ );
+----
+
+Kotlin::
++
+[source=kotlin,role="secondary"]
+----
+http {
+ oidcLogout {
+ backChannel {
+ logoutUri = "http://localhost:9000/logout/connect/back-channel/+{registrationId}+"
+ }
+ }
+}
+----
+======
+
+=== Customizing the Session Logout Cookie Name
+
+By default, the session logout endpoint uses the `JSESSIONID` cookie to correlate the session to the corresponding `OidcSessionInformation`.
+
+However, the default cookie name in Spring Session is `SESSION`.
+
+You can configure Spring Session's cookie name in the DSL like so:
+
+[tabs]
+======
+Java::
++
+[source=java,role="primary"]
+----
+@Bean
+OidcBackChannelServerLogoutHandler oidcLogoutHandler(ReactiveOidcSessionRegistry sessionRegistry) {
+ OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(sessionRegistry);
+ logoutHandler.setSessionCookieName("SESSION");
+ return logoutHandler;
+}
+----
+
+Kotlin::
++
+[source=kotlin,role="secondary"]
+----
+@Bean
+open fun oidcLogoutHandler(val sessionRegistry: ReactiveOidcSessionRegistry): OidcBackChannelServerLogoutHandler {
+ val logoutHandler = OidcBackChannelServerLogoutHandler(sessionRegistry)
+ logoutHandler.setSessionCookieName("SESSION")
+ return logoutHandler
+}
+----
+======
+
+[[oidc-backchannel-logout-session-registry]]
=== Customizing the OIDC Provider Session Registry
By default, Spring Security stores in-memory all links between the OIDC Provider session and the Client session.
diff --git a/docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc b/docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc
index 32577615ed..9edd4492bd 100644
--- a/docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc
+++ b/docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc
@@ -136,6 +136,11 @@ Java::
+
[source,java,role="primary"]
----
+@Bean
+OidcBackChannelLogoutHandler oidcLogoutHandler() {
+ return new OidcBackChannelLogoutHandler();
+}
+
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
@@ -154,6 +159,11 @@ Kotlin::
+
[source,kotlin,role="secondary"]
----
+@Bean
+fun oidcLogoutHandler(): OidcBackChannelLogoutHandler {
+ return OidcBackChannelLogoutHandler()
+}
+
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
@@ -223,6 +233,87 @@ The overall flow for a Back-Channel logout is like this:
Remember that Spring Security's OIDC support is multi-tenant.
This means that it will only terminate sessions whose Client matches the `aud` claim in the Logout Token.
+One notable part of this architecture's implementation is that it propagates the incoming back-channel request internally for each corresponding session.
+Initially, this may seem unnecessary.
+However, recall that the Servlet API does not give direct access to the `HttpSession` store.
+By making an internal logout call, the corresponding session can now be validated.
+
+Additionally, forging a logout call internally allows for each set of ``LogoutHandler``s to be run against that session and corresponding `SecurityContext`.
+
+=== Customizing the Session Logout Endpoint
+
+With `OidcBackChannelLogoutHandler` published, the session logout endpoint is `+{baseUrl}+/logout/connect/back-channel/+{registrationId}+`.
+
+If `OidcBackChannelLogoutHandler` is not wired, then the URL is `+{baseUrl}+/logout/connect/back-channel/+{registrationId}+`, which is not recommended since it requires passing a CSRF token, which can be challenging depending on the kind of repository your application uses.
+
+In the event that you need to customize the endpoint, you can provide the URL as follows:
+
+
+[tabs]
+======
+Java::
++
+[source=java,role="primary"]
+----
+http
+ // ...
+ .oidcLogout((oidc) -> oidc
+ .backChannel((backChannel) -> backChannel
+ .logoutUri("http://localhost:9000/logout/connect/back-channel/+{registrationId}+")
+ )
+ );
+----
+
+Kotlin::
++
+[source=kotlin,role="secondary"]
+----
+http {
+ oidcLogout {
+ backChannel {
+ logoutUri = "http://localhost:9000/logout/connect/back-channel/+{registrationId}+"
+ }
+ }
+}
+----
+======
+
+=== Customizing the Session Logout Cookie Name
+
+By default, the session logout endpoint uses the `JSESSIONID` cookie to correlate the session to the corresponding `OidcSessionInformation`.
+
+However, the default cookie name in Spring Session is `SESSION`.
+
+You can configure Spring Session's cookie name in the DSL like so:
+
+[tabs]
+======
+Java::
++
+[source=java,role="primary"]
+----
+@Bean
+OidcBackChannelLogoutHandler oidcLogoutHandler(OidcSessionRegistry sessionRegistry) {
+ OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(oidcSessionRegistry);
+ logoutHandler.setSessionCookieName("SESSION");
+ return logoutHandler;
+}
+----
+
+Kotlin::
++
+[source=kotlin,role="secondary"]
+----
+@Bean
+open fun oidcLogoutHandler(val sessionRegistry: OidcSessionRegistry): OidcBackChannelLogoutHandler {
+ val logoutHandler = OidcBackChannelLogoutHandler(sessionRegistry)
+ logoutHandler.setSessionCookieName("SESSION")
+ return logoutHandler
+}
+----
+======
+
+[[oidc-backchannel-logout-session-registry]]
=== Customizing the OIDC Provider Session Registry
By default, Spring Security stores in-memory all links between the OIDC Provider session and the Client session.