2
0
Эх сурвалжийг харах

Expose OidcBackChannelLogoutHandler

This component already uses by default a URI that doesn't require
a CSRF token and aalready allows for configuring a cookie name.

So, by making it public and configurable in the DSL, both
of these tickets quite naturally close.

Closes gh-13841
Closes gh-14904
Josh Cummings 1 жил өмнө
parent
commit
8bb5875595
22 өөрчлөгдсөн 910 нэмэгдсэн , 137 устгасан
  1. 9 1
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthentication.java
  2. 1 1
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthenticationProvider.java
  3. 4 12
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutFilter.java
  4. 26 34
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandler.java
  5. 133 6
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurer.java
  6. 9 1
      config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutAuthentication.java
  7. 1 1
      config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutReactiveAuthenticationManager.java
  8. 4 13
      config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutWebFilter.java
  9. 30 35
      config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandler.java
  10. 123 12
      config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
  11. 2 0
      config/src/main/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDsl.kt
  12. 21 1
      config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcBackChannelLogoutDsl.kt
  13. 22 1
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcBackChannelLogoutDsl.kt
  14. 3 1
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDsl.kt
  15. 20 9
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandlerTests.java
  16. 128 0
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java
  17. 20 9
      config/src/test/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandlerTests.java
  18. 133 0
      config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java
  19. 24 0
      config/src/test/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDslTests.kt
  20. 22 0
      config/src/test/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDslTests.kt
  21. 84 0
      docs/modules/ROOT/pages/reactive/oauth2/login/logout.adoc
  22. 91 0
      docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc

+ 9 - 1
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthentication.java

@@ -20,6 +20,7 @@ import java.util.Collections;
 
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
 
 /**
  * An {@link org.springframework.security.core.Authentication} implementation that
@@ -37,13 +38,16 @@ class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
 
 	private final OidcLogoutToken logoutToken;
 
+	private final ClientRegistration clientRegistration;
+
 	/**
 	 * Construct an {@link OidcBackChannelLogoutAuthentication}
 	 * @param logoutToken a deserialized, verified OIDC Logout Token
 	 */
-	OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken) {
+	OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken, ClientRegistration clientRegistration) {
 		super(Collections.emptyList());
 		this.logoutToken = logoutToken;
+		this.clientRegistration = clientRegistration;
 		setAuthenticated(true);
 	}
 
@@ -63,4 +67,8 @@ class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
 		return this.logoutToken;
 	}
 
+	ClientRegistration getClientRegistration() {
+		return this.clientRegistration;
+	}
+
 }

+ 1 - 1
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthenticationProvider.java

@@ -99,7 +99,7 @@ final class OidcBackChannelLogoutAuthenticationProvider implements Authenticatio
 		OidcLogoutToken oidcLogoutToken = OidcLogoutToken.withTokenValue(logoutToken)
 			.claims((claims) -> claims.putAll(jwt.getClaims()))
 			.build();
-		return new OidcBackChannelLogoutAuthentication(oidcLogoutToken);
+		return new OidcBackChannelLogoutAuthentication(oidcLogoutToken, registration);
 	}
 
 	/**

+ 4 - 12
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutFilter.java

@@ -58,7 +58,7 @@ class OidcBackChannelLogoutFilter extends OncePerRequestFilter {
 
 	private final OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter();
 
-	private LogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
+	private final LogoutHandler logoutHandler;
 
 	/**
 	 * Construct an {@link OidcBackChannelLogoutFilter}
@@ -68,11 +68,13 @@ class OidcBackChannelLogoutFilter extends OncePerRequestFilter {
 	 * Logout Tokens
 	 */
 	OidcBackChannelLogoutFilter(AuthenticationConverter authenticationConverter,
-			AuthenticationManager authenticationManager) {
+			AuthenticationManager authenticationManager, LogoutHandler logoutHandler) {
 		Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
 		Assert.notNull(authenticationManager, "authenticationManager cannot be null");
+		Assert.notNull(logoutHandler, "logoutHandler cannot be null");
 		this.authenticationConverter = authenticationConverter;
 		this.authenticationManager = authenticationManager;
+		this.logoutHandler = logoutHandler;
 	}
 
 	/**
@@ -126,14 +128,4 @@ class OidcBackChannelLogoutFilter extends OncePerRequestFilter {
 				"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
 	}
 
-	/**
-	 * The strategy for expiring all Client sessions indicated by the logout request.
-	 * Defaults to {@link OidcBackChannelLogoutHandler}.
-	 * @param logoutHandler the {@link LogoutHandler} to use
-	 */
-	void setLogoutHandler(LogoutHandler logoutHandler) {
-		Assert.notNull(logoutHandler, "logoutHandler cannot be null");
-		this.logoutHandler = logoutHandler;
-	}
-
 }

+ 26 - 34
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandler.java

@@ -29,10 +29,9 @@ import org.apache.commons.logging.LogFactory;
 
 import org.springframework.http.HttpEntity;
 import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
 import org.springframework.http.server.ServletServerHttpResponse;
 import org.springframework.security.core.Authentication;
-import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
-import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
 import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
 import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
 import org.springframework.security.oauth2.core.OAuth2Error;
@@ -40,6 +39,8 @@ import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMe
 import org.springframework.security.web.authentication.logout.LogoutHandler;
 import org.springframework.security.web.util.UrlUtils;
 import org.springframework.util.Assert;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
 import org.springframework.web.client.RestClientException;
 import org.springframework.web.client.RestOperations;
 import org.springframework.web.client.RestTemplate;
@@ -51,25 +52,29 @@ import org.springframework.web.util.UriComponentsBuilder;
  * Back-Channel Logout Token and invalidates each one.
  *
  * @author Josh Cummings
- * @since 6.2
+ * @since 6.4
  * @see <a target="_blank" href=
  * "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
  * Spec</a>
  */
-final class OidcBackChannelLogoutHandler implements LogoutHandler {
+public final class OidcBackChannelLogoutHandler implements LogoutHandler {
 
 	private final Log logger = LogFactory.getLog(getClass());
 
-	private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
+	private final OidcSessionRegistry sessionRegistry;
 
 	private RestOperations restOperations = new RestTemplate();
 
-	private String logoutUri = "{baseScheme}://localhost{basePort}/logout";
+	private String logoutUri = "{baseUrl}/logout/connect/back-channel/{registrationId}";
 
 	private String sessionCookieName = "JSESSIONID";
 
 	private final OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter();
 
+	public OidcBackChannelLogoutHandler(OidcSessionRegistry sessionRegistry) {
+		this.sessionRegistry = sessionRegistry;
+	}
+
 	@Override
 	public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
 		if (!(authentication instanceof OidcBackChannelLogoutAuthentication token)) {
@@ -86,7 +91,7 @@ final class OidcBackChannelLogoutHandler implements LogoutHandler {
 		for (OidcSessionInformation session : sessions) {
 			totalCount++;
 			try {
-				eachLogout(request, session);
+				eachLogout(request, token, session);
 				invalidatedCount++;
 			}
 			catch (RestClientException ex) {
@@ -103,18 +108,23 @@ final class OidcBackChannelLogoutHandler implements LogoutHandler {
 		}
 	}
 
-	private void eachLogout(HttpServletRequest request, OidcSessionInformation session) {
+	private void eachLogout(HttpServletRequest request, OidcBackChannelLogoutAuthentication token,
+			OidcSessionInformation session) {
 		HttpHeaders headers = new HttpHeaders();
 		headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId());
 		for (Map.Entry<String, String> credential : session.getAuthorities().entrySet()) {
 			headers.add(credential.getKey(), credential.getValue());
 		}
-		String logout = computeLogoutEndpoint(request);
-		HttpEntity<?> entity = new HttpEntity<>(null, headers);
+		headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+		String logout = computeLogoutEndpoint(request, token);
+		MultiValueMap<String, String> body = new LinkedMultiValueMap();
+		body.add("logout_token", token.getPrincipal().getTokenValue());
+		body.add("_spring_security_internal_logout", "true");
+		HttpEntity<?> entity = new HttpEntity<>(body, headers);
 		this.restOperations.postForEntity(logout, entity, Object.class);
 	}
 
-	String computeLogoutEndpoint(HttpServletRequest request) {
+	String computeLogoutEndpoint(HttpServletRequest request, OidcBackChannelLogoutAuthentication token) {
 		// @formatter:off
 		UriComponents uriComponents = UriComponentsBuilder
 				.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
@@ -137,6 +147,9 @@ final class OidcBackChannelLogoutHandler implements LogoutHandler {
 		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();
@@ -158,34 +171,13 @@ final class OidcBackChannelLogoutHandler implements LogoutHandler {
 		}
 	}
 
-	/**
-	 * 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(OidcSessionRegistry sessionRegistry) {
-		Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
-		this.sessionRegistry = sessionRegistry;
-	}
-
-	/**
-	 * Use this {@link RestOperations} to perform the per-session back-channel logout
-	 * @param restOperations the {@link RestOperations} to use
-	 */
-	void setRestOperations(RestOperations restOperations) {
-		Assert.notNull(restOperations, "restOperations cannot be null");
-		this.restOperations = restOperations;
-	}
-
 	/**
 	 * 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;
 	}
@@ -197,7 +189,7 @@ final class OidcBackChannelLogoutHandler implements LogoutHandler {
 	 * 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;
 	}

+ 133 - 6
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurer.java

@@ -19,16 +19,24 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.cl
 import java.util.function.Consumer;
 import java.util.function.Function;
 
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.springframework.context.ApplicationContext;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.ProviderManager;
 import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
+import org.springframework.security.core.Authentication;
 import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
 import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
 import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
 import org.springframework.security.web.authentication.logout.LogoutHandler;
+import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
 import org.springframework.security.web.csrf.CsrfFilter;
 import org.springframework.util.Assert;
 
@@ -140,8 +148,11 @@ public final class OidcLogoutConfigurer<B extends HttpSecurityBuilder<B>>
 		}
 
 		private LogoutHandler logoutHandler(B http) {
-			OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
-			logoutHandler.setSessionRegistry(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
+			OidcBackChannelLogoutHandler logoutHandler = getBeanOrNull(OidcBackChannelLogoutHandler.class);
+			if (logoutHandler != null) {
+				return logoutHandler;
+			}
+			logoutHandler = new OidcBackChannelLogoutHandler(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
 			return logoutHandler;
 		}
 
@@ -176,21 +187,137 @@ public final class OidcLogoutConfigurer<B extends HttpSecurityBuilder<B>>
 		 */
 		public BackChannelLogoutConfigurer logoutUri(String logoutUri) {
 			this.logoutHandler = (http) -> {
-				OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
-				logoutHandler.setSessionRegistry(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
+				OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(
+						OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
 				logoutHandler.setLogoutUri(logoutUri);
 				return logoutHandler;
 			};
 			return this;
 		}
 
+		/**
+		 * Configure what and how per-session logout will be performed.
+		 *
+		 * <p>
+		 * This overrides any value given to {@link #logoutUri(String)}
+		 *
+		 * <p>
+		 * By default, the resulting {@link LogoutHandler} will {@code POST} the session
+		 * cookie and OIDC logout token back to the original back-channel logout endpoint.
+		 *
+		 * <p>
+		 * 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.
+		 *
+		 * <p>
+		 * For example, configuring Back-Channel Logout in the following way:
+		 *
+		 * <pre>
+		 * 	http
+		 *     	.oidcLogout((oidc) -&gt; oidc
+		 *     		.backChannel((backChannel) -&gt; backChannel
+		 *     			.logoutHandler(new OidcBackChannelLogoutHandler())
+		 *     		)
+		 *     	);
+		 * </pre>
+		 *
+		 * will make so that the per-session logout invocation no longer requires special
+		 * CSRF configurations.
+		 *
+		 * <p>
+		 * 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 OidcBackChannelLogoutHandler#setLogoutUri(String)} to alter
+		 * the scheme, server name, or port in the {@code Host} header to accommodate how
+		 * your application would address itself internally.
+		 *
+		 * <p>
+		 * 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:
+		 *
+		 * <pre>
+		 * 	http
+		 * 		.oidcLogout((oidc) -&gt; oidc
+		 * 			.backChannel((backChannel) -&gt; backChannel
+		 * 				.logoutHandler("http://localhost:9000/logout/connect/back-channel/{registrationId}")
+		 * 			)
+		 * 		);
+		 * </pre>
+		 *
+		 * <p>
+		 * You can also publish it as a {@code @Bean} as follows:
+		 *
+		 * <pre>
+		 *	&commat;Bean
+		 *	OidcBackChannelLogoutHandler oidcLogoutHandler(OidcSessionRegistry sessionRegistry) {
+		 *  	OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(sessionRegistry);
+		 *  	logoutHandler.setSessionCookieName("SESSION");
+		 *  	return logoutHandler;
+		 *	}
+		 * </pre>
+		 *
+		 * to have the same effect.
+		 * @param logoutHandler the {@link LogoutHandler} to use each individual session
+		 * @return {@link BackChannelLogoutConfigurer} for further customizations
+		 * @since 6.4
+		 */
+		public BackChannelLogoutConfigurer logoutHandler(LogoutHandler logoutHandler) {
+			this.logoutHandler = (http) -> logoutHandler;
+			return this;
+		}
+
 		void configure(B http) {
+			LogoutHandler oidcLogout = this.logoutHandler.apply(http);
+			LogoutHandler sessionLogout = new SecurityContextLogoutHandler();
+			LogoutConfigurer<B> logout = http.getConfigurer(LogoutConfigurer.class);
+			if (logout != null) {
+				sessionLogout = new CompositeLogoutHandler(logout.getLogoutHandlers());
+			}
 			OidcBackChannelLogoutFilter filter = new OidcBackChannelLogoutFilter(authenticationConverter(http),
-					authenticationManager());
-			filter.setLogoutHandler(this.logoutHandler.apply(http));
+					authenticationManager(), new EitherLogoutHandler(oidcLogout, sessionLogout));
 			http.addFilterBefore(filter, CsrfFilter.class);
 		}
 
+		private <T> T getBeanOrNull(Class<?> clazz) {
+			ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class);
+			if (context != null) {
+				String[] names = context.getBeanNamesForType(clazz);
+				if (names.length == 1) {
+					return (T) context.getBean(names[0]);
+				}
+			}
+			return null;
+		}
+
+		private static final class EitherLogoutHandler implements LogoutHandler {
+
+			private final LogoutHandler left;
+
+			private final LogoutHandler right;
+
+			EitherLogoutHandler(LogoutHandler left, LogoutHandler right) {
+				this.left = left;
+				this.right = right;
+			}
+
+			@Override
+			public void logout(HttpServletRequest request, HttpServletResponse response,
+					Authentication authentication) {
+				if (request.getParameter("_spring_security_internal_logout") == null) {
+					this.left.logout(request, response, authentication);
+				}
+				else {
+					this.right.logout(request, response, authentication);
+				}
+			}
+
+		}
+
 	}
 
 }

+ 9 - 1
config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutAuthentication.java

@@ -20,6 +20,7 @@ import java.util.Collections;
 
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
 
 /**
  * An {@link org.springframework.security.core.Authentication} implementation that
@@ -37,13 +38,16 @@ class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
 
 	private final OidcLogoutToken logoutToken;
 
+	private final ClientRegistration clientRegistration;
+
 	/**
 	 * Construct an {@link OidcBackChannelLogoutAuthentication}
 	 * @param logoutToken a deserialized, verified OIDC Logout Token
 	 */
-	OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken) {
+	OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken, ClientRegistration clientRegistration) {
 		super(Collections.emptyList());
 		this.logoutToken = logoutToken;
+		this.clientRegistration = clientRegistration;
 		setAuthenticated(true);
 	}
 
@@ -63,4 +67,8 @@ class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
 		return this.logoutToken;
 	}
 
+	ClientRegistration getClientRegistration() {
+		return this.clientRegistration;
+	}
+
 }

+ 1 - 1
config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutReactiveAuthenticationManager.java

@@ -80,7 +80,7 @@ final class OidcBackChannelLogoutReactiveAuthenticationManager implements Reacti
 			.map((jwt) -> OidcLogoutToken.withTokenValue(logoutToken)
 				.claims((claims) -> claims.putAll(jwt.getClaims()))
 				.build())
-			.map(OidcBackChannelLogoutAuthentication::new);
+			.map((oidcLogoutToken) -> new OidcBackChannelLogoutAuthentication(oidcLogoutToken, registration));
 	}
 
 	private Mono<Jwt> decode(ClientRegistration registration, String token) {

+ 4 - 13
config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutWebFilter.java

@@ -34,7 +34,6 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
 import org.springframework.security.web.authentication.AuthenticationConverter;
-import org.springframework.security.web.authentication.logout.LogoutHandler;
 import org.springframework.security.web.server.WebFilterExchange;
 import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
 import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
@@ -60,7 +59,7 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
 
 	private final ReactiveAuthenticationManager authenticationManager;
 
-	private ServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
+	private final ServerLogoutHandler logoutHandler;
 
 	/**
 	 * Construct an {@link OidcBackChannelLogoutWebFilter}
@@ -70,11 +69,13 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
 	 * Logout Tokens
 	 */
 	OidcBackChannelLogoutWebFilter(ServerAuthenticationConverter authenticationConverter,
-			ReactiveAuthenticationManager authenticationManager) {
+			ReactiveAuthenticationManager authenticationManager, ServerLogoutHandler logoutHandler) {
 		Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
 		Assert.notNull(authenticationManager, "authenticationManager cannot be null");
+		Assert.notNull(logoutHandler, "logoutHandler cannot be null");
 		this.authenticationConverter = authenticationConverter;
 		this.authenticationManager = authenticationManager;
+		this.logoutHandler = logoutHandler;
 	}
 
 	@Override
@@ -124,14 +125,4 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
 				"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
 	}
 
-	/**
-	 * The strategy for expiring all Client sessions indicated by the logout request.
-	 * Defaults to {@link OidcBackChannelServerLogoutHandler}.
-	 * @param logoutHandler the {@link LogoutHandler} to use
-	 */
-	void setLogoutHandler(ServerLogoutHandler logoutHandler) {
-		Assert.notNull(logoutHandler, "logoutHandler cannot be null");
-		this.logoutHandler = logoutHandler;
-	}
-
 }

+ 30 - 35
config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandler.java

@@ -34,15 +34,15 @@ import org.springframework.http.ResponseEntity;
 import org.springframework.http.server.reactive.ServerHttpRequest;
 import org.springframework.http.server.reactive.ServerHttpResponse;
 import org.springframework.security.core.Authentication;
-import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
-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.oidc.session.OidcSessionInformation;
-import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.web.server.WebFilterExchange;
 import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
 import org.springframework.util.Assert;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.reactive.function.BodyInserters;
 import org.springframework.web.reactive.function.client.WebClient;
 import org.springframework.web.util.UriComponents;
 import org.springframework.web.util.UriComponentsBuilder;
@@ -52,23 +52,27 @@ import org.springframework.web.util.UriComponentsBuilder;
  * Back-Channel Logout Token and invalidates each one.
  *
  * @author Josh Cummings
- * @since 6.2
+ * @since 6.4
  * @see <a target="_blank" href=
  * "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
  * Spec</a>
  */
-final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
+public final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
 
 	private final Log logger = LogFactory.getLog(getClass());
 
-	private ReactiveOidcSessionRegistry sessionRegistry = new InMemoryReactiveOidcSessionRegistry();
+	private final ReactiveOidcSessionRegistry sessionRegistry;
 
 	private WebClient web = WebClient.create();
 
-	private String logoutUri = "{baseScheme}://localhost{basePort}/logout";
+	private String logoutUri = "{baseUrl}/logout/connect/back-channel/{registrationId}";
 
 	private String sessionCookieName = "SESSION";
 
+	public OidcBackChannelServerLogoutHandler(ReactiveOidcSessionRegistry sessionRegistry) {
+		this.sessionRegistry = sessionRegistry;
+	}
+
 	@Override
 	public Mono<Void> logout(WebFilterExchange exchange, Authentication authentication) {
 		if (!(authentication instanceof OidcBackChannelLogoutAuthentication token)) {
@@ -84,7 +88,7 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
 		AtomicInteger invalidatedCount = new AtomicInteger(0);
 		return this.sessionRegistry.removeSessionInformation(token.getPrincipal()).concatMap((session) -> {
 			totalCount.incrementAndGet();
-			return eachLogout(exchange, session).flatMap((response) -> {
+			return eachLogout(exchange, session, token).flatMap((response) -> {
 				invalidatedCount.incrementAndGet();
 				return Mono.empty();
 			}).onErrorResume((ex) -> {
@@ -105,17 +109,26 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
 		});
 	}
 
-	private Mono<ResponseEntity<Void>> eachLogout(WebFilterExchange exchange, OidcSessionInformation session) {
+	private Mono<ResponseEntity<Void>> eachLogout(WebFilterExchange exchange, OidcSessionInformation session,
+			OidcBackChannelLogoutAuthentication token) {
 		HttpHeaders headers = new HttpHeaders();
 		headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId());
 		for (Map.Entry<String, String> credential : session.getAuthorities().entrySet()) {
 			headers.add(credential.getKey(), credential.getValue());
 		}
-		String logout = computeLogoutEndpoint(exchange.getExchange().getRequest());
-		return this.web.post().uri(logout).headers((h) -> h.putAll(headers)).retrieve().toBodilessEntity();
+		String logout = computeLogoutEndpoint(exchange.getExchange().getRequest(), token);
+		MultiValueMap<String, String> 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;
 	}

+ 123 - 12
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 {
 			 *
 			 * <p>
 			 * 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.
 			 *
 			 * <p>
 			 * 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.
+			 *
+			 * <p>
+			 * This overrides any value given to {@link #logoutUri(String)}
+			 *
+			 * <p>
+			 * By default, the resulting {@link LogoutHandler} will {@code POST} the
+			 * session cookie and OIDC logout token back to the original back-channel
+			 * logout endpoint.
+			 *
+			 * <p>
+			 * 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.
+			 *
+			 * <p>
+			 * For example, configuring Back-Channel Logout in the following way:
+			 *
+			 * <pre>
+			 * 	http
+			 *     	.oidcLogout((oidc) -&gt; oidc
+			 *     		.backChannel((backChannel) -&gt; backChannel
+			 *     			.logoutHandler(new OidcBackChannelServerLogoutHandler())
+			 *     		)
+			 *     	);
+			 * </pre>
+			 *
+			 * will make so that the per-session logout invocation no longer requires
+			 * special CSRF configurations.
+			 *
+			 * <p>
+			 * 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.
+			 *
+			 * <p>
+			 * 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:
+			 *
+			 * <pre>
+			 * 	http
+			 * 		.oidcLogout((oidc) -&gt; oidc
+			 * 			.backChannel((backChannel) -&gt; backChannel
+			 * 				.logoutUri("http://localhost:9000/logout/connect/back-channel/{registrationId}")
+			 * 			)
+			 * 		);
+			 * </pre>
+			 *
+			 * <p>
+			 * You can also publish it as a {@code @Bean} as follows:
+			 *
+			 * <pre>
+			 *	&commat;Bean
+			 *	OidcBackChannelServerLogoutHandler oidcLogoutHandler() {
+			 *  	OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
+			 *  	logoutHandler.setLogoutUri("http://localhost:9000/logout/connect/back-channel/{registrationId}");
+			 *  	return logoutHandler;
+			 *	}
+			 * </pre>
+			 *
+			 * 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<Void> 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);
+						}
+					});
+				}
+
+			}
+
 		}
 
 	}

+ 2 - 0
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) }
         }
     }
+
 }

+ 21 - 1
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<HttpSecurity>.BackChannelLogoutConfigurer) -> Unit {
-        return { backChannel -> }
+        return { backChannel ->
+            logoutHandler?.also { backChannel.logoutHandler(logoutHandler) }
+            logoutUri?.also { backChannel.logoutUri(logoutUri) }
+        }
     }
 }

+ 22 - 1
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) }
+        }
     }
 }

+ 3 - 1
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 { }
+     *              }
      *          }
      *       }
      *   }

+ 20 - 9
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");
 	}
 
 }

+ 128 - 0
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<MockMvc> 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<RecordedRequest> assertion = (rr) -> { };
+
 		MockMvcDispatcher(ObjectProvider<MockMvc> 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<RecordedRequest> 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();
 		}

+ 20 - 9
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");
 	}
 
 }

+ 133 - 0
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<WebTestClient> 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<RecordedRequest> assertion = (rr) -> { };
+
 		WebTestClientDispatcher(ObjectProvider<WebTestClient> 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<RecordedRequest> 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;
 		}

+ 24 - 0
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()

+ 22 - 0
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()

+ 84 - 0
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.

+ 91 - 0
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.