Bladeren bron

Add Saml2LogoutConfigurer

Closes gh-9497
Josh Cummings 4 jaren geleden
bovenliggende
commit
4f06fc6ed1

+ 138 - 0
config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java

@@ -72,6 +72,7 @@ import org.springframework.security.config.annotation.web.configurers.oauth2.cli
 import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
 import org.springframework.security.config.annotation.web.configurers.openid.OpenIDLoginConfigurer;
 import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LoginConfigurer;
+import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
@@ -2209,6 +2210,143 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
 		return HttpSecurity.this;
 	}
 
+	/**
+	 * Configures logout support for an SAML 2.0 Relying Party. <br>
+	 * <br>
+	 *
+	 * Implements the <b>Single Logout Profile, using POST and REDIRECT bindings</b>, as
+	 * documented in the
+	 * <a target="_blank" href="https://docs.oasis-open.org/security/saml/">SAML V2.0
+	 * Core, Profiles and Bindings</a> specifications. <br>
+	 * <br>
+	 *
+	 * As a prerequisite to using this feature, is that you have a SAML v2.0 Asserting
+	 * Party to sent a logout request to. The representation of the relying party and the
+	 * asserting party is contained within {@link RelyingPartyRegistration}. <br>
+	 * <br>
+	 *
+	 * {@link RelyingPartyRegistration}(s) are composed within a
+	 * {@link RelyingPartyRegistrationRepository}, which is <b>required</b> and must be
+	 * registered with the {@link ApplicationContext} or configured via
+	 * {@link #saml2Login(Customizer)}.<br>
+	 * <br>
+	 *
+	 * The default configuration provides an auto-generated logout endpoint at
+	 * <code>&quot;/logout&quot;</code> and redirects to <code>/login?logout</code> when
+	 * logout completes. <br>
+	 * <br>
+	 *
+	 * <p>
+	 * <h2>Example Configuration</h2>
+	 *
+	 * The following example shows the minimal configuration required, using a
+	 * hypothetical asserting party.
+	 *
+	 * <pre>
+	 *	&#064;EnableWebSecurity
+	 *	&#064;Configuration
+	 *	public class Saml2LogoutSecurityConfig {
+	 *		&#064;Bean
+	 *		public SecurityFilterChain web(HttpSecurity http) throws Exception {
+	 *			http
+	 *				.authorizeRequests((authorize) -> authorize
+	 *					.anyRequest().authenticated()
+	 *				)
+	 *				.saml2Login(withDefaults())
+	 *				.saml2Logout(withDefaults());
+	 *			return http.build();
+	 *		}
+	 *
+	 *		&#064;Bean
+	 *		public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
+	 *			RelyingPartyRegistration registration = RelyingPartyRegistrations
+	 *					.withMetadataLocation("https://ap.example.org/metadata")
+	 *					.registrationId("simple")
+	 *					.build();
+	 *			return new InMemoryRelyingPartyRegistrationRepository(registration);
+	 *		}
+	 *	}
+	 * </pre>
+	 *
+	 * <p>
+	 * @return the {@link HttpSecurity} for further customizations
+	 * @throws Exception
+	 * @since 5.6
+	 */
+	public HttpSecurity saml2Logout(Customizer<Saml2LogoutConfigurer<HttpSecurity>> saml2LogoutCustomizer)
+			throws Exception {
+		saml2LogoutCustomizer.customize(getOrApply(new Saml2LogoutConfigurer<>(getContext())));
+		return HttpSecurity.this;
+	}
+
+	/**
+	 * Configures logout support for an SAML 2.0 Relying Party. <br>
+	 * <br>
+	 *
+	 * Implements the <b>Single Logout Profile, using POST and REDIRECT bindings</b>, as
+	 * documented in the
+	 * <a target="_blank" href="https://docs.oasis-open.org/security/saml/">SAML V2.0
+	 * Core, Profiles and Bindings</a> specifications. <br>
+	 * <br>
+	 *
+	 * As a prerequisite to using this feature, is that you have a SAML v2.0 Asserting
+	 * Party to sent a logout request to. The representation of the relying party and the
+	 * asserting party is contained within {@link RelyingPartyRegistration}. <br>
+	 * <br>
+	 *
+	 * {@link RelyingPartyRegistration}(s) are composed within a
+	 * {@link RelyingPartyRegistrationRepository}, which is <b>required</b> and must be
+	 * registered with the {@link ApplicationContext} or configured via
+	 * {@link #saml2Login()}.<br>
+	 * <br>
+	 *
+	 * The default configuration provides an auto-generated logout endpoint at
+	 * <code>&quot;/logout&quot;</code> and redirects to <code>/login?logout</code> when
+	 * logout completes. <br>
+	 * <br>
+	 *
+	 * <p>
+	 * <h2>Example Configuration</h2>
+	 *
+	 * The following example shows the minimal configuration required, using a
+	 * hypothetical asserting party.
+	 *
+	 * <pre>
+	 *	&#064;EnableWebSecurity
+	 *	&#064;Configuration
+	 *	public class Saml2LogoutSecurityConfig {
+	 *		&#064;Bean
+	 *		public SecurityFilterChain web(HttpSecurity http) throws Exception {
+	 *			http
+	 *				.authorizeRequests()
+	 *					.anyRequest().authenticated()
+	 *					.and()
+	 *				.saml2Login()
+	 *					.and()
+	 *				.saml2Logout();
+	 *			return http.build();
+	 *		}
+	 *
+	 *		&#064;Bean
+	 *		public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
+	 *			RelyingPartyRegistration registration = RelyingPartyRegistrations
+	 *					.withMetadataLocation("https://ap.example.org/metadata")
+	 *					.registrationId("simple")
+	 *					.build();
+	 *			return new InMemoryRelyingPartyRegistrationRepository(registration);
+	 *		}
+	 *	}
+	 * </pre>
+	 *
+	 * <p>
+	 * @return the {@link Saml2LoginConfigurer} for further customizations
+	 * @throws Exception
+	 * @since 5.6
+	 */
+	public Saml2LogoutConfigurer<HttpSecurity> saml2Logout() throws Exception {
+		return getOrApply(new Saml2LogoutConfigurer<>(getContext()));
+	}
+
 	/**
 	 * Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0
 	 * Provider. <br>

+ 3 - 2
config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java

@@ -250,10 +250,11 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>>
 	 * {@link SimpleUrlLogoutSuccessHandler} using the {@link #logoutSuccessUrl(String)}.
 	 * @return the {@link LogoutSuccessHandler} to use
 	 */
-	private LogoutSuccessHandler getLogoutSuccessHandler() {
+	public LogoutSuccessHandler getLogoutSuccessHandler() {
 		LogoutSuccessHandler handler = this.logoutSuccessHandler;
 		if (handler == null) {
 			handler = createDefaultSuccessHandler();
+			this.logoutSuccessHandler = handler;
 		}
 		return handler;
 	}
@@ -312,7 +313,7 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>>
 	 * Gets the {@link LogoutHandler} instances that will be used.
 	 * @return the {@link LogoutHandler} instances. Cannot be null.
 	 */
-	List<LogoutHandler> getLogoutHandlers() {
+	public List<LogoutHandler> getLogoutHandlers() {
 		return this.logoutHandlers;
 	}
 

+ 8 - 3
config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java

@@ -205,9 +205,7 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
 	@Override
 	public void init(B http) throws Exception {
 		registerDefaultCsrfOverride(http);
-		if (this.relyingPartyRegistrationRepository == null) {
-			this.relyingPartyRegistrationRepository = getSharedOrBean(http, RelyingPartyRegistrationRepository.class);
-		}
+		relyingPartyRegistrationRepository(http);
 		this.saml2WebSsoAuthenticationFilter = new Saml2WebSsoAuthenticationFilter(getAuthenticationConverter(http),
 				this.loginProcessingUrl);
 		setAuthenticationRequestRepository(http, this.saml2WebSsoAuthenticationFilter);
@@ -257,6 +255,13 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
 		}
 	}
 
+	RelyingPartyRegistrationRepository relyingPartyRegistrationRepository(B http) {
+		if (this.relyingPartyRegistrationRepository == null) {
+			this.relyingPartyRegistrationRepository = getSharedOrBean(http, RelyingPartyRegistrationRepository.class);
+		}
+		return this.relyingPartyRegistrationRepository;
+	}
+
 	private void setAuthenticationRequestRepository(B http,
 			Saml2WebSsoAuthenticationFilter saml2WebSsoAuthenticationFilter) {
 		saml2WebSsoAuthenticationFilter.setAuthenticationRequestRepository(getAuthenticationRequestRepository(http));

+ 523 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java

@@ -0,0 +1,523 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.annotation.web.configurers.saml2;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Predicate;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.opensaml.core.Version;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.security.authentication.AuthenticationManager;
+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.core.context.SecurityContextHolder;
+import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
+import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutRequestValidator;
+import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutResponseValidator;
+import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator;
+import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
+import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
+import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml3LogoutRequestResolver;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml3LogoutResponseResolver;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseFilter;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2RelyingPartyInitiatedLogoutSuccessHandler;
+import org.springframework.security.web.authentication.logout.LogoutFilter;
+import org.springframework.security.web.authentication.logout.LogoutHandler;
+import org.springframework.security.web.authentication.logout.LogoutSuccessEventPublishingLogoutHandler;
+import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
+import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
+import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
+import org.springframework.security.web.csrf.CsrfFilter;
+import org.springframework.security.web.csrf.CsrfLogoutHandler;
+import org.springframework.security.web.csrf.CsrfTokenRepository;
+import org.springframework.security.web.util.matcher.AndRequestMatcher;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+
+/**
+ * Adds SAML 2.0 logout support.
+ *
+ * <h2>Security Filters</h2>
+ *
+ * The following Filters are populated
+ *
+ * <ul>
+ * <li>{@link LogoutFilter}</li>
+ * <li>{@link Saml2LogoutRequestFilter}</li>
+ * <li>{@link Saml2LogoutResponseFilter}</li>
+ * </ul>
+ *
+ * <p>
+ * The following configuration options are available:
+ *
+ * <ul>
+ * <li>{@link #logoutUrl} - The URL to to process SAML 2.0 Logout</li>
+ * <li>{@link LogoutRequestConfigurer#logoutRequestValidator} - The
+ * {@link AuthenticationManager} for authenticating SAML 2.0 Logout Requests</li>
+ * <li>{@link LogoutRequestConfigurer#logoutRequestResolver} - The
+ * {@link Saml2LogoutRequestResolver} for creating SAML 2.0 Logout Requests</li>
+ * <li>{@link LogoutRequestConfigurer#logoutRequestRepository} - The
+ * {@link Saml2LogoutRequestRepository} for storing SAML 2.0 Logout Requests</li>
+ * <li>{@link LogoutResponseConfigurer#logoutResponseValidator} - The
+ * {@link AuthenticationManager} for authenticating SAML 2.0 Logout Responses</li>
+ * <li>{@link LogoutResponseConfigurer#logoutResponseResolver} - The
+ * {@link Saml2LogoutResponseResolver} for creating SAML 2.0 Logout Responses</li>
+ * </ul>
+ *
+ * <h2>Shared Objects Created</h2>
+ *
+ * No shared Objects are created
+ *
+ * <h2>Shared Objects Used</h2>
+ *
+ * Uses {@link CsrfTokenRepository} to add the {@link CsrfLogoutHandler}.
+ *
+ * @author Josh Cummings
+ * @since 5.6
+ * @see Saml2LogoutConfigurer
+ */
+public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
+		extends AbstractHttpConfigurer<Saml2LogoutConfigurer<H>, H> {
+
+	private ApplicationContext context;
+
+	private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
+
+	private String logoutUrl = "/logout";
+
+	private List<LogoutHandler> logoutHandlers = new ArrayList<>();
+
+	private LogoutSuccessHandler logoutSuccessHandler;
+
+	private LogoutRequestConfigurer logoutRequestConfigurer;
+
+	private LogoutResponseConfigurer logoutResponseConfigurer;
+
+	/**
+	 * Creates a new instance
+	 * @see HttpSecurity#logout()
+	 */
+	public Saml2LogoutConfigurer(ApplicationContext context) {
+		this.context = context;
+		this.logoutHandlers.add(new SecurityContextLogoutHandler());
+		this.logoutHandlers.add(new LogoutSuccessEventPublishingLogoutHandler());
+		SimpleUrlLogoutSuccessHandler logoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
+		logoutSuccessHandler.setDefaultTargetUrl("/login?logout");
+		this.logoutSuccessHandler = logoutSuccessHandler;
+		this.logoutRequestConfigurer = new LogoutRequestConfigurer();
+		this.logoutResponseConfigurer = new LogoutResponseConfigurer();
+	}
+
+	/**
+	 * The URL by which the relying or asserting party can trigger logout.
+	 *
+	 * <p>
+	 * The Relying Party triggers logout by POSTing to the endpoint. The Asserting Party
+	 * triggers logout based on what is specified by
+	 * {@link RelyingPartyRegistration#getSingleLogoutServiceBinding()}.
+	 * @param logoutUrl the URL that will invoke logout
+	 * @return the {@link LogoutConfigurer} for further customizations
+	 * @see LogoutConfigurer#logoutUrl(String)
+	 * @see HttpSecurity#csrf()
+	 */
+	public Saml2LogoutConfigurer<H> logoutUrl(String logoutUrl) {
+		this.logoutUrl = logoutUrl;
+		return this;
+	}
+
+	/**
+	 * Sets the {@link RelyingPartyRegistrationRepository} of relying parties, each party
+	 * representing a service provider, SP and this host, and identity provider, IDP pair
+	 * that communicate with each other.
+	 * @param repo the repository of relying parties
+	 * @return the {@link Saml2LogoutConfigurer} for further customizations
+	 */
+	public Saml2LogoutConfigurer<H> relyingPartyRegistrationRepository(RelyingPartyRegistrationRepository repo) {
+		this.relyingPartyRegistrationRepository = repo;
+		return this;
+	}
+
+	/**
+	 * Get configurer for SAML 2.0 Logout Request components
+	 * @return the {@link LogoutRequestConfigurer} for further customizations
+	 */
+	public LogoutRequestConfigurer logoutRequest() {
+		return this.logoutRequestConfigurer;
+	}
+
+	/**
+	 * Configures SAML 2.0 Logout Request components
+	 * @param logoutRequestConfigurerCustomizer the {@link Customizer} to provide more
+	 * options for the {@link LogoutRequestConfigurer}
+	 * @return the {@link Saml2LogoutConfigurer} for further customizations
+	 */
+	public Saml2LogoutConfigurer<H> logoutRequest(
+			Customizer<LogoutRequestConfigurer> logoutRequestConfigurerCustomizer) {
+		logoutRequestConfigurerCustomizer.customize(this.logoutRequestConfigurer);
+		return this;
+	}
+
+	/**
+	 * Get configurer for SAML 2.0 Logout Response components
+	 * @return the {@link LogoutResponseConfigurer} for further customizations
+	 */
+	public LogoutResponseConfigurer logoutResponse() {
+		return this.logoutResponseConfigurer;
+	}
+
+	/**
+	 * Configures SAML 2.0 Logout Request components
+	 * @param logoutResponseConfigurerCustomizer the {@link Customizer} to provide more
+	 * options for the {@link LogoutResponseConfigurer}
+	 * @return the {@link Saml2LogoutConfigurer} for further customizations
+	 */
+	public Saml2LogoutConfigurer<H> logoutResponse(
+			Customizer<LogoutResponseConfigurer> logoutResponseConfigurerCustomizer) {
+		logoutResponseConfigurerCustomizer.customize(this.logoutResponseConfigurer);
+		return this;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void configure(H http) throws Exception {
+		LogoutConfigurer<H> logout = http.getConfigurer(LogoutConfigurer.class);
+		if (logout != null) {
+			this.logoutHandlers = logout.getLogoutHandlers();
+			this.logoutSuccessHandler = logout.getLogoutSuccessHandler();
+		}
+		RelyingPartyRegistrationResolver registrations = relyingPartyRegistrationResolver(http);
+		http.addFilterBefore(createLogoutRequestProcessingFilter(registrations), CsrfFilter.class);
+		http.addFilterBefore(createLogoutResponseProcessingFilter(registrations), CsrfFilter.class);
+		http.addFilterBefore(createRelyingPartyLogoutFilter(registrations), LogoutFilter.class);
+	}
+
+	private RelyingPartyRegistrationResolver relyingPartyRegistrationResolver(H http) {
+		RelyingPartyRegistrationRepository registrations = getRelyingPartyRegistrationRepository(http);
+		return new DefaultRelyingPartyRegistrationResolver(registrations);
+	}
+
+	private RelyingPartyRegistrationRepository getRelyingPartyRegistrationRepository(H http) {
+		if (this.relyingPartyRegistrationRepository != null) {
+			return this.relyingPartyRegistrationRepository;
+		}
+		Saml2LoginConfigurer<H> login = http.getConfigurer(Saml2LoginConfigurer.class);
+		if (login != null) {
+			this.relyingPartyRegistrationRepository = login.relyingPartyRegistrationRepository(http);
+		}
+		else {
+			this.relyingPartyRegistrationRepository = getBeanOrNull(RelyingPartyRegistrationRepository.class);
+		}
+		return this.relyingPartyRegistrationRepository;
+	}
+
+	private Saml2LogoutRequestFilter createLogoutRequestProcessingFilter(
+			RelyingPartyRegistrationResolver registrations) {
+		LogoutHandler[] logoutHandlers = this.logoutHandlers.toArray(new LogoutHandler[0]);
+		Saml2LogoutResponseResolver logoutResponseResolver = createSaml2LogoutResponseResolver(registrations);
+		Saml2LogoutRequestFilter filter = new Saml2LogoutRequestFilter(registrations,
+				this.logoutRequestConfigurer.logoutRequestValidator(), logoutResponseResolver, logoutHandlers);
+		filter.setLogoutRequestMatcher(createLogoutRequestMatcher());
+		return filter;
+	}
+
+	private Saml2LogoutResponseFilter createLogoutResponseProcessingFilter(
+			RelyingPartyRegistrationResolver registrations) {
+		Saml2LogoutResponseFilter logoutResponseFilter = new Saml2LogoutResponseFilter(registrations,
+				this.logoutResponseConfigurer.logoutResponseValidator(), this.logoutSuccessHandler);
+		logoutResponseFilter.setLogoutRequestMatcher(createLogoutResponseMatcher());
+		logoutResponseFilter.setLogoutRequestRepository(this.logoutRequestConfigurer.logoutRequestRepository);
+		return logoutResponseFilter;
+	}
+
+	private LogoutFilter createRelyingPartyLogoutFilter(RelyingPartyRegistrationResolver registrations) {
+		LogoutHandler[] logoutHandlers = this.logoutHandlers.toArray(new LogoutHandler[0]);
+		Saml2RelyingPartyInitiatedLogoutSuccessHandler logoutRequestSuccessHandler = createSaml2LogoutRequestSuccessHandler(
+				registrations);
+		LogoutFilter logoutFilter = new LogoutFilter(logoutRequestSuccessHandler, logoutHandlers);
+		logoutFilter.setLogoutRequestMatcher(createLogoutMatcher());
+		return logoutFilter;
+	}
+
+	private RequestMatcher createLogoutMatcher() {
+		RequestMatcher logout = new AntPathRequestMatcher(this.logoutUrl, "POST");
+		RequestMatcher saml2 = new Saml2RequestMatcher();
+		return new AndRequestMatcher(logout, saml2);
+	}
+
+	private RequestMatcher createLogoutRequestMatcher() {
+		RequestMatcher logout = new AntPathRequestMatcher(this.logoutRequestConfigurer.logoutUrl);
+		RequestMatcher samlRequest = new ParameterRequestMatcher("SAMLRequest");
+		return new AndRequestMatcher(logout, samlRequest);
+	}
+
+	private RequestMatcher createLogoutResponseMatcher() {
+		RequestMatcher logout = new AntPathRequestMatcher(this.logoutResponseConfigurer.logoutUrl);
+		RequestMatcher samlResponse = new ParameterRequestMatcher("SAMLResponse");
+		return new AndRequestMatcher(logout, samlResponse);
+	}
+
+	private Saml2RelyingPartyInitiatedLogoutSuccessHandler createSaml2LogoutRequestSuccessHandler(
+			RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
+		Saml2LogoutRequestResolver logoutRequestResolver = this.logoutRequestConfigurer
+				.logoutRequestResolver(relyingPartyRegistrationResolver);
+		return new Saml2RelyingPartyInitiatedLogoutSuccessHandler(logoutRequestResolver);
+	}
+
+	private Saml2LogoutResponseResolver createSaml2LogoutResponseResolver(
+			RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
+		return this.logoutResponseConfigurer.logoutResponseResolver(relyingPartyRegistrationResolver);
+	}
+
+	private <C> C getBeanOrNull(Class<C> clazz) {
+		if (this.context == null) {
+			return null;
+		}
+		if (this.context.getBeanNamesForType(clazz).length == 0) {
+			return null;
+		}
+		return this.context.getBean(clazz);
+	}
+
+	private String version() {
+		String version = Version.getVersion();
+		if (version != null) {
+			return version;
+		}
+		return Version.class.getModule().getDescriptor().version().map(Object::toString)
+				.orElseThrow(() -> new IllegalStateException("cannot determine OpenSAML version"));
+	}
+
+	/**
+	 * A configurer for SAML 2.0 LogoutRequest components
+	 */
+	public final class LogoutRequestConfigurer {
+
+		private String logoutUrl = "/logout/saml2/slo";
+
+		private Saml2LogoutRequestValidator logoutRequestValidator;
+
+		private Saml2LogoutRequestResolver logoutRequestResolver;
+
+		private Saml2LogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository();
+
+		LogoutRequestConfigurer() {
+		}
+
+		/**
+		 * The URL by which the asserting party can send a SAML 2.0 Logout Request
+		 *
+		 * <p>
+		 * The Asserting Party should use whatever HTTP method specified in
+		 * {@link RelyingPartyRegistration#getSingleLogoutServiceBinding()}.
+		 * @param logoutUrl the URL that will receive the SAML 2.0 Logout Request
+		 * @return the {@link LogoutRequestConfigurer} for further customizations
+		 * @see Saml2LogoutConfigurer#logoutUrl(String)
+		 */
+		public LogoutRequestConfigurer logoutUrl(String logoutUrl) {
+			this.logoutUrl = logoutUrl;
+			return this;
+		}
+
+		/**
+		 * Use this {@link LogoutHandler} for processing a logout request from the
+		 * asserting party
+		 * @param authenticator the {@link Saml2LogoutRequestValidator} to use
+		 * @return the {@link LogoutRequestConfigurer} for further customizations
+		 */
+		public LogoutRequestConfigurer logoutRequestValidator(Saml2LogoutRequestValidator authenticator) {
+			this.logoutRequestValidator = authenticator;
+			return this;
+		}
+
+		/**
+		 * Use this {@link Saml2LogoutRequestResolver} for producing a logout request to
+		 * send to the asserting party
+		 * @param logoutRequestResolver the {@link Saml2LogoutRequestResolver} to use
+		 * @return the {@link LogoutRequestConfigurer} for further customizations
+		 */
+		public LogoutRequestConfigurer logoutRequestResolver(Saml2LogoutRequestResolver logoutRequestResolver) {
+			this.logoutRequestResolver = logoutRequestResolver;
+			return this;
+		}
+
+		/**
+		 * Use this {@link Saml2LogoutRequestRepository} for storing logout requests
+		 * @param logoutRequestRepository the {@link Saml2LogoutRequestRepository} to use
+		 * @return the {@link LogoutRequestConfigurer} for further customizations
+		 */
+		public LogoutRequestConfigurer logoutRequestRepository(Saml2LogoutRequestRepository logoutRequestRepository) {
+			this.logoutRequestRepository = logoutRequestRepository;
+			return this;
+		}
+
+		public Saml2LogoutConfigurer<H> and() {
+			return Saml2LogoutConfigurer.this;
+		}
+
+		private Saml2LogoutRequestValidator logoutRequestValidator() {
+			if (this.logoutRequestValidator == null) {
+				return new OpenSamlLogoutRequestValidator();
+			}
+			return this.logoutRequestValidator;
+		}
+
+		private Saml2LogoutRequestResolver logoutRequestResolver(
+				RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
+			if (this.logoutRequestResolver != null) {
+				return this.logoutRequestResolver;
+			}
+			if (version().startsWith("4")) {
+				return new OpenSaml4LogoutRequestResolver(relyingPartyRegistrationResolver);
+			}
+			return new OpenSaml3LogoutRequestResolver(relyingPartyRegistrationResolver);
+		}
+
+	}
+
+	public final class LogoutResponseConfigurer {
+
+		private String logoutUrl = "/logout/saml2/slo";
+
+		private Saml2LogoutResponseValidator logoutResponseValidator;
+
+		private Saml2LogoutResponseResolver logoutResponseResolver;
+
+		LogoutResponseConfigurer() {
+		}
+
+		/**
+		 * The URL by which the asserting party can send a SAML 2.0 Logout Response
+		 *
+		 * <p>
+		 * The Asserting Party should use whatever HTTP method specified in
+		 * {@link RelyingPartyRegistration#getSingleLogoutServiceBinding()}.
+		 * @param logoutUrl the URL that will receive the SAML 2.0 Logout Response
+		 * @return the {@link LogoutResponseConfigurer} for further customizations
+		 * @see Saml2LogoutConfigurer#logoutUrl(String)
+		 */
+		public LogoutResponseConfigurer logoutUrl(String logoutUrl) {
+			this.logoutUrl = logoutUrl;
+			return this;
+		}
+
+		/**
+		 * Use this {@link LogoutHandler} for processing a logout response from the
+		 * asserting party
+		 * @param authenticator the {@link AuthenticationManager} to use
+		 * @return the {@link LogoutRequestConfigurer} for further customizations
+		 */
+		public LogoutResponseConfigurer logoutResponseValidator(Saml2LogoutResponseValidator authenticator) {
+			this.logoutResponseValidator = authenticator;
+			return this;
+		}
+
+		/**
+		 * Use this {@link Saml2LogoutRequestResolver} for producing a logout response to
+		 * send to the asserting party
+		 * @param logoutResponseResolver the {@link Saml2LogoutResponseResolver} to use
+		 * @return the {@link LogoutRequestConfigurer} for further customizations
+		 */
+		public LogoutResponseConfigurer logoutResponseResolver(Saml2LogoutResponseResolver logoutResponseResolver) {
+			this.logoutResponseResolver = logoutResponseResolver;
+			return this;
+		}
+
+		public Saml2LogoutConfigurer<H> and() {
+			return Saml2LogoutConfigurer.this;
+		}
+
+		private Saml2LogoutResponseValidator logoutResponseValidator() {
+			if (this.logoutResponseValidator == null) {
+				return new OpenSamlLogoutResponseValidator();
+			}
+			return this.logoutResponseValidator;
+		}
+
+		private Saml2LogoutResponseResolver logoutResponseResolver(
+				RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
+			if (this.logoutResponseResolver == null) {
+				if (version().startsWith("4")) {
+					return new OpenSaml4LogoutResponseResolver(relyingPartyRegistrationResolver);
+				}
+				return new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver);
+			}
+			return this.logoutResponseResolver;
+		}
+
+	}
+
+	private static class Saml2RequestMatcher implements RequestMatcher {
+
+		@Override
+		public boolean matches(HttpServletRequest request) {
+			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+			if (authentication == null) {
+				return false;
+			}
+			return authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal;
+		}
+
+	}
+
+	private static class ParameterRequestMatcher implements RequestMatcher {
+
+		Predicate<String> test = Objects::nonNull;
+
+		String name;
+
+		ParameterRequestMatcher(String name) {
+			this.name = name;
+		}
+
+		@Override
+		public boolean matches(HttpServletRequest request) {
+			return this.test.test(request.getParameter(this.name));
+		}
+
+	}
+
+	private static class NoopLogoutHandler implements LogoutHandler {
+
+		@Override
+		public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
+		}
+
+	}
+
+}

+ 6 - 4
config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java

@@ -97,7 +97,9 @@ public class LogoutConfigurerTests {
 	@Test
 	public void configureWhenRegisteringObjectPostProcessorThenInvokedOnLogoutFilter() {
 		this.spring.register(ObjectPostProcessorConfig.class).autowire();
-		verify(ObjectPostProcessorConfig.objectPostProcessor).postProcess(any(LogoutFilter.class));
+		ObjectPostProcessor<LogoutFilter> objectPostProcessor = this.spring.getContext()
+				.getBean(ObjectPostProcessor.class);
+		verify(objectPostProcessor).postProcess(any(LogoutFilter.class));
 	}
 
 	@Test
@@ -361,7 +363,7 @@ public class LogoutConfigurerTests {
 	@EnableWebSecurity
 	static class ObjectPostProcessorConfig extends WebSecurityConfigurerAdapter {
 
-		static ObjectPostProcessor<Object> objectPostProcessor = spy(ReflectingObjectPostProcessor.class);
+		ObjectPostProcessor<Object> objectPostProcessor = spy(ReflectingObjectPostProcessor.class);
 
 		@Override
 		protected void configure(HttpSecurity http) throws Exception {
@@ -372,8 +374,8 @@ public class LogoutConfigurerTests {
 		}
 
 		@Bean
-		static ObjectPostProcessor<Object> objectPostProcessor() {
-			return objectPostProcessor;
+		ObjectPostProcessor<Object> objectPostProcessor() {
+			return this.objectPostProcessor;
 		}
 
 	}

+ 493 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java

@@ -0,0 +1,493 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.annotation.web.configurers.saml2;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.function.Consumer;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.opensaml.saml.saml2.core.LogoutRequest;
+import org.opensaml.xmlsec.signature.support.SignatureConstants;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Import;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.mock.web.MockHttpSession;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.test.SpringTestContext;
+import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.saml2.core.Saml2Utils;
+import org.springframework.security.saml2.core.Saml2X509Credential;
+import org.springframework.security.saml2.core.TestSaml2X509Credentials;
+import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal;
+import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
+import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
+import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
+import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator;
+import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
+import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator;
+import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutValidatorResult;
+import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
+import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
+import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.logout.LogoutHandler;
+import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+
+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;
+import static org.mockito.BDDMockito.mock;
+import static org.mockito.BDDMockito.verify;
+import static org.mockito.BDDMockito.verifyNoInteractions;
+import static org.springframework.security.config.Customizer.withDefaults;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Tests for different Java configuration for {@link Saml2LogoutConfigurer}
+ */
+@ExtendWith(SpringTestContextExtension.class)
+public class Saml2LogoutConfigurerTests {
+
+	@Autowired
+	private ConfigurableApplicationContext context;
+
+	@Autowired
+	private RelyingPartyRegistrationRepository repository;
+
+	private final Saml2LogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository();
+
+	public final SpringTestContext spring = new SpringTestContext(this);
+
+	@Autowired(required = false)
+	MockMvc mvc;
+
+	private Saml2Authentication user;
+
+	String apLogoutRequest = "nZFBa4MwGIb/iuQeE2NTXFDLQAaC26Hrdtgt1dQFNMnyxdH9+zlboeyww275SN7nzcOX787jEH0qD9qaAiUxRZEyre206Qv0cnjAGdqVOchxYE40trdT2KuPSUGI5qQBcbkq0OSNsBI0CCNHBSK04vn+sREspsJ5G2xrBxRVc1AbGZa29xAcCEK8i9VZjm5QsfU9GZYWsoCJv5ShqK4K1Ow5p5LyU4aP6XaLN3cpw9mGctydjrxNaZt1XM5vASZVGwjShAIxyhJMU8z4gSWCM8GSmDH+hqLX1Xv+JLpaiiXsb+3+lpMAyv8IoVI6rEzQ4QvrLie3uBX+NMfr6l/waT6t0AumvI6/FlN+Aw==";
+
+	String apLogoutRequestSigAlg = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256;
+
+	String apLogoutRequestRelayState = "33591874-b123-4f2c-ab0d-2d0d84aa8b56";
+
+	String apLogoutRequestSignature = "oKqdzrmn2YAqXcwkow2lzRXr5PNHm0s/gWsRnaZYhC+Oq5ekK5uIKQYvtmNR94HJjDe1VRs+vVQCYivgdoTzBV2ZlffTXZmYsCsY9q4jbCWR6R5CbhU73/MkKQsPcyVvMhNYxnDYapIlxDsfoZNTboDEz3GM+HRoGRfl9emCXY0lPRYwqC4kpu7oMDBkafR0A09jPIxFuNpqlLPwUxL9m+DGkvDK3mFDN1xJcgZaK73HcuJe7Qh4huOrKNFetwc5EvqfiwgiWF6sfq9A+rZBfCIYo10NNLY7fNQAR2IqwcKtawHgTGWbeshRyFrwVYMR64EnClfxUHsHKf5kiZ2dlw==";
+
+	String apLogoutResponse = "fZHRa4MwEMb/Fcl7jEadGqplrAwK3Uvb9WFvZ4ydoInk4uj++1nXbmWMvhwcd9/3Jb9bLE99530oi63RBQn9gHhKS1O3+liQ1/0zzciyXCD0HR/ExhzN6LYKB6NReZNUo/ieFWS0WhjAFoWGXqFwUuweXzaC+4EYrHFGmo54K4Wu1eDmuHfnBhSM2cFXJ+iHTvnGHlk3x7DZmNlLGvHWq4Jstk0GUSjjiIZJI2lcpQnNeRLTAOo4fwCeQg3Trr6+cm/OqmnWVHECVGWQ0jgCSatsKvXUxhFvZF7xSYU4qrVGB9oVhAc8pEFEebLnkeBc8NyPePpGvMOV1/Q3cqEjZrG9hXKfCSAqe+ZAShio0q51n7StF+zW7gf9zoEb8U/7ZGrlHaAb1f0onLfFbpRSIRJWXkJ+bdm/Fy6/AA==";
+
+	String apLogoutResponseSigAlg = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256;
+
+	String apLogoutResponseRelayState = "8f63887a-ec7e-4149-b6a0-dd730017f315";
+
+	String apLogoutResponseSignature = "h2fDqSIBfmnkRHKDMY4IxkCXcI0w98ydNsnPmv1b7GTZCWLbJ+oxaP2yZNPw7wOWXTv86cTPwKLjx5halKy5C+hhWnT0haKhuMcUvHlsgAMBbJKLV+1afzL4O77cvAQJmMNRK7ugXGNV5PTEnd1U4voy134OgdD5XycYiFVRZOwP5H84eJ9xxlvqQwqDvZTcgiF/ZS4ioZgzgnIFcbagZQ12LWNh26OMaUpIW04kCeO6t2dUsxOL6nZWvNrX/Zx1sORIpu4doDUa1RYC8YnjZeQEzDqUVC/dBO/mbVJ/hbF9tD0jBUx7YIgoXpqsWK4TcCsvmlmhrJXvGxDyoAWu2Q==";
+
+	String rpLogoutRequest = "nZFBa4MwGIb/iuQeY6NlGtQykIHgdui6HXaLmrqAJlm+OLp/v0wrlB122CXkI3mfNw/JD5dpDD6FBalVgXZhhAKhOt1LNRTo5fSAU3Qoc+DTSA1r9KBndxQfswAX+KQCth4VaLaKaQ4SmOKTAOY69nz/2DAaRsxY7XSnRxRUPigVd0vbu3MGGCHchOLCJzOKUNuBjEsLWcDErmUoqKsCNcc+yc5tsudYpPwOJzHvcJv6pfdjEtNzl7XU3wWYRa3AceUKRCO6w1GM6f5EY0Ypo1lIk+gNBa+bt38kulqyJWxv7f6W4wDC/gih0hoslJPuC8s+J7e4Df7k43X1L/jsdxt0xZTX8dfHlN8=";
+
+	String rpLogoutRequestId = "LRd49fb45a-e8a7-43ac-b8ac-d8a7432fc9b2";
+
+	String rpLogoutRequestRelayState = "8f63887a-ec7e-4149-b6a0-dd730017f315";
+
+	String rpLogoutRequestSignature = "h2fDqSIBfmnkRHKDMY4IxkCXcI0w98ydNsnPmv1b7GTZCWLbJ+oxaP2yZNPw7wOWXTv86cTPwKLjx5halKy5C+hhWnT0haKhuMcUvHlsgAMBbJKLV+1afzL4O77cvAQJmMNRK7ugXGNV5PTEnd1U4voy134OgdD5XycYiFVRZOwP5H84eJ9xxlvqQwqDvZTcgiF/ZS4ioZgzgnIFcbagZQ12LWNh26OMaUpIW04kCeO6t2dUsxOL6nZWvNrX/Zx1sORIpu4doDUa1RYC8YnjZeQEzDqUVC/dBO/mbVJ/hbF9tD0jBUx7YIgoXpqsWK4TcCsvmlmhrJXvGxDyoAWu2Q==";
+
+	private MockHttpServletRequest request;
+
+	private MockHttpServletResponse response;
+
+	@BeforeEach
+	public void setup() {
+		DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user",
+				Collections.emptyMap());
+		principal.setRelyingPartyRegistrationId("registration-id");
+		this.user = new Saml2Authentication(principal, "response", AuthorityUtils.createAuthorityList("ROLE_USER"));
+		this.request = new MockHttpServletRequest("POST", "");
+		this.request.setServletPath("/login/saml2/sso/test-rp");
+		this.response = new MockHttpServletResponse();
+	}
+
+	@AfterEach
+	public void cleanup() {
+		if (this.context != null) {
+			this.context.close();
+		}
+	}
+
+	@Test
+	public void logoutWhenDefaultsAndNotSaml2LoginThenDefaultLogout() throws Exception {
+		this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
+		TestingAuthenticationToken user = new TestingAuthenticationToken("user", "password");
+		MvcResult result = this.mvc.perform(post("/logout").with(authentication(user)).with(csrf()))
+				.andExpect(status().isFound()).andReturn();
+		String location = result.getResponse().getHeader("Location");
+		LogoutHandler logoutHandler = this.spring.getContext().getBean(LogoutHandler.class);
+		assertThat(location).isEqualTo("/login?logout");
+		verify(logoutHandler).logout(any(), any(), any());
+	}
+
+	@Test
+	public void saml2LogoutWhenDefaultsThenLogsOutAndSendsLogoutRequest() throws Exception {
+		this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
+		MvcResult result = this.mvc.perform(post("/logout").with(authentication(this.user)).with(csrf()))
+				.andExpect(status().isFound()).andReturn();
+		String location = result.getResponse().getHeader("Location");
+		LogoutHandler logoutHandler = this.spring.getContext().getBean(LogoutHandler.class);
+		assertThat(location).startsWith("https://ap.example.org/logout/saml2/request");
+		verify(logoutHandler).logout(any(), any(), any());
+	}
+
+	@Test
+	public void saml2LogoutWhenUnauthenticatedThenEntryPoint() throws Exception {
+		this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
+		this.mvc.perform(post("/logout").with(csrf())).andExpect(status().isFound())
+				.andExpect(redirectedUrl("/login?logout"));
+	}
+
+	@Test
+	public void saml2LogoutWhenMissingCsrfThen403() throws Exception {
+		this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
+		this.mvc.perform(post("/logout").with(authentication(this.user))).andExpect(status().isForbidden());
+		verifyNoInteractions(getBean(LogoutHandler.class));
+	}
+
+	@Test
+	public void saml2LogoutWhenGetThenDefaultLogoutPage() throws Exception {
+		this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
+		MvcResult result = this.mvc.perform(get("/logout").with(authentication(this.user))).andExpect(status().isOk())
+				.andReturn();
+		assertThat(result.getResponse().getContentAsString()).contains("Are you sure you want to log out?");
+		verifyNoInteractions(getBean(LogoutHandler.class));
+	}
+
+	@Test
+	public void saml2LogoutWhenPutOrDeleteThen404() throws Exception {
+		this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
+		this.mvc.perform(put("/logout").with(authentication(this.user)).with(csrf())).andExpect(status().isNotFound());
+		this.mvc.perform(delete("/logout").with(authentication(this.user)).with(csrf()))
+				.andExpect(status().isNotFound());
+		verifyNoInteractions(this.spring.getContext().getBean(LogoutHandler.class));
+	}
+
+	@Test
+	public void saml2LogoutWhenNoRegistrationThen401() throws Exception {
+		this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
+		DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user",
+				Collections.emptyMap());
+		principal.setRelyingPartyRegistrationId("wrong");
+		Saml2Authentication authentication = new Saml2Authentication(principal, "response",
+				AuthorityUtils.createAuthorityList("ROLE_USER"));
+		this.mvc.perform(post("/logout").with(authentication(authentication)).with(csrf()))
+				.andExpect(status().isUnauthorized());
+	}
+
+	@Test
+	public void saml2LogoutWhenCsrfDisabledAndNoAuthenticationThenFinalRedirect() throws Exception {
+		this.spring.register(Saml2LogoutCsrfDisabledConfig.class).autowire();
+		this.mvc.perform(post("/logout"));
+		LogoutSuccessHandler logoutSuccessHandler = this.spring.getContext().getBean(LogoutSuccessHandler.class);
+		verify(logoutSuccessHandler).onLogoutSuccess(any(), any(), any());
+	}
+
+	@Test
+	public void saml2LogoutWhenCustomLogoutRequestResolverThenUses() throws Exception {
+		this.spring.register(Saml2LogoutComponentsConfig.class).autowire();
+		RelyingPartyRegistration registration = this.repository.findByRegistrationId("registration-id");
+		Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
+				.samlRequest(this.rpLogoutRequest).id(this.rpLogoutRequestId).relayState(this.rpLogoutRequestRelayState)
+				.parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature)).build();
+		given(getBean(Saml2LogoutRequestResolver.class).resolve(any(), any())).willReturn(logoutRequest);
+		this.mvc.perform(post("/logout").with(authentication(this.user)).with(csrf()));
+		verify(getBean(Saml2LogoutRequestResolver.class)).resolve(any(), any());
+	}
+
+	@Test
+	public void saml2LogoutRequestWhenDefaultsThenLogsOutAndSendsLogoutResponse() throws Exception {
+		this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
+		DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user",
+				Collections.emptyMap());
+		principal.setRelyingPartyRegistrationId("get");
+		Saml2Authentication user = new Saml2Authentication(principal, "response",
+				AuthorityUtils.createAuthorityList("ROLE_USER"));
+		MvcResult result = this.mvc
+				.perform(get("/logout/saml2/slo").param("SAMLRequest", this.apLogoutRequest)
+						.param("RelayState", this.apLogoutRequestRelayState).param("SigAlg", this.apLogoutRequestSigAlg)
+						.param("Signature", this.apLogoutRequestSignature).with(authentication(user)))
+				.andExpect(status().isFound()).andReturn();
+		String location = result.getResponse().getHeader("Location");
+		assertThat(location).startsWith("https://ap.example.org/logout/saml2/response");
+		verify(getBean(LogoutHandler.class)).logout(any(), any(), any());
+	}
+
+	@Test
+	public void saml2LogoutRequestWhenNoRegistrationThen400() throws Exception {
+		this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
+		DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user",
+				Collections.emptyMap());
+		principal.setRelyingPartyRegistrationId("wrong");
+		Saml2Authentication user = new Saml2Authentication(principal, "response",
+				AuthorityUtils.createAuthorityList("ROLE_USER"));
+		this.mvc.perform(get("/logout/saml2/slo").param("SAMLRequest", this.apLogoutRequest)
+				.param("RelayState", this.apLogoutRequestRelayState).param("SigAlg", this.apLogoutRequestSigAlg)
+				.param("Signature", this.apLogoutRequestSignature).with(authentication(user)))
+				.andExpect(status().isBadRequest());
+		verifyNoInteractions(getBean(LogoutHandler.class));
+	}
+
+	@Test
+	public void saml2LogoutRequestWhenInvalidSamlRequestThen401() throws Exception {
+		this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
+		this.mvc.perform(get("/logout/saml2/slo").param("SAMLRequest", this.apLogoutRequest)
+				.param("RelayState", this.apLogoutRequestRelayState).param("SigAlg", this.apLogoutRequestSigAlg)
+				.with(authentication(this.user))).andExpect(status().isUnauthorized());
+		verifyNoInteractions(getBean(LogoutHandler.class));
+	}
+
+	@Test
+	public void saml2LogoutRequestWhenCustomLogoutRequestHandlerThenUses() throws Exception {
+		this.spring.register(Saml2LogoutComponentsConfig.class).autowire();
+		RelyingPartyRegistration registration = this.repository.findByRegistrationId("registration-id");
+		LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
+		logoutRequest.setIssueInstant(Instant.now());
+		given(getBean(Saml2LogoutRequestValidator.class).validate(any()))
+				.willReturn(Saml2LogoutValidatorResult.success());
+		Saml2LogoutResponse logoutResponse = Saml2LogoutResponse.withRelyingPartyRegistration(registration).build();
+		given(getBean(Saml2LogoutResponseResolver.class).resolve(any(), any())).willReturn(logoutResponse);
+		this.mvc.perform(post("/logout/saml2/slo").param("SAMLRequest", "samlRequest").with(authentication(this.user)))
+				.andReturn();
+		verify(getBean(Saml2LogoutRequestValidator.class)).validate(any());
+		verify(getBean(Saml2LogoutResponseResolver.class)).resolve(any(), any());
+	}
+
+	@Test
+	public void saml2LogoutResponseWhenDefaultsThenRedirects() throws Exception {
+		this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
+		RelyingPartyRegistration registration = this.repository.findByRegistrationId("get");
+		Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
+				.samlRequest(this.rpLogoutRequest).id(this.rpLogoutRequestId).relayState(this.rpLogoutRequestRelayState)
+				.parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature)).build();
+		this.logoutRequestRepository.saveLogoutRequest(logoutRequest, this.request, this.response);
+		this.request.setParameter("RelayState", logoutRequest.getRelayState());
+		assertThat(this.logoutRequestRepository.loadLogoutRequest(this.request)).isNotNull();
+		this.mvc.perform(get("/logout/saml2/slo").session(((MockHttpSession) this.request.getSession()))
+				.param("SAMLResponse", this.apLogoutResponse).param("RelayState", this.apLogoutResponseRelayState)
+				.param("SigAlg", this.apLogoutResponseSigAlg).param("Signature", this.apLogoutResponseSignature))
+				.andExpect(status().isFound()).andExpect(redirectedUrl("/login?logout"));
+		verifyNoInteractions(getBean(LogoutHandler.class));
+		assertThat(this.logoutRequestRepository.loadLogoutRequest(this.request)).isNull();
+	}
+
+	@Test
+	public void saml2LogoutResponseWhenInvalidSamlResponseThen401() throws Exception {
+		this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
+		RelyingPartyRegistration registration = this.repository.findByRegistrationId("registration-id");
+		Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
+				.samlRequest(this.rpLogoutRequest).id(this.rpLogoutRequestId).relayState(this.rpLogoutRequestRelayState)
+				.parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature)).build();
+		this.logoutRequestRepository.saveLogoutRequest(logoutRequest, this.request, this.response);
+		String deflatedApLogoutResponse = Saml2Utils.samlEncode(
+				Saml2Utils.samlInflate(Saml2Utils.samlDecode(this.apLogoutResponse)).getBytes(StandardCharsets.UTF_8));
+		this.mvc.perform(post("/logout/saml2/slo").session((MockHttpSession) this.request.getSession())
+				.param("SAMLResponse", deflatedApLogoutResponse).param("RelayState", this.rpLogoutRequestRelayState)
+				.param("SigAlg", this.apLogoutRequestSigAlg).param("Signature", this.apLogoutResponseSignature))
+				.andExpect(status().reason(containsString("invalid_signature"))).andExpect(status().isUnauthorized());
+		verifyNoInteractions(getBean(LogoutHandler.class));
+	}
+
+	@Test
+	public void saml2LogoutResponseWhenCustomLogoutResponseHandlerThenUses() throws Exception {
+		this.spring.register(Saml2LogoutComponentsConfig.class).autowire();
+		RelyingPartyRegistration registration = this.repository.findByRegistrationId("get");
+		Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
+				.samlRequest(this.rpLogoutRequest).id(this.rpLogoutRequestId).relayState(this.rpLogoutRequestRelayState)
+				.parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature)).build();
+		given(getBean(Saml2LogoutRequestRepository.class).removeLogoutRequest(any(), any())).willReturn(logoutRequest);
+		given(getBean(Saml2LogoutResponseValidator.class).validate(any()))
+				.willReturn(Saml2LogoutValidatorResult.success());
+		this.mvc.perform(get("/logout/saml2/slo").param("SAMLResponse", "samlResponse")).andReturn();
+		verify(getBean(Saml2LogoutResponseValidator.class)).validate(any());
+	}
+
+	private <T> T getBean(Class<T> clazz) {
+		return this.spring.getContext().getBean(clazz);
+	}
+
+	@EnableWebSecurity
+	@Import(Saml2LoginConfigBeans.class)
+	static class Saml2LogoutDefaultsConfig {
+
+		LogoutHandler mockLogoutHandler = mock(LogoutHandler.class);
+
+		@Bean
+		SecurityFilterChain web(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeRequests((authorize) -> authorize.anyRequest().authenticated())
+				.logout((logout) -> logout.addLogoutHandler(this.mockLogoutHandler))
+				.saml2Login(withDefaults())
+				.saml2Logout(withDefaults());
+			return http.build();
+			// @formatter:on
+		}
+
+		@Bean
+		LogoutHandler logoutHandler() {
+			return this.mockLogoutHandler;
+		}
+
+	}
+
+	@EnableWebSecurity
+	@Import(Saml2LoginConfigBeans.class)
+	static class Saml2LogoutCsrfDisabledConfig {
+
+		LogoutSuccessHandler mockLogoutSuccessHandler = mock(LogoutSuccessHandler.class);
+
+		@Bean
+		SecurityFilterChain web(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeRequests((authorize) -> authorize.anyRequest().authenticated())
+				.logout((logout) -> logout.logoutSuccessHandler(this.mockLogoutSuccessHandler))
+				.saml2Login(withDefaults())
+				.saml2Logout(withDefaults())
+				.csrf().disable();
+			return http.build();
+			// @formatter:on
+		}
+
+		@Bean
+		LogoutSuccessHandler logoutSuccessHandler() {
+			return this.mockLogoutSuccessHandler;
+		}
+
+	}
+
+	@EnableWebSecurity
+	@Import(Saml2LoginConfigBeans.class)
+	static class Saml2LogoutComponentsConfig {
+
+		Saml2LogoutRequestRepository logoutRequestRepository = mock(Saml2LogoutRequestRepository.class);
+
+		Saml2LogoutRequestValidator logoutRequestValidator = mock(Saml2LogoutRequestValidator.class);
+
+		Saml2LogoutRequestResolver logoutRequestResolver = mock(Saml2LogoutRequestResolver.class);
+
+		Saml2LogoutResponseValidator logoutResponseValidator = mock(Saml2LogoutResponseValidator.class);
+
+		Saml2LogoutResponseResolver logoutResponseResolver = mock(Saml2LogoutResponseResolver.class);
+
+		@Bean
+		SecurityFilterChain web(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeRequests((authorize) -> authorize.anyRequest().authenticated())
+				.saml2Login(withDefaults())
+				.saml2Logout((logout) -> logout
+					.logoutRequest((request) -> request
+						.logoutRequestRepository(this.logoutRequestRepository)
+						.logoutRequestValidator(this.logoutRequestValidator)
+						.logoutRequestResolver(this.logoutRequestResolver)
+					)
+					.logoutResponse((response) -> response
+						.logoutResponseValidator(this.logoutResponseValidator)
+						.logoutResponseResolver(this.logoutResponseResolver)
+					)
+				);
+			return http.build();
+			// @formatter:on
+		}
+
+		@Bean
+		Saml2LogoutRequestRepository logoutRequestRepository() {
+			return this.logoutRequestRepository;
+		}
+
+		@Bean
+		Saml2LogoutRequestValidator logoutRequestAuthenticator() {
+			return this.logoutRequestValidator;
+		}
+
+		@Bean
+		Saml2LogoutRequestResolver logoutRequestResolver() {
+			return this.logoutRequestResolver;
+		}
+
+		@Bean
+		Saml2LogoutResponseValidator logoutResponseAuthenticator() {
+			return this.logoutResponseValidator;
+		}
+
+		@Bean
+		Saml2LogoutResponseResolver logoutResponseResolver() {
+			return this.logoutResponseResolver;
+		}
+
+	}
+
+	static class Saml2LoginConfigBeans {
+
+		@Bean
+		RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
+			Saml2X509Credential signing = TestSaml2X509Credentials.assertingPartySigningCredential();
+			Saml2X509Credential verification = TestSaml2X509Credentials.relyingPartyVerifyingCredential();
+			RelyingPartyRegistration.Builder withCreds = TestRelyingPartyRegistrations.noCredentials()
+					.signingX509Credentials(credential(signing))
+					.assertingPartyDetails((party) -> party.verificationX509Credentials(credential(verification)));
+			RelyingPartyRegistration post = withCreds.build();
+			RelyingPartyRegistration get = withCreds.registrationId("get")
+					.singleLogoutServiceBinding(Saml2MessageBinding.REDIRECT).build();
+			RelyingPartyRegistration ap = withCreds.registrationId("ap").entityId("ap-entity-id")
+					.assertingPartyDetails((party) -> party
+							.singleLogoutServiceLocation("https://rp.example.org/logout/saml2/request")
+							.singleLogoutServiceResponseLocation("https://rp.example.org/logout/saml2/response"))
+					.build();
+
+			return new InMemoryRelyingPartyRegistrationRepository(ap, get, post);
+		}
+
+		private Consumer<Collection<Saml2X509Credential>> credential(Saml2X509Credential credential) {
+			return (credentials) -> credentials.add(credential);
+		}
+
+	}
+
+}

+ 142 - 146
docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc

@@ -1,4 +1,5 @@
 
+
 [[servlet-saml2login]]
 == SAML 2.0 Login
 :figures: images/servlet/saml2
@@ -791,7 +792,7 @@ spring:
         okta:
           signing.credentials: &relying-party-credentials
             - private-key-location: classpath:rp.key
-            - certificate-location: classpath:rp.crt
+              certificate-location: classpath:rp.crt
           identityprovider:
             entity-id: ...
         azure:
@@ -1639,9 +1640,7 @@ To use Spring Security's SAML 2.0 Single Logout feature, you will need the follo
 * Second, the asserting party should be configured to sign and POST `saml2:LogoutRequest` s and `saml2:LogoutResponse` s your application's `/logout/saml2/slo` endpoint
 * Third, your application must have a PKCS#8 private key and X.509 certificate for signing `saml2:LogoutRequest` s and `saml2:LogoutResponse` s
 
-==== RP-Initiated Single Logout
-
-Given those, then for RP-initiated Single Logout, you can begin from the initial minimal example and add the following configuration:
+You can begin from the initial minimal example and add the following configuration:
 
 [source,java]
 ----
@@ -1650,48 +1649,31 @@ Given those, then for RP-initiated Single Logout, you can begin from the initial
 
 @Bean
 RelyingPartyRegistrationRepository registrations() {
-    RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
+    Saml2X509Credential credential = Saml2X509Credential.signing(key, certificate);
+    RelyingPartyRegistration registration = RelyingPartyRegistrations
             .fromMetadataLocation("https://ap.example.org/metadata")
             .registrationId("id")
-            .singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo")
-            .signingX509Credentials((signing) -> signing.add(Saml2X509Credential.signing(key, certificate))) <1>
+            .signingX509Credentials((signing) -> signing.add(credential)) <1>
             .build();
-    return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
+    return new InMemoryRelyingPartyRegistrationRepository(registration);
 }
 
 @Bean
 SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception {
-	RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations);
-    LogoutHandler logoutResponseHandler = logoutResponseHandler(registrationResolver);
-    LogoutSuccessHandler logoutRequestSuccessHandler = logoutRequestSuccessHandler(registrationResolver);
-
     http
         .authorizeRequests((authorize) -> authorize
             .anyRequest().authenticated()
         )
         .saml2Login(withDefaults())
-        .logout((logout) -> logout
-                .logoutUrl("/saml2/logout")
-                .logoutSuccessHandler(successHandler))
-        .addFilterBefore(new Saml2LogoutResponseFilter(logoutHandler), CsrfFilter.class);
+        .saml2Logout(withDefaults()); <2>
 
     return http.build();
 }
-
-private LogoutSuccessHandler logoutRequestSuccessHandler(RelyingPartyRegistrationResolver registrationResolver) { <2>
-    OpenSaml4LogoutRequestResolver logoutRequestResolver = new OpenSaml4LogoutRequestResolver(registrationResolver);
-    return new Saml2LogoutRequestSuccessHandler(logoutRequestResolver);
-}
-
-private LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { <3>
-    return new OpenSamlLogoutResponseHandler(relyingPartyRegistrationResolver);
-}
 ----
 <1> - First, add your signing key to the `RelyingPartyRegistration` instance or to <<servlet-saml2login-rpr-duplicated,multiple instances>>
-<2> - Second, supply a `LogoutSuccessHandler` for initiating Single Logout, sending a `saml2:LogoutRequest` to the asserting party
-<3> - Third, supply the `LogoutHandler` s needed to handle the `saml2:LogoutResponse` s sent from the asserting party.
+<2> - Second, indicate that your application wants to use SAML SLO to logout the end user
 
-==== Runtime Expectations for RP-Initiated
+==== Runtime Expectations
 
 Given the above configuration any logged in user can send a `POST /logout` to your application to perform RP-initiated SLO.
 Your application will then do the following:
@@ -1702,86 +1684,30 @@ Your application will then do the following:
 4. Deserialize, verify, and process the `<saml2:LogoutResponse>` sent by the asserting party
 5. Redirect to any configured successful logout endpoint
 
-[TIP]
-If your asserting party does not send `<saml2:LogoutResponse>` s when logout is complete, the asserting party can still send a `POST /saml2/logout` and then there is no need to configure the `Saml2LogoutResponseHandler`.
-
-==== AP-Initiated Single Logout
-
-Instead of RP-initiated Single Logout, you can again begin from the initial minimal example and add the following configuration to achieve AP-initiated Single Logout:
-
-[source,java]
-----
-@Value("${private.key}") RSAPrivateKey key;
-@Value("${public.certificate}") X509Certificate certificate;
-
-@Bean
-RelyingPartyRegistrationRepository registrations() {
-    RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
-            .fromMetadataLocation("https://ap.example.org/metadata")
-            .registrationId("id")
-            .signingX509Credentials((signing) -> signing.add(Saml2X509Credential.signing(key, certificate))) <1>
-            .build();
-    return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
-}
-
-@Bean
-SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception {
-	RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations);
-    LogoutHandler logoutRequestHandler = logoutRequestHandler(registrationResolver);
-    LogoutSuccessHandler logoutResponseSuccessHandler = logoutResponseSuccessHandler(registrationResolver);
-
-    http
-        .authorizeRequests((authorize) -> authorize
-            .anyRequest().authenticated()
-        )
-        .saml2Login(withDefaults())
-        .addFilterBefore(new Saml2LogoutRequestFilter(logoutResponseSuccessHandler, logoutRequestHandler), CsrfFilter.class);
-
-    return http.build();
-}
-
-private LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { <2>
-    return new CompositeLogoutHandler(
-    		new OpenSamlLogoutRequestHandler(relyingPartyRegistrationResolver),
-            new SecurityContextLogoutHandler(),
-            new LogoutSuccessEventPublishingLogoutHandler());
-}
-
-private LogoutSuccessHandler logoutSuccessHandler(RelyingPartyRegistrationResolver registrationResolver) { <3>
-    OpenSaml4LogoutResponseResolver logoutResponseResolver = new OpenSaml4LogoutResponseResolver(registrationResolver);
-    return new Saml2LogoutResponseSuccessHandler(logoutResponseResolver);
-}
-----
-<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to <<servlet-saml2login-rpr-duplicated,multiple instances>>
-<2> - Second, supply the `LogoutHandler` needed to handle the `saml2:LogoutRequest` s sent from the asserting party.
-<3> - Third, supply a `LogoutSuccessHandler` for completing Single Logout, sending a `saml2:LogoutResponse` to the asserting party
-
-==== Runtime Expectations for AP-Initiated
-
-Given the above configuration, an asserting party can send a `POST /logout/saml2` to your application that includes a `<saml2:LogoutRequest>`
-Also, your application can participate in an AP-initated logout when the asserting party sends a `<saml2:LogoutRequest>` to `/logout/saml2/slo`:
+Also, your application can participate in an AP-initiated logout when the asserting party sends a `<saml2:LogoutRequest>` to `/logout/saml2/slo`:
 
 1. Use a `Saml2LogoutRequestHandler` to deserialize, verify, and process the `<saml2:LogoutRequest>` sent by the asserting party
 2. Logout the user and invalidate the session
 3. Create, sign, and serialize a `<saml2:LogoutResponse>` based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>> associated with the just logged-out user
 4. Send a redirect or post to the asserting party based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>
 
-[TIP]
-If your asserting party does not expect you do send a `<saml2:LogoutResponse>` s when logout is complete, you may not need to configure a `LogoutSuccessHandler`
+=== Configuring Logout Endpoints
 
-[NOTE]
-In the event that you need to support both logout flows, you can combine the above to configurations.
+There are three behaviors that can be triggered by different endpoints:
+* RP-initiated logout, which allows an authenticated user to `POST` and trigger the logout process by sending the asserting party a `<saml2:LogoutRequest>`
+* AP-initiated logout, which allows an asserting party to send a `<saml2:LogoutRequest>` to the application
+* AP logout response, which allows an asserting party to send a `<saml2:LogoutResponse>` in response to the RP-initiated `<saml2:LogoutRequest>`
 
-=== Configuring Logout Endpoints
+The first is triggered by performing normal `POST /logout` when the principal is of type `Saml2AuthenticatedPrincipal`.
 
-There are three default endpoints that Spring Security's SAML 2.0 Single Logout support exposes:
-* `/logout` - the endpoint for initiating single logout with an asserting party
-* `/logout/saml2/slo` - the endpoint for receiving logout requests or responses from an asserting party
+The second is triggered by POSTing to the `/logout/saml2/slo` endpoint with a `SAMLRequest` signed by the asserting party.
 
-Because the user is already logged in, the `registrationId` is already known.
+The third is triggered by POSTing to the `/logout/saml2/slo` endpoint with a `SAMLResponse` signed by the asserting party.
+
+Because the user is already logged in or the original Logout Request is known, the `registrationId` is already known.
 For this reason, `+{registrationId}+` is not part of these URLs by default.
 
-These URLs are customizable in the DSL.
+This URL is customizable in the DSL.
 
 For example, if you are migrating your existing relying party over to Spring Security, your asserting party may already be pointing to `GET /SLOService.saml2`.
 To reduce changes in configuration for the asserting party, you can configure the filter in the DSL like so:
@@ -1790,12 +1716,15 @@ To reduce changes in configuration for the asserting party, you can configure th
 .Java
 [source,java,role="primary"]
 ----
-Saml2LogoutResponseFilter filter = new Saml2LogoutResponseFilter(logoutHandler);
-filter.setLogoutRequestMatcher(new AntPathRequestMatcher("/SLOService.saml2", "GET"));
 http
-    // ...
-    .addFilterBefore(filter, CsrfFilter.class);
+    .saml2Logout((saml2) -> saml2
+        .logoutRequest((request) -> request.logoutUrl("/SLOService.saml2"))
+        .logoutResponse((response) -> response.logoutUrl("/SLOService.saml2"))
+    );
 ----
+====
+
+You should also configure these endpoints in your `RelyingPartyRegistration`.
 
 === Customizing `<saml2:LogoutRequest>` Resolution
 
@@ -1812,22 +1741,33 @@ To add other values, you can use delegation, like so:
 
 [source,java]
 ----
-OpenSamlLogoutRequestResolver delegate = new OpenSamlLogoutRequestResolver(registrationResolver);
-return (request, response, authentication) -> {
-	OpenSamlLogoutRequestBuilder builder = delegate.resolveLogoutRequest(request, response, authentication); <1>
-	builder.name(((Saml2AuthenticatedPrincipal) authentication.getPrincipal()).getFirstAttribute("CustomAttribute")); <2>
-	builder.logoutRequest((logoutRequest) -> logoutRequest.setIssueInstant(DateTime.now()));
-	return builder.logoutRequest(); <3>
-};
+@Bean
+Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationResolver registrationResolver) {
+	OpenSaml4LogoutRequestResolver logoutRequestResolver
+			new OpenSaml4LogoutRequestResolver(registrationResolver);
+	logoutRequestResolver.setParametersConsumer((parameters) -> {
+		String name = ((Saml2AuthenticatedPrincipal) parameters.getAuthentication().getPrincipal()).getFirstAttribute("CustomAttribute");
+		String format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient";
+		LogoutRequest logoutRequest = parameters.getLogoutRequest();
+		NameID nameId = logoutRequest.getNameID();
+		nameId.setValue(name);
+		nameId.setFormat(format);
+	});
+	return logoutRequestResolver;
+}
 ----
-<1> - Spring Security applies default values to a `<saml2:LogoutRequest>`
-<2> - Your application specifies customizations
-<3> - You complete the invocation by calling `request()`
 
-[NOTE]
-Support for OpenSAML 4 is coming.
-In anticipation of that, `OpenSamlLogoutRequestResolver` does not add an `IssueInstant`.
-Once OpenSAML 4 support is added, the default will be able to appropriate negotiate that datatype change, meaning you will no longer have to set it.
+Then, you can supply your custom `Saml2LogoutRequestResolver` in the DSL as follows:
+
+[source,java]
+----
+http
+    .saml2Logout((saml2) -> saml2
+        .logoutRequest((request) -> request
+            .logoutRequestResolver(this.logoutRequestResolver)
+        )
+    );
+----
 
 === Customizing `<saml2:LogoutResponse>` Resolution
 
@@ -1844,55 +1784,111 @@ To add other values, you can use delegation, like so:
 
 [source,java]
 ----
-OpenSamlLogoutResponseResolver delegate = new OpenSamlLogoutResponseResolver(registrationResolver);
-return (request, response, authentication) -> {
-	OpenSamlLogoutResponseBuilder builder = delegate.resolveLogoutResponse(request, response, authentication); <1>
-    if (checkOtherPrevailingConditions()) {
-        builder.status(StatusCode.PARTIAL_LOGOUT); <2>
-    }
-	builder.logoutResponse((logoutResponse) -> logoutResponse.setIssueInstant(DateTime.now()));
-	return builder.logoutResponse(); <3>
-};
+@Bean
+public Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationResolver registrationResolver) {
+	OpenSaml4LogoutResponseResolver logoutRequestResolver =
+			new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver);
+	logoutRequestResolver.setParametersConsumer((parameters) -> {
+		if (checkOtherPrevailingConditions(parameters.getRequest())) {
+			parameters.getLogoutRequest().getStatus().getStatusCode().setCode(StatusCode.PARTIAL_LOGOUT);
+		}
+	});
+	return logoutRequestResolver;
+}
 ----
-<1> - Spring Security applies default values to a `<saml2:LogoutResponse>`
-<2> - Your application specifies customizations
-<3> - You complete the invocation by calling `response()`
 
-[NOTE]
-Support for OpenSAML 4 is coming.
-In anticipation of that, `OpenSamlLogoutResponseResolver` does not add an `IssueInstant`.
-Once OpenSAML 4 support is added, the default will be able to appropriate negotiate that datatype change, meaning you will no longer have to set it.
+Then, you can supply your custom `Saml2LogoutResponseResolver` in the DSL as follows:
 
-=== Customizing `<saml2:LogoutRequest>` Validation
+[source,java]
+----
+http
+    .saml2Logout((saml2) -> saml2
+        .logoutRequest((request) -> request
+            .logoutRequestResolver(this.logoutRequestResolver)
+        )
+    );
+----
+
+=== Customizing `<saml2:LogoutRequest>` Authentication
 
-To customize validation, you can implement your own `LogoutHandler`.
-At this point, the validation is minimal, so you may be able to first delegate to the default `LogoutHandler` like so:
+To customize validation, you can implement your own `Saml2LogoutRequestValidator`.
+At this point, the validation is minimal, so you may be able to first delegate to the default `Saml2LogoutRequestValidator` like so:
 
 [source,java]
 ----
-LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) {
-	OpenSamlLogoutRequestHandler delegate = new OpenSamlLogoutRequestHandler(registrationResolver);
-	return (request, response, authentication) -> {
-		delegate.logout(request, response, authentication); // verify signature, issuer, destination, and principal name
+@Component
+public class MyOpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator {
+	private final Saml2LogoutRequestValidator delegate = new OpenSamlLogoutRequestValidator();
+
+	@Override
+    public Saml2LogoutRequestValidator logout(Saml2LogoutRequestValidatorParameters parameters) {
+		 // verify signature, issuer, destination, and principal name
+		Saml2LogoutValidatorResult result = delegate.authenticate(authentication);
+
 		LogoutRequest logoutRequest = // ... parse using OpenSAML
         // perform custom validation
-	}
+    }
 }
 ----
 
-=== Customizing `<saml2:LogoutResponse>` Validation
+Then, you can supply your custom `Saml2LogoutRequestValidator` in the DSL as follows:
 
-To customize validation, you can implement your own `LogoutHandler`.
-At this point, the validation is minimal, so you may be able to first delegate to the default `LogoutHandler` like so:
+[source,java]
+----
+http
+    .saml2Logout((saml2) -> saml2
+        .logoutRequest((request) -> request
+            .logoutRequestAuthenticator(myOpenSamlLogoutRequestAuthenticator)
+        )
+    );
+----
+
+=== Customizing `<saml2:LogoutResponse>` Authentication
+
+To customize validation, you can implement your own `Saml2LogoutResponseValidator`.
+At this point, the validation is minimal, so you may be able to first delegate to the default `Saml2LogoutResponseValidator` like so:
 
 [source,java]
 ----
-LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) {
-	OpenSamlLogoutResponseHandler delegate = new OpenSamlLogoutResponseHandler(registrationResolver);
-	return (request, response, authentication) -> {
-		delegate.logout(request, response, authentication); // verify signature, issuer, destination, and status
+@Component
+public class MyOpenSamlLogoutResponseValidator implements Saml2LogoutResponseValidator {
+	private final Saml2LogoutResponseValidator delegate = new OpenSamlLogoutResponseValidator();
+
+	@Override
+    public Saml2LogoutValidatorResult logout(Saml2LogoutResponseValidatorParameters parameters) {
+		// verify signature, issuer, destination, and status
+		Saml2LogoutValidatorResult result = delegate.authenticate(parameters);
+
 		LogoutResponse logoutResponse = // ... parse using OpenSAML
         // perform custom validation
-	}
+    }
 }
 ----
+
+Then, you can supply your custom `Saml2LogoutResponseValidator` in the DSL as follows:
+
+[source,java]
+----
+http
+    .saml2Logout((saml2) -> saml2
+        .logoutResponse((response) -> response
+            .logoutResponseAuthenticator(myOpenSamlLogoutResponseAuthenticator)
+        )
+    );
+----
+
+=== Customizing `<saml2:LogoutRequest>` storage
+
+When your application sends a `<saml2:LogoutRequest>`, the value is stored in the session so that the `RelayState` parameter and the `InResponseTo` attribute in the `<saml2:LogoutResponse>` can be verified.
+
+If you want to store logout requests in some place other than the session, you can supply your custom implementation in the DSL, like so:
+
+[source,java]
+----
+http
+    .saml2Logout((saml2) -> saml2
+        .logoutRequest((request) -> request
+            .logoutRequestRepository(myCustomLogoutRequestRepository)
+        )
+    );
+----