Sfoglia il codice sorgente

Add SAML 2.0 Single Logout XML Support

Closes gh-10842
Marcus Da Coregio 3 anni fa
parent
commit
93d4fd3559
18 ha cambiato i file con 1336 aggiunte e 6 eliminazioni
  1. 2 0
      config/src/main/java/org/springframework/security/config/Elements.java
  2. 41 0
      config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java
  3. 7 0
      config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java
  4. 236 0
      config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java
  5. 104 0
      config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserUtils.java
  6. 6 0
      config/src/main/java/org/springframework/security/config/http/SecurityFilters.java
  7. 43 2
      config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java
  8. 51 2
      config/src/main/resources/org/springframework/security/config/spring-security-5.7.rnc
  9. 117 0
      config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd
  10. 1 1
      config/src/main/resources/org/springframework/security/config/spring-security.xsl
  11. 1 1
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java
  12. 327 0
      config/src/test/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests.java
  13. 41 0
      config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CsrfDisabled-MockLogoutSuccessHandler.xml
  14. 56 0
      config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CustomComponents.xml
  15. 35 0
      config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-Default.xml
  16. 40 0
      config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-LogoutSuccessHandler.xml
  17. 83 0
      config/src/test/resources/org/springframework/security/config/saml2/logout-registrations.xml
  18. 145 0
      docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc

+ 2 - 0
config/src/main/java/org/springframework/security/config/Elements.java

@@ -142,4 +142,6 @@ public abstract class Elements {
 
 	public static final String SAML2_LOGIN = "saml2-login";
 
+	public static final String SAML2_LOGOUT = "saml2-logout";
+
 }

+ 41 - 0
config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java

@@ -164,6 +164,8 @@ final class AuthenticationConfigBuilder {
 	@SuppressWarnings("rawtypes")
 	private ManagedList logoutHandlers;
 
+	private BeanMetadataElement logoutSuccessHandler;
+
 	private BeanDefinition loginPageGenerationFilter;
 
 	private BeanDefinition logoutPageGenerationFilter;
@@ -210,6 +212,12 @@ final class AuthenticationConfigBuilder {
 
 	private String saml2AuthenticationRequestFilterId;
 
+	private String saml2LogoutFilterId;
+
+	private String saml2LogoutRequestFilterId;
+
+	private String saml2LogoutResponseFilterId;
+
 	private boolean oauth2ClientEnabled;
 
 	private BeanDefinition authorizationRequestRedirectFilter;
@@ -250,6 +258,7 @@ final class AuthenticationConfigBuilder {
 		createX509Filter(authenticationManager);
 		createJeeFilter(authenticationManager);
 		createLogoutFilter();
+		createSaml2LogoutFilter();
 		createLoginPageFilterIfNeeded();
 		createUserDetailsServiceFactory();
 		createExceptionTranslationFilter();
@@ -720,9 +729,33 @@ final class AuthenticationConfigBuilder {
 					this.rememberMeServicesId, this.csrfLogoutHandler);
 			this.logoutFilter = logoutParser.parse(logoutElt, this.pc);
 			this.logoutHandlers = logoutParser.getLogoutHandlers();
+			this.logoutSuccessHandler = logoutParser.getLogoutSuccessHandler();
 		}
 	}
 
+	private void createSaml2LogoutFilter() {
+		Element saml2LogoutElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.SAML2_LOGOUT);
+		if (saml2LogoutElt == null) {
+			return;
+		}
+		Saml2LogoutBeanDefinitionParser parser = new Saml2LogoutBeanDefinitionParser(this.logoutHandlers,
+				this.logoutSuccessHandler);
+		parser.parse(saml2LogoutElt, this.pc);
+		BeanDefinition saml2LogoutFilter = parser.getLogoutFilter();
+		BeanDefinition saml2LogoutRequestFilter = parser.getLogoutRequestFilter();
+		BeanDefinition saml2LogoutResponseFilter = parser.getLogoutResponseFilter();
+		this.saml2LogoutFilterId = this.pc.getReaderContext().generateBeanName(saml2LogoutFilter);
+		this.saml2LogoutRequestFilterId = this.pc.getReaderContext().generateBeanName(saml2LogoutRequestFilter);
+		this.saml2LogoutResponseFilterId = this.pc.getReaderContext().generateBeanName(saml2LogoutResponseFilter);
+
+		// register the component
+		this.pc.registerBeanComponent(new BeanComponentDefinition(saml2LogoutFilter, this.saml2LogoutFilterId));
+		this.pc.registerBeanComponent(
+				new BeanComponentDefinition(saml2LogoutRequestFilter, this.saml2LogoutRequestFilterId));
+		this.pc.registerBeanComponent(
+				new BeanComponentDefinition(saml2LogoutResponseFilter, this.saml2LogoutResponseFilterId));
+	}
+
 	@SuppressWarnings({ "rawtypes", "unchecked" })
 	ManagedList getLogoutHandlers() {
 		if (this.logoutHandlers == null && this.rememberMeProviderRef != null) {
@@ -968,6 +1001,14 @@ final class AuthenticationConfigBuilder {
 			filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2AuthenticationRequestFilterId),
 					SecurityFilters.SAML2_AUTHENTICATION_REQUEST_FILTER));
 		}
+		if (this.saml2LogoutFilterId != null) {
+			filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2LogoutFilterId),
+					SecurityFilters.SAML2_LOGOUT_FILTER));
+			filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2LogoutRequestFilterId),
+					SecurityFilters.SAML2_LOGOUT_REQUEST_FILTER));
+			filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2LogoutResponseFilterId),
+					SecurityFilters.SAML2_LOGOUT_RESPONSE_FILTER));
+		}
 		filters.add(new OrderDecorator(this.etf, SecurityFilters.EXCEPTION_TRANSLATION_FILTER));
 		return filters;
 	}

+ 7 - 0
config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java

@@ -59,6 +59,8 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser {
 
 	private boolean csrfEnabled;
 
+	private BeanMetadataElement logoutSuccessHandler;
+
 	LogoutBeanDefinitionParser(String loginPageUrl, String rememberMeServices, BeanMetadataElement csrfLogoutHandler) {
 		this.defaultLogoutUrl = loginPageUrl + "?logout";
 		this.rememberMeServices = rememberMeServices;
@@ -98,6 +100,7 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser {
 						pc.extractSource(element));
 			}
 			builder.addConstructorArgReference(successHandlerRef);
+			this.logoutSuccessHandler = new RuntimeBeanReference(successHandlerRef);
 		}
 		else {
 			// Use the logout URL if no handler set
@@ -137,4 +140,8 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser {
 		return this.logoutHandlers;
 	}
 
+	BeanMetadataElement getLogoutSuccessHandler() {
+		return this.logoutSuccessHandler;
+	}
+
 }

+ 236 - 0
config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java

@@ -0,0 +1,236 @@
+/*
+ * Copyright 2002-2022 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.http;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Predicate;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.w3c.dom.Element;
+
+import org.springframework.beans.BeanMetadataElement;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.ManagedList;
+import org.springframework.beans.factory.xml.BeanDefinitionParser;
+import org.springframework.beans.factory.xml.ParserContext;
+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.web.DefaultRelyingPartyRegistrationResolver;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseFilter;
+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.LogoutSuccessEventPublishingLogoutHandler;
+import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
+import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
+import org.springframework.security.web.util.matcher.AndRequestMatcher;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * SAML 2.0 Single Logout {@link BeanDefinitionParser}
+ *
+ * @author Marcus da Coregio
+ * @since 5.7
+ */
+final class Saml2LogoutBeanDefinitionParser implements BeanDefinitionParser {
+
+	private static final String ATT_LOGOUT_REQUEST_URL = "logout-request-url";
+
+	private static final String ATT_LOGOUT_RESPONSE_URL = "logout-response-url";
+
+	private static final String ATT_LOGOUT_URL = "logout-url";
+
+	private List<BeanMetadataElement> logoutHandlers;
+
+	private String logoutUrl = "/logout";
+
+	private String logoutRequestUrl = "/logout/saml2/slo";
+
+	private String logoutResponseUrl = "/logout/saml2/slo";
+
+	private BeanMetadataElement logoutSuccessHandler;
+
+	private BeanDefinition logoutRequestFilter;
+
+	private BeanDefinition logoutResponseFilter;
+
+	private BeanDefinition logoutFilter;
+
+	Saml2LogoutBeanDefinitionParser(ManagedList<BeanMetadataElement> logoutHandlers,
+			BeanMetadataElement logoutSuccessHandler) {
+		this.logoutHandlers = logoutHandlers;
+		this.logoutSuccessHandler = logoutSuccessHandler;
+	}
+
+	@Override
+	public BeanDefinition parse(Element element, ParserContext pc) {
+		String logoutUrl = element.getAttribute(ATT_LOGOUT_URL);
+		if (StringUtils.hasText(logoutUrl)) {
+			this.logoutUrl = logoutUrl;
+		}
+		String logoutRequestUrl = element.getAttribute(ATT_LOGOUT_REQUEST_URL);
+		if (StringUtils.hasText(logoutRequestUrl)) {
+			this.logoutRequestUrl = logoutRequestUrl;
+		}
+		String logoutResponseUrl = element.getAttribute(ATT_LOGOUT_RESPONSE_URL);
+		if (StringUtils.hasText(logoutResponseUrl)) {
+			this.logoutResponseUrl = logoutResponseUrl;
+		}
+		WebConfigUtils.validateHttpRedirect(this.logoutUrl, pc, element);
+		WebConfigUtils.validateHttpRedirect(this.logoutRequestUrl, pc, element);
+		WebConfigUtils.validateHttpRedirect(this.logoutResponseUrl, pc, element);
+		if (CollectionUtils.isEmpty(this.logoutHandlers)) {
+			this.logoutHandlers = createDefaultLogoutHandlers();
+		}
+		if (this.logoutSuccessHandler == null) {
+			this.logoutSuccessHandler = createDefaultLogoutSuccessHandler();
+		}
+		BeanMetadataElement relyingPartyRegistrationRepository = Saml2LogoutBeanDefinitionParserUtils
+				.getRelyingPartyRegistrationRepository(element);
+		BeanMetadataElement registrations = BeanDefinitionBuilder
+				.rootBeanDefinition(DefaultRelyingPartyRegistrationResolver.class)
+				.addConstructorArgValue(relyingPartyRegistrationRepository).getBeanDefinition();
+		BeanMetadataElement logoutResponseResolver = Saml2LogoutBeanDefinitionParserUtils
+				.getLogoutResponseResolver(element, registrations);
+		BeanMetadataElement logoutRequestValidator = Saml2LogoutBeanDefinitionParserUtils
+				.getLogoutRequestValidator(element);
+		BeanMetadataElement logoutRequestMatcher = createSaml2LogoutRequestMatcher();
+		this.logoutRequestFilter = BeanDefinitionBuilder.rootBeanDefinition(Saml2LogoutRequestFilter.class)
+				.addConstructorArgValue(registrations).addConstructorArgValue(logoutRequestValidator)
+				.addConstructorArgValue(logoutResponseResolver).addConstructorArgValue(this.logoutHandlers)
+				.addPropertyValue("logoutRequestMatcher", logoutRequestMatcher).getBeanDefinition();
+		BeanMetadataElement logoutResponseValidator = Saml2LogoutBeanDefinitionParserUtils
+				.getLogoutResponseValidator(element);
+		BeanMetadataElement logoutRequestRepository = Saml2LogoutBeanDefinitionParserUtils
+				.getLogoutRequestRepository(element);
+		BeanMetadataElement logoutResponseMatcher = createSaml2LogoutResponseMatcher();
+		this.logoutResponseFilter = BeanDefinitionBuilder.rootBeanDefinition(Saml2LogoutResponseFilter.class)
+				.addConstructorArgValue(registrations).addConstructorArgValue(logoutResponseValidator)
+				.addConstructorArgValue(this.logoutSuccessHandler)
+				.addPropertyValue("logoutRequestMatcher", logoutResponseMatcher)
+				.addPropertyValue("logoutRequestRepository", logoutRequestRepository).getBeanDefinition();
+		BeanMetadataElement logoutRequestResolver = Saml2LogoutBeanDefinitionParserUtils
+				.getLogoutRequestResolver(element, registrations);
+		BeanMetadataElement saml2LogoutRequestSuccessHandler = BeanDefinitionBuilder
+				.rootBeanDefinition(Saml2RelyingPartyInitiatedLogoutSuccessHandler.class)
+				.addConstructorArgValue(logoutRequestResolver).getBeanDefinition();
+		this.logoutFilter = BeanDefinitionBuilder.rootBeanDefinition(LogoutFilter.class)
+				.addConstructorArgValue(saml2LogoutRequestSuccessHandler).addConstructorArgValue(this.logoutHandlers)
+				.addPropertyValue("logoutRequestMatcher", createLogoutRequestMatcher()).getBeanDefinition();
+		return null;
+	}
+
+	private static List<BeanMetadataElement> createDefaultLogoutHandlers() {
+		List<BeanMetadataElement> handlers = new ManagedList<>();
+		handlers.add(BeanDefinitionBuilder.rootBeanDefinition(SecurityContextLogoutHandler.class).getBeanDefinition());
+		handlers.add(BeanDefinitionBuilder.rootBeanDefinition(LogoutSuccessEventPublishingLogoutHandler.class)
+				.getBeanDefinition());
+		return handlers;
+	}
+
+	private static BeanMetadataElement createDefaultLogoutSuccessHandler() {
+		return BeanDefinitionBuilder.rootBeanDefinition(SimpleUrlLogoutSuccessHandler.class)
+				.addPropertyValue("defaultTargetUrl", "/login?logout").getBeanDefinition();
+	}
+
+	private BeanMetadataElement createLogoutRequestMatcher() {
+		BeanMetadataElement logoutMatcher = BeanDefinitionBuilder.rootBeanDefinition(AntPathRequestMatcher.class)
+				.addConstructorArgValue(this.logoutUrl).addConstructorArgValue("POST").getBeanDefinition();
+		BeanMetadataElement saml2Matcher = BeanDefinitionBuilder.rootBeanDefinition(Saml2RequestMatcher.class)
+				.getBeanDefinition();
+		return BeanDefinitionBuilder.rootBeanDefinition(AndRequestMatcher.class)
+				.addConstructorArgValue(toManagedList(logoutMatcher, saml2Matcher)).getBeanDefinition();
+	}
+
+	private BeanMetadataElement createSaml2LogoutRequestMatcher() {
+		BeanMetadataElement logoutRequestMatcher = BeanDefinitionBuilder.rootBeanDefinition(AntPathRequestMatcher.class)
+				.addConstructorArgValue(this.logoutRequestUrl).getBeanDefinition();
+		BeanMetadataElement saml2RequestMatcher = BeanDefinitionBuilder
+				.rootBeanDefinition(ParameterRequestMatcher.class).addConstructorArgValue("SAMLRequest")
+				.getBeanDefinition();
+		return BeanDefinitionBuilder.rootBeanDefinition(AndRequestMatcher.class)
+				.addConstructorArgValue(toManagedList(logoutRequestMatcher, saml2RequestMatcher)).getBeanDefinition();
+	}
+
+	private BeanMetadataElement createSaml2LogoutResponseMatcher() {
+		BeanMetadataElement logoutResponseMatcher = BeanDefinitionBuilder
+				.rootBeanDefinition(AntPathRequestMatcher.class).addConstructorArgValue(this.logoutResponseUrl)
+				.getBeanDefinition();
+		BeanMetadataElement saml2ResponseMatcher = BeanDefinitionBuilder
+				.rootBeanDefinition(ParameterRequestMatcher.class).addConstructorArgValue("SAMLResponse")
+				.getBeanDefinition();
+		return BeanDefinitionBuilder.rootBeanDefinition(AndRequestMatcher.class)
+				.addConstructorArgValue(toManagedList(logoutResponseMatcher, saml2ResponseMatcher)).getBeanDefinition();
+	}
+
+	private static List<BeanMetadataElement> toManagedList(BeanMetadataElement... elements) {
+		List<BeanMetadataElement> managedList = new ManagedList<>();
+		managedList.addAll(Arrays.asList(elements));
+		return managedList;
+	}
+
+	BeanDefinition getLogoutRequestFilter() {
+		return this.logoutRequestFilter;
+	}
+
+	BeanDefinition getLogoutResponseFilter() {
+		return this.logoutResponseFilter;
+	}
+
+	BeanDefinition getLogoutFilter() {
+		return this.logoutFilter;
+	}
+
+	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 Saml2RequestMatcher implements RequestMatcher {
+
+		@Override
+		public boolean matches(HttpServletRequest request) {
+			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+			if (authentication == null) {
+				return false;
+			}
+			return authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal;
+		}
+
+	}
+
+}

+ 104 - 0
config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserUtils.java

@@ -0,0 +1,104 @@
+/*
+ * Copyright 2002-2022 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.http;
+
+import org.w3c.dom.Element;
+
+import org.springframework.beans.BeanMetadataElement;
+import org.springframework.beans.factory.config.RuntimeBeanReference;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+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.registration.RelyingPartyRegistrationRepository;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author Marcus da Coregio
+ * @since 5.7
+ */
+final class Saml2LogoutBeanDefinitionParserUtils {
+
+	private static final String ATT_RELYING_PARTY_REGISTRATION_REPOSITORY_REF = "relying-party-registration-repository-ref";
+
+	private static final String ATT_LOGOUT_REQUEST_VALIDATOR_REF = "logout-request-validator-ref";
+
+	private static final String ATT_LOGOUT_REQUEST_REPOSITORY_REF = "logout-request-repository-ref";
+
+	private static final String ATT_LOGOUT_REQUEST_RESOLVER_REF = "logout-request-resolver-ref";
+
+	private static final String ATT_LOGOUT_RESPONSE_RESOLVER_REF = "logout-response-resolver-ref";
+
+	private static final String ATT_LOGOUT_RESPONSE_VALIDATOR_REF = "logout-response-validator-ref";
+
+	private Saml2LogoutBeanDefinitionParserUtils() {
+	}
+
+	static BeanMetadataElement getRelyingPartyRegistrationRepository(Element element) {
+		String relyingPartyRegistrationRepositoryRef = element
+				.getAttribute(ATT_RELYING_PARTY_REGISTRATION_REPOSITORY_REF);
+		if (StringUtils.hasText(relyingPartyRegistrationRepositoryRef)) {
+			return new RuntimeBeanReference(relyingPartyRegistrationRepositoryRef);
+		}
+		return new RuntimeBeanReference(RelyingPartyRegistrationRepository.class);
+	}
+
+	static BeanMetadataElement getLogoutResponseResolver(Element element, BeanMetadataElement registrations) {
+		String logoutResponseResolver = element.getAttribute(ATT_LOGOUT_RESPONSE_RESOLVER_REF);
+		if (StringUtils.hasText(logoutResponseResolver)) {
+			return new RuntimeBeanReference(logoutResponseResolver);
+		}
+		return BeanDefinitionBuilder.rootBeanDefinition(
+				"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver")
+				.addConstructorArgValue(registrations).getBeanDefinition();
+	}
+
+	static BeanMetadataElement getLogoutRequestValidator(Element element) {
+		String logoutRequestValidator = element.getAttribute(ATT_LOGOUT_REQUEST_VALIDATOR_REF);
+		if (StringUtils.hasText(logoutRequestValidator)) {
+			return new RuntimeBeanReference(logoutRequestValidator);
+		}
+		return BeanDefinitionBuilder.rootBeanDefinition(OpenSamlLogoutRequestValidator.class).getBeanDefinition();
+	}
+
+	static BeanMetadataElement getLogoutResponseValidator(Element element) {
+		String logoutResponseValidator = element.getAttribute(ATT_LOGOUT_RESPONSE_VALIDATOR_REF);
+		if (StringUtils.hasText(logoutResponseValidator)) {
+			return new RuntimeBeanReference(logoutResponseValidator);
+		}
+		return BeanDefinitionBuilder.rootBeanDefinition(OpenSamlLogoutResponseValidator.class).getBeanDefinition();
+	}
+
+	static BeanMetadataElement getLogoutRequestRepository(Element element) {
+		String logoutRequestRepository = element.getAttribute(ATT_LOGOUT_REQUEST_REPOSITORY_REF);
+		if (StringUtils.hasText(logoutRequestRepository)) {
+			return new RuntimeBeanReference(logoutRequestRepository);
+		}
+		return BeanDefinitionBuilder.rootBeanDefinition(HttpSessionLogoutRequestRepository.class).getBeanDefinition();
+	}
+
+	static BeanMetadataElement getLogoutRequestResolver(Element element, BeanMetadataElement registrations) {
+		String logoutRequestResolver = element.getAttribute(ATT_LOGOUT_REQUEST_RESOLVER_REF);
+		if (StringUtils.hasText(logoutRequestResolver)) {
+			return new RuntimeBeanReference(logoutRequestResolver);
+		}
+		return BeanDefinitionBuilder.rootBeanDefinition(
+				"org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver")
+				.addConstructorArgValue(registrations).getBeanDefinition();
+	}
+
+}

+ 6 - 0
config/src/main/java/org/springframework/security/config/http/SecurityFilters.java

@@ -41,8 +41,14 @@ enum SecurityFilters {
 
 	CORS_FILTER,
 
+	SAML2_LOGOUT_REQUEST_FILTER,
+
+	SAML2_LOGOUT_RESPONSE_FILTER,
+
 	CSRF_FILTER,
 
+	SAML2_LOGOUT_FILTER,
+
 	LOGOUT_FILTER,
 
 	OAUTH2_AUTHORIZATION_REQUEST_FILTER,

+ 43 - 2
config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java

@@ -88,6 +88,12 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
 
 	private static final String ATT_SIGNING_ALGORITHMS = "signing-algorithms";
 
+	private static final String ATT_SINGLE_LOGOUT_SERVICE_LOCATION = "single-logout-service-location";
+
+	private static final String ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION = "single-logout-service-response-location";
+
+	private static final String ATT_SINGLE_LOGOUT_SERVICE_BINDING = "single-logout-service-binding";
+
 	private static final ResourceLoader resourceLoader = new DefaultResourceLoader();
 
 	@Override
@@ -120,12 +126,19 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
 			String singleSignOnServiceLocation = assertingPartyElt.getAttribute(ATT_SINGLE_SIGN_ON_SERVICE_LOCATION);
 			String singleSignOnServiceBinding = assertingPartyElt.getAttribute(ATT_SINGLE_SIGN_ON_SERVICE_BINDING);
 			String signingAlgorithms = assertingPartyElt.getAttribute(ATT_SIGNING_ALGORITHMS);
+			String singleLogoutServiceLocation = assertingPartyElt.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_LOCATION);
+			String singleLogoutServiceResponseLocation = assertingPartyElt
+					.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION);
+			String singleLogoutServiceBinding = assertingPartyElt.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_BINDING);
 			assertingParty.put(ATT_ASSERTING_PARTY_ID, assertingPartyId);
 			assertingParty.put(ATT_ENTITY_ID, entityId);
 			assertingParty.put(ATT_WANT_AUTHN_REQUESTS_SIGNED, wantAuthnRequestsSigned);
 			assertingParty.put(ATT_SINGLE_SIGN_ON_SERVICE_LOCATION, singleSignOnServiceLocation);
 			assertingParty.put(ATT_SINGLE_SIGN_ON_SERVICE_BINDING, singleSignOnServiceBinding);
 			assertingParty.put(ATT_SIGNING_ALGORITHMS, signingAlgorithms);
+			assertingParty.put(ATT_SINGLE_LOGOUT_SERVICE_LOCATION, singleLogoutServiceLocation);
+			assertingParty.put(ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION, singleLogoutServiceResponseLocation);
+			assertingParty.put(ATT_SINGLE_LOGOUT_SERVICE_BINDING, singleLogoutServiceBinding);
 			addVerificationCredentials(assertingPartyElt, assertingParty);
 			addEncryptionCredentials(assertingPartyElt, assertingParty);
 			providers.put(assertingPartyId, assertingParty);
@@ -195,8 +208,16 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
 			ParserContext parserContext) {
 		String registrationId = relyingPartyRegistrationElt.getAttribute(ATT_REGISTRATION_ID);
 		String metadataLocation = relyingPartyRegistrationElt.getAttribute(ATT_METADATA_LOCATION);
+		String singleLogoutServiceLocation = relyingPartyRegistrationElt
+				.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_LOCATION);
+		String singleLogoutServiceResponseLocation = relyingPartyRegistrationElt
+				.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION);
+		Saml2MessageBinding singleLogoutServiceBinding = getSingleLogoutServiceBinding(relyingPartyRegistrationElt);
 		if (StringUtils.hasText(metadataLocation)) {
-			return RelyingPartyRegistrations.fromMetadataLocation(metadataLocation).registrationId(registrationId);
+			return RelyingPartyRegistrations.fromMetadataLocation(metadataLocation).registrationId(registrationId)
+					.singleLogoutServiceLocation(singleLogoutServiceLocation)
+					.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation)
+					.singleLogoutServiceBinding(singleLogoutServiceBinding);
 		}
 		String entityId = relyingPartyRegistrationElt.getAttribute(ATT_ENTITY_ID);
 		String assertionConsumerServiceLocation = relyingPartyRegistrationElt
@@ -206,6 +227,9 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
 		return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(entityId)
 				.assertionConsumerServiceLocation(assertionConsumerServiceLocation)
 				.assertionConsumerServiceBinding(assertionConsumerServiceBinding)
+				.singleLogoutServiceLocation(singleLogoutServiceLocation)
+				.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation)
+				.singleLogoutServiceBinding(singleLogoutServiceBinding)
 				.assertingPartyDetails((builder) -> buildAssertingParty(relyingPartyRegistrationElt, assertingParties,
 						builder, parserContext));
 	}
@@ -225,9 +249,18 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
 		String singleSignOnServiceBinding = getAsString(assertingParty, ATT_SINGLE_SIGN_ON_SERVICE_BINDING);
 		Saml2MessageBinding saml2MessageBinding = StringUtils.hasText(singleSignOnServiceBinding)
 				? Saml2MessageBinding.valueOf(singleSignOnServiceBinding) : Saml2MessageBinding.REDIRECT;
+		String singleLogoutServiceLocation = getAsString(assertingParty, ATT_SINGLE_LOGOUT_SERVICE_LOCATION);
+		String singleLogoutServiceResponseLocation = getAsString(assertingParty,
+				ATT_SINGLE_LOGOUT_SERVICE_RESPONSE_LOCATION);
+		String singleLogoutServiceBinding = getAsString(assertingParty, ATT_SINGLE_LOGOUT_SERVICE_BINDING);
+		Saml2MessageBinding saml2LogoutMessageBinding = StringUtils.hasText(singleLogoutServiceBinding)
+				? Saml2MessageBinding.valueOf(singleLogoutServiceBinding) : Saml2MessageBinding.REDIRECT;
 		builder.entityId(entityId).wantAuthnRequestsSigned(Boolean.parseBoolean(wantAuthnRequestsSigned))
 				.singleSignOnServiceLocation(singleSignOnServiceLocation)
-				.singleSignOnServiceBinding(saml2MessageBinding);
+				.singleSignOnServiceBinding(saml2MessageBinding)
+				.singleLogoutServiceLocation(singleLogoutServiceLocation)
+				.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation)
+				.singleLogoutServiceBinding(saml2LogoutMessageBinding);
 		addSigningAlgorithms(assertingParty, builder);
 		addVerificationCredentials(assertingParty, builder);
 		addEncryptionCredentials(assertingParty, builder);
@@ -279,6 +312,14 @@ public final class RelyingPartyRegistrationsBeanDefinitionParser implements Bean
 		return Saml2MessageBinding.REDIRECT;
 	}
 
+	private static Saml2MessageBinding getSingleLogoutServiceBinding(Element relyingPartyRegistrationElt) {
+		String singleLogoutServiceBinding = relyingPartyRegistrationElt.getAttribute(ATT_SINGLE_LOGOUT_SERVICE_BINDING);
+		if (StringUtils.hasText(singleLogoutServiceBinding)) {
+			return Saml2MessageBinding.valueOf(singleLogoutServiceBinding);
+		}
+		return Saml2MessageBinding.POST;
+	}
+
 	private static Saml2X509Credential getSaml2VerificationCredential(String certificateLocation) {
 		return getSaml2Credential(certificateLocation, Saml2X509Credential.Saml2X509CredentialType.VERIFICATION);
 	}

+ 51 - 2
config/src/main/resources/org/springframework/security/config/spring-security-5.7.rnc

@@ -312,7 +312,7 @@ http-firewall =
 
 http =
 	## Container element for HTTP security configuration. Multiple elements can now be defined, each with a specific pattern to which the enclosed security configuration applies. A pattern can also be configured to bypass Spring Security's filters completely by setting the "security" attribute to "none".
-	element http {http.attlist, (intercept-url* & access-denied-handler? & form-login? & oauth2-login? & oauth2-client? & oauth2-resource-server? & openid-login? & saml2-login? & x509? & jee? & http-basic? & logout? & password-management? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache? & expression-handler? & headers? & csrf? & cors?) }
+	element http {http.attlist, (intercept-url* & access-denied-handler? & form-login? & oauth2-login? & oauth2-client? & oauth2-resource-server? & openid-login? & saml2-login? & saml2-logout? & x509? & jee? & http-basic? & logout? & password-management? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache? & expression-handler? & headers? & csrf? & cors?) }
 http.attlist &=
 	## The request URL pattern which will be mapped to the filter chain created by this <http> element. If omitted, the filter chain will match all requests.
 	attribute pattern {xsd:token}?
@@ -690,6 +690,37 @@ saml2-login.attlist &=
 	## Reference to the AuthenticationManager
 	attribute authentication-manager-ref {xsd:token}?
 
+saml2-logout =
+	## Configures SAML 2.0 Single Logout support
+	element saml2-logout {saml2-logout.attlist}
+saml2-logout.attlist &=
+	## The URL by which the relying or asserting party can trigger logout
+	attribute logout-url {xsd:token}?
+saml2-logout.attlist &=
+	## The URL by which the asserting party can send a SAML 2.0 Logout Request
+	attribute logout-request-url {xsd:token}?
+saml2-logout.attlist &=
+	## The URL by which the asserting party can send a SAML 2.0 Logout Response
+	attribute logout-response-url {xsd:token}?
+saml2-logout.attlist &=
+	## Reference to the RelyingPartyRegistrationRepository
+	attribute relying-party-registration-repository-ref {xsd:token}?
+saml2-logout.attlist &=
+	## Reference to the Saml2LogoutRequestValidator
+	attribute logout-request-validator-ref {xsd:token}?
+saml2-logout.attlist &=
+	## Reference to the Saml2LogoutRequestResolver
+	attribute logout-request-resolver-ref {xsd:token}?
+saml2-logout.attlist &=
+	## Reference to the Saml2LogoutRequestRepository
+	attribute logout-request-repository-ref {xsd:token}?
+saml2-logout.attlist &=
+	## Reference to the Saml2LogoutResponseValidator
+	attribute logout-response-validator-ref {xsd:token}?
+saml2-logout.attlist &=
+	## Reference to the Saml2LogoutResponseResolver
+	attribute logout-response-resolver-ref {xsd:token}?
+
 relying-party-registrations =
 	## Container element for relying party(ies) registered with a SAML 2.0 identity provider
 	element relying-party-registrations {relying-party-registration+, asserting-party*}
@@ -715,6 +746,15 @@ relying-party-registration.attlist &=
 relying-party-registration.attlist &=
 	## A reference to the associated asserting party.
 	attribute asserting-party-id {xsd:token}?
+relying-party-registration.attlist &=
+	## The relying party <a href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService Location</a>
+	attribute single-logout-service-location {xsd:token}?
+relying-party-registration.attlist &=
+	## The relying party <a href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService Response Location</a>
+	attribute single-logout-service-response-location {xsd:token}?
+relying-party-registration.attlist &=
+	## The relying party <a href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService Binding</a>
+	attribute single-logout-service-binding {xsd:token}?
 
 signing-credential =
 	## The relying party's signing credential
@@ -757,6 +797,15 @@ asserting-party.attlist &=
 asserting-party.attlist &=
 	## A comma separated list of org.opensaml.saml.ext.saml2alg.SigningMethod Algorithms for this asserting party, in preference order.
 	attribute signing-algorithms {xsd:token}?
+asserting-party.attlist &=
+	## The asserting party <a href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService Location</a>
+	attribute single-logout-service-location {xsd:token}?
+asserting-party.attlist &=
+	## The asserting party <a href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService Response Location</a>
+	attribute single-logout-service-response-location {xsd:token}?
+asserting-party.attlist &=
+	## The asserting party <a href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService Binding</a>
+	attribute single-logout-service-binding {xsd:token}?
 
 verification-credential =
 	## The relying party's verification credential
@@ -1266,4 +1315,4 @@ position =
 	## The explicit position at which the custom-filter should be placed in the chain. Use if you are replacing a standard filter.
 	attribute position {named-security-filter}
 
-named-security-filter = "FIRST" | "CHANNEL_FILTER" | "SECURITY_CONTEXT_FILTER" | "CONCURRENT_SESSION_FILTER" | "WEB_ASYNC_MANAGER_FILTER" | "HEADERS_FILTER" | "CORS_FILTER" | "CSRF_FILTER" | "LOGOUT_FILTER" | "OAUTH2_AUTHORIZATION_REQUEST_FILTER" | "SAML2_AUTHENTICATION_REQUEST_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "OAUTH2_LOGIN_FILTER" | "SAML2_AUTHENTICATION_FILTER" | "FORM_LOGIN_FILTER" | "OPENID_FILTER" | "LOGIN_PAGE_FILTER" |"LOGOUT_PAGE_FILTER" | "DIGEST_AUTH_FILTER" | "BEARER_TOKEN_AUTH_FILTER" | "BASIC_AUTH_FILTER" | "REQUEST_CACHE_FILTER" | "SERVLET_API_SUPPORT_FILTER" | "JAAS_API_SUPPORT_FILTER" | "REMEMBER_ME_FILTER" | "ANONYMOUS_FILTER" | "OAUTH2_AUTHORIZATION_CODE_GRANT_FILTER" | "WELL_KNOWN_CHANGE_PASSWORD_REDIRECT_FILTER" | "SESSION_MANAGEMENT_FILTER" | "EXCEPTION_TRANSLATION_FILTER" | "FILTER_SECURITY_INTERCEPTOR" | "SWITCH_USER_FILTER" | "LAST"
+named-security-filter = "FIRST" | "CHANNEL_FILTER" | "SECURITY_CONTEXT_FILTER" | "CONCURRENT_SESSION_FILTER" | "WEB_ASYNC_MANAGER_FILTER" | "HEADERS_FILTER" | "CORS_FILTER" | "SAML2_LOGOUT_REQUEST_FILTER" | "SAML2_LOGOUT_RESPONSE_FILTER" | "CSRF_FILTER" | "SAML2_LOGOUT_FILTER" | "LOGOUT_FILTER" | "OAUTH2_AUTHORIZATION_REQUEST_FILTER" | "SAML2_AUTHENTICATION_REQUEST_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "OAUTH2_LOGIN_FILTER" | "SAML2_AUTHENTICATION_FILTER" | "FORM_LOGIN_FILTER" | "OPENID_FILTER" | "LOGIN_PAGE_FILTER" |"LOGOUT_PAGE_FILTER" | "DIGEST_AUTH_FILTER" | "BEARER_TOKEN_AUTH_FILTER" | "BASIC_AUTH_FILTER" | "REQUEST_CACHE_FILTER" | "SERVLET_API_SUPPORT_FILTER" | "JAAS_API_SUPPORT_FILTER" | "REMEMBER_ME_FILTER" | "ANONYMOUS_FILTER" | "OAUTH2_AUTHORIZATION_CODE_GRANT_FILTER" | "WELL_KNOWN_CHANGE_PASSWORD_REDIRECT_FILTER" | "SESSION_MANAGEMENT_FILTER" | "EXCEPTION_TRANSLATION_FILTER" | "FILTER_SECURITY_INTERCEPTOR" | "SWITCH_USER_FILTER" | "LAST"

+ 117 - 0
config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd

@@ -1046,6 +1046,15 @@
                   <xs:attributeGroup ref="security:saml2-login.attlist"/>
                </xs:complexType>
             </xs:element>
+            <xs:element name="saml2-logout">
+               <xs:annotation>
+                  <xs:documentation>Configures SAML 2.0 Single Logout support
+                </xs:documentation>
+               </xs:annotation>
+               <xs:complexType>
+                  <xs:attributeGroup ref="security:saml2-logout.attlist"/>
+               </xs:complexType>
+            </xs:element>
             <xs:element name="x509">
                <xs:annotation>
                   <xs:documentation>Adds support for X.509 client authentication.
@@ -2075,6 +2084,63 @@
          </xs:annotation>
       </xs:attribute>
   </xs:attributeGroup>
+  
+  <xs:attributeGroup name="saml2-logout.attlist">
+      <xs:attribute name="logout-url" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>The URL by which the relying or asserting party can trigger logout
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="logout-request-url" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>The URL by which the asserting party can send a SAML 2.0 Logout Request
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="logout-response-url" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>The URL by which the asserting party can send a SAML 2.0 Logout Response
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="relying-party-registration-repository-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Reference to the RelyingPartyRegistrationRepository
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="logout-request-validator-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Reference to the Saml2LogoutRequestValidator
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="logout-request-resolver-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Reference to the Saml2LogoutRequestResolver
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="logout-request-repository-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Reference to the Saml2LogoutRequestRepository
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="logout-response-validator-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Reference to the Saml2LogoutResponseValidator
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="logout-response-resolver-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Reference to the Saml2LogoutResponseResolver
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+  </xs:attributeGroup>
   <xs:element name="relying-party-registrations">
       <xs:annotation>
          <xs:documentation>Container element for relying party(ies) registered with a SAML 2.0 identity provider
@@ -2137,6 +2203,30 @@
                 </xs:documentation>
          </xs:annotation>
       </xs:attribute>
+      <xs:attribute name="single-logout-service-location" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>The relying party &lt;a
+                href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7"&gt;SingleLogoutService
+                Location&lt;/a&gt;
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="single-logout-service-response-location" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>The relying party &lt;a
+                href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7"&gt;SingleLogoutService
+                Response Location&lt;/a&gt;
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="single-logout-service-binding" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>The relying party &lt;a
+                href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7"&gt;SingleLogoutService
+                Binding&lt;/a&gt;
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
   </xs:attributeGroup>
   <xs:element name="signing-credential">
       <xs:annotation>
@@ -2240,6 +2330,30 @@
                 </xs:documentation>
          </xs:annotation>
       </xs:attribute>
+      <xs:attribute name="single-logout-service-location" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>The asserting party &lt;a
+                href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7"&gt;SingleLogoutService
+                Location&lt;/a&gt;
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="single-logout-service-response-location" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>The asserting party &lt;a
+                href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7"&gt;SingleLogoutService
+                Response Location&lt;/a&gt;
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="single-logout-service-binding" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>The asserting party &lt;a
+                href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7"&gt;SingleLogoutService
+                Binding&lt;/a&gt;
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
   </xs:attributeGroup>
   <xs:element name="verification-credential">
       <xs:annotation>
@@ -3605,7 +3719,10 @@
          <xs:enumeration value="WEB_ASYNC_MANAGER_FILTER"/>
          <xs:enumeration value="HEADERS_FILTER"/>
          <xs:enumeration value="CORS_FILTER"/>
+         <xs:enumeration value="SAML2_LOGOUT_REQUEST_FILTER"/>
+         <xs:enumeration value="SAML2_LOGOUT_RESPONSE_FILTER"/>
          <xs:enumeration value="CSRF_FILTER"/>
+         <xs:enumeration value="SAML2_LOGOUT_FILTER"/>
          <xs:enumeration value="LOGOUT_FILTER"/>
          <xs:enumeration value="OAUTH2_AUTHORIZATION_REQUEST_FILTER"/>
          <xs:enumeration value="SAML2_AUTHENTICATION_REQUEST_FILTER"/>

+ 1 - 1
config/src/main/resources/org/springframework/security/config/spring-security.xsl

@@ -9,7 +9,7 @@
     <xsl:output method="xml"  indent="yes"/>
 
     <xsl:variable name="elts-to-inline">
-        <xsl:text>,access-denied-handler,anonymous,session-management,concurrency-control,after-invocation-provider,authentication-provider,ldap-authentication-provider,user,port-mapping,openid-login,saml2-login,expression-handler,form-login,http-basic,intercept-url,logout,password-encoder,port-mappings,port-mapper,password-compare,protect,protect-pointcut,pre-post-annotation-handling,pre-invocation-advice,post-invocation-advice,invocation-attribute-factory,remember-me,salt-source,x509,add-headers,</xsl:text>
+        <xsl:text>,access-denied-handler,anonymous,session-management,concurrency-control,after-invocation-provider,authentication-provider,ldap-authentication-provider,user,port-mapping,openid-login,saml2-login,saml2-logout,expression-handler,form-login,http-basic,intercept-url,logout,password-encoder,port-mappings,port-mapper,password-compare,protect,protect-pointcut,pre-post-annotation-handling,pre-invocation-advice,post-invocation-advice,invocation-attribute-factory,remember-me,salt-source,x509,add-headers,</xsl:text>
     </xsl:variable>
 
     <xsl:template match="xs:element">

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

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 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.

+ 327 - 0
config/src/test/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests.java

@@ -0,0 +1,327 @@
+/*
+ * Copyright 2002-2022 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.http;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.Collections;
+
+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.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.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.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.RelyingPartyRegistration;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
+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.test.context.annotation.SecurityTestExecutionListeners;
+import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+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.Mockito.verify;
+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 {@link Saml2LogoutBeanDefinitionParser}
+ *
+ * @author Marcus da Coregio
+ */
+@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
+@SecurityTestExecutionListeners
+public class Saml2LogoutBeanDefinitionParserTests {
+
+	public final SpringTestContext spring = new SpringTestContext(this);
+
+	private final Saml2LogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository();
+
+	private static final String CONFIG_LOCATION_PREFIX = "classpath:org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests";
+
+	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==";
+
+	@Autowired(required = false)
+	private RelyingPartyRegistrationRepository repository;
+
+	@Autowired
+	private MockMvc mvc;
+
+	private Saml2Authentication saml2User;
+
+	private MockHttpServletRequest request;
+
+	private MockHttpServletResponse response;
+
+	@BeforeEach
+	public void setup() {
+		DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user",
+				Collections.emptyMap());
+		principal.setRelyingPartyRegistrationId("registration-id");
+		this.saml2User = 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();
+	}
+
+	@Test
+	public void logoutWhenLogoutSuccessHandlerAndNotSaml2LoginThenDefaultLogoutSuccessHandler() throws Exception {
+		this.spring.configLocations(this.xml("LogoutSuccessHandler")).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");
+		assertThat(location).isEqualTo("/logoutSuccessEndpoint");
+	}
+
+	@Test
+	public void saml2LogoutWhenDefaultsThenLogsOutAndSendsLogoutRequest() throws Exception {
+		this.spring.configLocations(this.xml("Default")).autowire();
+		MvcResult result = this.mvc.perform(post("/logout").with(authentication(this.saml2User)).with(csrf()))
+				.andExpect(status().isFound()).andReturn();
+		String location = result.getResponse().getHeader("Location");
+		assertThat(location).startsWith("https://ap.example.org/logout/saml2/request");
+	}
+
+	@Test
+	public void saml2LogoutWhenUnauthenticatedThenEntryPoint() throws Exception {
+		this.spring.configLocations(this.xml("Default")).autowire();
+		this.mvc.perform(post("/logout").with(csrf())).andExpect(status().isFound())
+				.andExpect(redirectedUrl("/login?logout"));
+	}
+
+	@Test
+	public void saml2LogoutWhenMissingCsrfThen403() throws Exception {
+		this.spring.configLocations(this.xml("Default")).autowire();
+		this.mvc.perform(post("/logout").with(authentication(this.saml2User))).andExpect(status().isForbidden());
+	}
+
+	@Test
+	public void saml2LogoutWhenGetThenDefaultLogoutPage() throws Exception {
+		this.spring.configLocations(this.xml("Default")).autowire();
+		MvcResult result = this.mvc.perform(get("/logout").with(authentication(this.saml2User)))
+				.andExpect(status().isOk()).andReturn();
+		assertThat(result.getResponse().getContentAsString()).contains("Are you sure you want to log out?");
+	}
+
+	@Test
+	public void saml2LogoutWhenPutOrDeleteThen404() throws Exception {
+		this.spring.configLocations(this.xml("Default")).autowire();
+		this.mvc.perform(put("/logout").with(authentication(this.saml2User)).with(csrf()))
+				.andExpect(status().isNotFound());
+		this.mvc.perform(delete("/logout").with(authentication(this.saml2User)).with(csrf()))
+				.andExpect(status().isNotFound());
+	}
+
+	@Test
+	public void saml2LogoutWhenNoRegistrationThen401() throws Exception {
+		this.spring.configLocations(this.xml("Default")).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.configLocations(this.xml("CsrfDisabled-MockLogoutSuccessHandler")).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.configLocations(this.xml("CustomComponents")).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.saml2User)).with(csrf()));
+		verify(getBean(Saml2LogoutRequestResolver.class)).resolve(any(), any());
+	}
+
+	@Test
+	public void saml2LogoutRequestWhenDefaultsThenLogsOutAndSendsLogoutResponse() throws Exception {
+		this.spring.configLocations(this.xml("Default")).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");
+	}
+
+	@Test
+	public void saml2LogoutRequestWhenNoRegistrationThen400() throws Exception {
+		this.spring.configLocations(this.xml("Default")).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());
+	}
+
+	@Test
+	public void saml2LogoutRequestWhenInvalidSamlRequestThen401() throws Exception {
+		this.spring.configLocations(this.xml("Default")).autowire();
+		this.mvc.perform(get("/logout/saml2/slo").param("SAMLRequest", this.apLogoutRequest)
+				.param("RelayState", this.apLogoutRequestRelayState).param("SigAlg", this.apLogoutRequestSigAlg)
+				.with(authentication(this.saml2User))).andExpect(status().isUnauthorized());
+	}
+
+	@Test
+	public void saml2LogoutRequestWhenCustomLogoutRequestHandlerThenUses() throws Exception {
+		this.spring.configLocations(this.xml("CustomComponents")).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.saml2User)))
+				.andReturn();
+		verify(getBean(Saml2LogoutRequestValidator.class)).validate(any());
+		verify(getBean(Saml2LogoutResponseResolver.class)).resolve(any(), any());
+	}
+
+	@Test
+	public void saml2LogoutResponseWhenDefaultsThenRedirects() throws Exception {
+		this.spring.configLocations(this.xml("Default")).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"));
+		assertThat(this.logoutRequestRepository.loadLogoutRequest(this.request)).isNull();
+	}
+
+	@Test
+	public void saml2LogoutResponseWhenInvalidSamlResponseThen401() throws Exception {
+		this.spring.configLocations(this.xml("Default")).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());
+	}
+
+	@Test
+	public void saml2LogoutResponseWhenCustomLogoutResponseHandlerThenUses() throws Exception {
+		this.spring.configLocations(this.xml("CustomComponents")).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);
+	}
+
+	private String xml(String configName) {
+		return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml";
+	}
+
+}

+ 41 - 0
config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CsrfDisabled-MockLogoutSuccessHandler.xml

@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2002-2022 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<csrf disabled="true"/>
+		<intercept-url pattern="/**" access="authenticated"/>
+		<logout success-handler-ref="logoutSuccessHandler"/>
+		<saml2-login/>
+		<saml2-logout/>
+	</http>
+	
+	<b:bean id="logoutSuccessHandler" class="org.mockito.Mockito" factory-method="mock">
+		<b:constructor-arg value="org.springframework.security.web.authentication.logout.LogoutSuccessHandler"/>
+	</b:bean>
+	
+	<b:import resource="userservice.xml"/>
+	<b:import resource="../saml2/logout-registrations.xml"/>
+</b:beans>

+ 56 - 0
config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CustomComponents.xml

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2002-2022 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
+		<saml2-login/>
+		<saml2-logout logout-request-resolver-ref="logoutRequestResolver" logout-request-repository-ref="logoutRequestRepository"
+				logout-request-validator-ref="logoutRequestValidator" logout-response-validator-ref="logoutResponseValidator" logout-response-resolver-ref="logoutResponseResolver"/>
+	</http>
+	
+	<b:bean id="logoutRequestResolver" class="org.mockito.Mockito" factory-method="mock">
+		<b:constructor-arg value="org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver"/>
+	</b:bean>
+	
+	<b:bean id="logoutRequestRepository" class="org.mockito.Mockito" factory-method="mock">
+		<b:constructor-arg value="org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository"/>
+	</b:bean>
+	
+	<b:bean id="logoutRequestValidator" class="org.mockito.Mockito" factory-method="mock">
+		<b:constructor-arg value="org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator"/>
+	</b:bean>
+	
+	<b:bean id="logoutResponseValidator" class="org.mockito.Mockito" factory-method="mock">
+		<b:constructor-arg value="org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator"/>
+	</b:bean>
+	
+	<b:bean id="logoutResponseResolver" class="org.mockito.Mockito" factory-method="mock">
+		<b:constructor-arg value="org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver"/>
+	</b:bean>
+	
+	<b:import resource="userservice.xml"/>
+	<b:import resource="../saml2/logout-registrations.xml"/>
+</b:beans>

+ 35 - 0
config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-Default.xml

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2002-2022 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
+		<saml2-login/>
+		<saml2-logout/>
+	</http>
+
+	<b:import resource="userservice.xml"/>
+	<b:import resource="../saml2/logout-registrations.xml"/>
+</b:beans>

+ 40 - 0
config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-LogoutSuccessHandler.xml

@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2002-2022 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
+		<logout success-handler-ref="logoutSuccessEndpoint"/>
+		<saml2-login/>
+		<saml2-logout/>
+	</http>
+	
+	<b:bean name="logoutSuccessEndpoint" class="org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler">
+		<b:property name="defaultTargetUrl" value="/logoutSuccessEndpoint"/>
+	</b:bean>
+	
+	<b:import resource="userservice.xml"/>
+	<b:import resource="../saml2/logout-registrations.xml"/>
+</b:beans>

+ 83 - 0
config/src/test/resources/org/springframework/security/config/saml2/logout-registrations.xml

@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://www.springframework.org/schema/security"
+         xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+  <relying-party-registrations>
+    <relying-party-registration registration-id="registration-id"
+                                entity-id="rp-entity-id"
+                                assertion-consumer-service-location="https://rp.example.org/acs"
+                                assertion-consumer-service-binding="REDIRECT"
+                                single-logout-service-location="https://rp.example.org/logout/saml2/request"
+                                single-logout-service-response-location="https://rp.example.org/logout/saml2/response"
+                                asserting-party-id="ap">
+      <signing-credential certificate-location="classpath:org/springframework/security/config/saml2/rp-certificate.crt"
+                          private-key-location="classpath:org/springframework/security/config/saml2/rp-private.key"/>
+    </relying-party-registration>
+  
+    <relying-party-registration registration-id="get"
+                                entity-id="rp-entity-id"
+                                assertion-consumer-service-location="https://rp.example.org/acs"
+                                assertion-consumer-service-binding="REDIRECT"
+                                single-logout-service-location="https://rp.example.org/logout/saml2/request"
+                                single-logout-service-response-location="https://rp.example.org/logout/saml2/response"
+                                single-logout-service-binding="REDIRECT"
+                                asserting-party-id="ap">
+      <signing-credential certificate-location="classpath:org/springframework/security/config/saml2/rp-certificate.crt"
+                          private-key-location="classpath:org/springframework/security/config/saml2/rp-private.key"/>
+    </relying-party-registration>
+  
+    <relying-party-registration registration-id="rp"
+                                entity-id="ap-entity-id"
+                                assertion-consumer-service-location="https://rp.example.org/acs"
+                                assertion-consumer-service-binding="REDIRECT"
+                                single-logout-service-location="https://rp.example.org/logout/saml2/request"
+                                single-logout-service-response-location="https://rp.example.org/logout/saml2/response"
+                                single-logout-service-binding="REDIRECT"
+                                asserting-party-id="rp"/>
+    
+    <asserting-party asserting-party-id="ap" entity-id="ap-entity-id"
+                     single-sign-on-service-location="https://ap.example.org/sso"
+                     single-logout-service-location="https://ap.example.org/logout/saml2/request"
+                     single-logout-service-response-location="https://ap.example.org/logout/saml2/response">
+      <verification-credential
+          certificate-location="classpath:org/springframework/security/config/saml2/idp-certificate.crt"
+          private-key-location="classpath:org/springframework/security/config/saml2/rp-private.key"/>
+      <encryption-credential
+          certificate-location="classpath:org/springframework/security/config/saml2/idp-certificate.crt"
+          private-key-location="classpath:org/springframework/security/config/saml2/rp-private.key"/>
+    </asserting-party>
+  
+    <asserting-party asserting-party-id="rp" entity-id="ap-entity-id"
+                     single-sign-on-service-location="https://rp.example.org/sso"
+                     single-logout-service-location="https://rp.example.org/logout/saml2/request"
+                     single-logout-service-response-location="https://ap.example.org/logout/saml2/response">
+      <verification-credential
+          certificate-location="classpath:org/springframework/security/config/saml2/idp-certificate.crt"
+          private-key-location="classpath:org/springframework/security/config/saml2/rp-private.key"/>
+      <encryption-credential
+          certificate-location="classpath:org/springframework/security/config/saml2/idp-certificate.crt"
+          private-key-location="classpath:org/springframework/security/config/saml2/rp-private.key"/>
+    </asserting-party>
+  </relying-party-registrations>
+</b:beans>

+ 145 - 0
docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc

@@ -165,6 +165,7 @@ The default value is true.
 * <<nsa-remember-me,remember-me>>
 * <<nsa-request-cache,request-cache>>
 * <<nsa-saml2-login,saml2-login>>
+* <<nsa-saml2-logout,saml2-logout>>
 * <<nsa-session-management,session-management>>
 * <<nsa-x509,x509>>
 
@@ -1342,11 +1343,78 @@ The AssertionConsumerService Location. Equivalent to the value found in `&lt;Ass
 the AssertionConsumerService Binding. Equivalent to the value found in `&lt;AssertionConsumerService Binding="..."/&gt;` in the relying party's `&lt;SPSSODescriptor&gt;`.
 The supported values are *POST* and *REDIRECT*.
 
+[[nsa-relying-party-registration-single-logout-service-location]]
+* **single-logout-service-location**
+The SingleLogoutService Location. Equivalent to the value found in &lt;SingleLogoutService Location="..."/&gt; in the relying party's &lt;SPSSODescriptor&gt;.
+
+[[nsa-relying-party-registration-single-logout-service-response-location]]
+* **single-logout-service-response-location**
+The SingleLogoutService ResponseLocation. Equivalent to the value found in &lt;SingleLogoutService ResponseLocation="..."/&gt; in the relying party's &lt;SPSSODescriptor&gt;.
+
+[[nsa-relying-party-registration-single-logout-service-binding]]
+* **single-logout-service-binding**
+The SingleLogoutService Binding. Equivalent to the value found in &lt;SingleLogoutService Binding="..."/&gt; in the relying party's &lt;SPSSODescriptor&gt;.
+The supported values are *POST* and *REDIRECT*.
 
 [[nsa-relying-party-registration-asserting-party-id]]
 * **asserting-party-id**
 A reference to the associated asserting party. Must reference an `<asserting-party>` element.
 
+[[nsa-relying-party-registration-children]]
+=== Child Elements of <relying-party-registration>
+
+* <<nsa-decryption-credential,decryption-credential>>
+* <<nsa-signing-credential,signing-credential>>
+
+
+[[nsa-decryption-credential]]
+== <decryption-credential>
+The decryption credentials associated with the relying party.
+
+
+[[nsa-decryption-credential-parents]]
+=== Parent Elements of <decryption-credential>
+
+* <<nsa-relying-party-registration,relying-party-registration>>
+
+
+[[nsa-decryption-credential-attributes]]
+=== <decryption-credential> Attributes
+
+
+[[nsa-decryption-credential-certificate-location]]
+* **certificate-location**
+The location to get the certificate
+
+[[nsa-decryption-credential-private-key-location]]
+* **private-key-location**
+The location to get the Relying Party's private key
+
+
+[[nsa-signing-credential]]
+== <signing-credential>
+The signing credentials associated with the relying party.
+
+
+[[nsa-signing-credential-parents]]
+=== Parent Elements of <verification-credential>
+
+* <<nsa-relying-party-registration,relying-party-registration>>
+
+
+[[nsa-signing-credential-attributes]]
+=== <verification-credential> Attributes
+
+
+[[nsa-signing-credential-certificate-location]]
+* **certificate-location**
+The location to get this certificate
+
+[[nsa-signing-credential-private-key-location]]
+* **private-key-location**
+The location to get the Relying Party's private key
+
+
 
 [[nsa-asserting-party]]
 == <asserting-party>
@@ -1394,6 +1462,22 @@ The supported values are *POST* and *REDIRECT*.
 The list of `org.opensaml.saml.ext.saml2alg.SigningMethod` Algorithms for this asserting party, in preference order.
 
 
+[[nsa-asserting-party-single-logout-service-location]]
+* **single-logout-service-location**
+The SingleLogoutService Location. Equivalent to the value found in &lt;SingleLogoutService Location="..."/&gt; in the asserting party's &lt;IDPSSODescriptor&gt;.
+
+
+[[nsa-asserting-party-single-logout-service-response-location]]
+* **single-logout-service-response-location**
+The SingleLogoutService ResponseLocation. Equivalent to the value found in &lt;SingleLogoutService ResponseLocation="..."/&gt; in the asserting party's &lt;IDPSSODescriptor&gt;.
+
+
+[[nsa-asserting-party-single-logout-service-binding]]
+* **single-logout-service-binding**
+The SingleLogoutService Binding. Equivalent to the value found in &lt;SingleLogoutService Binding="..."/&gt; in the asserting party's &lt;IDPSSODescriptor&gt;.
+The supported values are *POST* and *REDIRECT*.
+
+
 [[nsa-asserting-party-children]]
 === Child Elements of <asserting-party>
 
@@ -1795,6 +1879,67 @@ Reference to the `AuthenticationFailureHandler`.
 Reference to the `AuthenticationManager`.
 
 
+
+[[nsa-saml2-logout]]
+== <saml2-logout>
+The xref:servlet/saml2/logout.adoc#servlet-saml2login-logout[SAML 2.0 Single Logout] feature configures support for RP- and AP-initiated SAML 2.0 Single Logout.
+
+
+[[nsa-saml2-logout-parents]]
+=== Parent Elements of <saml2-logout>
+
+* <<nsa-http,http>>
+
+[[nsa-saml2-logout-attributes]]
+=== <saml2-logout> Attributes
+
+
+[[nsa-saml2-logout-logout-url]]
+* **logout-url**
+The URL by which the relying or asserting party can trigger logout.
+
+
+[[nsa-saml2-logout-logout-request-url]]
+* **logout-request-url**
+The URL by which the asserting party can send a SAML 2.0 Logout Request.
+
+
+[[nsa-saml2-logout-logout-response-url]]
+* **logout-response-url**
+The URL by which the asserting party can send a SAML 2.0 Logout Response.
+
+
+[[nsa-saml2-logout-relying-party-registration-repository-ref]]
+* **relying-party-registration-repository-ref**
+Reference to the `RelyingPartyRegistrationRepository`.
+
+
+[[nsa-saml2-logout-logout-request-validator-ref]]
+* **logout-request-validator-ref**
+Reference to the `Saml2LogoutRequestValidator`.
+
+
+[[nsa-saml2-logout-logout-request-resolver-ref]]
+* **logout-request-resolver-ref**
+Reference to the `Saml2LogoutRequestResolver`.
+
+
+[[nsa-saml2-logout-logout-request-repository-ref]]
+* **logout-request-repository-ref**
+Reference to the `Saml2LogoutRequestRepository`.
+
+
+[[nsa-saml2-logout-logout-response-validator-ref]]
+* **logout-response-validator-ref**
+Reference to the `Saml2LogoutResponseValidator`.
+
+
+[[nsa-saml2-logout-logout-response-resolver-ref]]
+* **logout-response-resolver-ref**
+Reference to the `Saml2LogoutResponseResolver`.
+
+
+
 [[nsa-attribute-exchange]]
 == <attribute-exchange>
 The `attribute-exchange` element defines the list of attributes which should be requested from the identity provider.