diff --git a/config/src/main/java/org/springframework/security/config/Elements.java b/config/src/main/java/org/springframework/security/config/Elements.java index 8c97c709fb..60e0b7371c 100644 --- a/config/src/main/java/org/springframework/security/config/Elements.java +++ b/config/src/main/java/org/springframework/security/config/Elements.java @@ -136,4 +136,6 @@ public abstract class Elements { public static final String SAML2_LOGIN = "saml2-login"; + public static final String SAML2_LOGOUT = "saml2-logout"; + } diff --git a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java index 8ae2bb34eb..7c04067173 100644 --- a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java @@ -148,6 +148,8 @@ final class AuthenticationConfigBuilder { @SuppressWarnings("rawtypes") private ManagedList logoutHandlers; + private BeanMetadataElement logoutSuccessHandler; + private BeanDefinition loginPageGenerationFilter; private BeanDefinition logoutPageGenerationFilter; @@ -190,6 +192,12 @@ final class AuthenticationConfigBuilder { private String saml2AuthenticationRequestFilterId; + private String saml2LogoutFilterId; + + private String saml2LogoutRequestFilterId; + + private String saml2LogoutResponseFilterId; + private boolean oauth2ClientEnabled; private BeanDefinition authorizationRequestRedirectFilter; @@ -229,6 +237,7 @@ final class AuthenticationConfigBuilder { createX509Filter(authenticationManager); createJeeFilter(authenticationManager); createLogoutFilter(); + createSaml2LogoutFilter(); createLoginPageFilterIfNeeded(); createUserDetailsServiceFactory(); createExceptionTranslationFilter(); @@ -592,9 +601,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) { @@ -822,6 +855,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; } diff --git a/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java index 65c1b3b931..51d9462d5e 100644 --- a/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java +++ b/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; + } + } diff --git a/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java new file mode 100644 index 0000000000..1dce9c7d1b --- /dev/null +++ b/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 jakarta.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 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 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 createDefaultLogoutHandlers() { + List 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 toManagedList(BeanMetadataElement... elements) { + List 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 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; + } + + } + +} diff --git a/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserUtils.java b/config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserUtils.java new file mode 100644 index 0000000000..96ca597889 --- /dev/null +++ b/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(); + } + +} diff --git a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java index bb2722df23..fb79092c0a 100644 --- a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java +++ b/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, diff --git a/config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java index a35f0ba00f..ab55ad0df8 100644 --- a/config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java +++ b/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); } diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-6.0.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-6.0.rnc index c52c2c300c..d2568fb19d 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-6.0.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-6.0.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? & 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? & 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 element. If omitted, the filter chain will match all requests. attribute pattern {xsd:token}? @@ -661,6 +661,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*} @@ -686,6 +717,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 SingleLogoutService Location + attribute single-logout-service-location {xsd:token}? +relying-party-registration.attlist &= + ## The relying party SingleLogoutService Response Location + attribute single-logout-service-response-location {xsd:token}? +relying-party-registration.attlist &= + ## The relying party SingleLogoutService Binding + attribute single-logout-service-binding {xsd:token}? signing-credential = ## The relying party's signing credential @@ -728,6 +768,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 SingleLogoutService Location + attribute single-logout-service-location {xsd:token}? +asserting-party.attlist &= + ## The asserting party SingleLogoutService Response Location + attribute single-logout-service-response-location {xsd:token}? +asserting-party.attlist &= + ## The asserting party SingleLogoutService Binding + attribute single-logout-service-binding {xsd:token}? verification-credential = ## The relying party's verification credential @@ -1238,4 +1287,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" | "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" | "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" diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-6.0.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-6.0.xsd index 40ac9430ee..5af38a04f7 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-6.0.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-6.0.xsd @@ -1024,6 +1024,15 @@ + + + Configures SAML 2.0 Single Logout support + + + + + + Adds support for X.509 client authentication. @@ -1986,6 +1995,63 @@ + + + + + The URL by which the relying or asserting party can trigger logout + + + + + + The URL by which the asserting party can send a SAML 2.0 Logout Request + + + + + + The URL by which the asserting party can send a SAML 2.0 Logout Response + + + + + + Reference to the RelyingPartyRegistrationRepository + + + + + + Reference to the Saml2LogoutRequestValidator + + + + + + Reference to the Saml2LogoutRequestResolver + + + + + + Reference to the Saml2LogoutRequestRepository + + + + + + Reference to the Saml2LogoutResponseValidator + + + + + + Reference to the Saml2LogoutResponseResolver + + + + Container element for relying party(ies) registered with a SAML 2.0 identity provider @@ -2048,6 +2114,30 @@ + + + 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> + + + + + + 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> + + + + + + 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> + + + @@ -2151,6 +2241,30 @@ + + + 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> + + + + + + 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> + + + + + + 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> + + + @@ -3516,7 +3630,10 @@ + + + diff --git a/config/src/main/resources/org/springframework/security/config/spring-security.xsl b/config/src/main/resources/org/springframework/security/config/spring-security.xsl index 5642c8301d..77ee925148 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security.xsl +++ b/config/src/main/resources/org/springframework/security/config/spring-security.xsl @@ -9,7 +9,7 @@ - ,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, + ,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, diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java index 051327548a..2d00a18a76 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java +++ b/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. diff --git a/config/src/test/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests.java new file mode 100644 index 0000000000..2473c80d51 --- /dev/null +++ b/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 getBean(Class clazz) { + return this.spring.getContext().getBean(clazz); + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } + +} diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CsrfDisabled-MockLogoutSuccessHandler.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CsrfDisabled-MockLogoutSuccessHandler.xml new file mode 100644 index 0000000000..7caa59eb92 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CsrfDisabled-MockLogoutSuccessHandler.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CustomComponents.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CustomComponents.xml new file mode 100644 index 0000000000..66068ed58d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CustomComponents.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-Default.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-Default.xml new file mode 100644 index 0000000000..79ad59b7a6 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-Default.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-LogoutSuccessHandler.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-LogoutSuccessHandler.xml new file mode 100644 index 0000000000..9c4b1c6497 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-LogoutSuccessHandler.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/saml2/logout-registrations.xml b/config/src/test/resources/org/springframework/security/config/saml2/logout-registrations.xml new file mode 100644 index 0000000000..c96d06ac7c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/saml2/logout-registrations.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc index 18d824af05..a966491c52 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc @@ -164,6 +164,7 @@ The default value is true. * <> * <> * <> +* <> * <> * <> @@ -1341,6 +1342,18 @@ The AssertionConsumerService Location. Equivalent to the value found in `<Ass the AssertionConsumerService Binding. Equivalent to the value found in `<AssertionConsumerService Binding="..."/>` in the relying party's `<SPSSODescriptor>`. 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 <SingleLogoutService Location="..."/> in the relying party's <SPSSODescriptor>. + +[[nsa-relying-party-registration-single-logout-service-response-location]] +* **single-logout-service-response-location** +The SingleLogoutService ResponseLocation. Equivalent to the value found in <SingleLogoutService ResponseLocation="..."/> in the relying party's <SPSSODescriptor>. + +[[nsa-relying-party-registration-single-logout-service-binding]] +* **single-logout-service-binding** +The SingleLogoutService Binding. Equivalent to the value found in <SingleLogoutService Binding="..."/> in the relying party's <SPSSODescriptor>. +The supported values are *POST* and *REDIRECT*. [[nsa-relying-party-registration-asserting-party-id]] * **asserting-party-id** @@ -1402,7 +1415,6 @@ The location to get the Relying Party's private key - [[nsa-asserting-party]] == The configuration information for a SAML 2.0 Asserting Party. @@ -1449,6 +1461,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 <SingleLogoutService Location="..."/> in the asserting party's <IDPSSODescriptor>. + + +[[nsa-asserting-party-single-logout-service-response-location]] +* **single-logout-service-response-location** +The SingleLogoutService ResponseLocation. Equivalent to the value found in <SingleLogoutService ResponseLocation="..."/> in the asserting party's <IDPSSODescriptor>. + + +[[nsa-asserting-party-single-logout-service-binding]] +* **single-logout-service-binding** +The SingleLogoutService Binding. Equivalent to the value found in <SingleLogoutService Binding="..."/> in the asserting party's <IDPSSODescriptor>. +The supported values are *POST* and *REDIRECT*. + + [[nsa-asserting-party-children]] === Child Elements of @@ -1750,6 +1778,66 @@ Reference to the `AuthenticationFailureHandler`. Reference to the `AuthenticationManager`. +[[nsa-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 + +* <> + +[[nsa-saml2-logout-attributes]] +=== 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-password-management]] == This element configures password management.