From 475b1d6bfe5f20e88fa5f9fd766b4de08934f0cd Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 8 Nov 2021 15:19:04 -0300 Subject: [PATCH 1/2] Add SAML 2.0 Login XML Support Closes gh-9012 --- .../security/config/Elements.java | 6 +- .../config/SecurityNamespaceHandler.java | 4 +- .../http/AuthenticationConfigBuilder.java | 49 ++- .../http/Saml2LoginBeanDefinitionParser.java | 311 ++++++++++++++++ .../Saml2LoginBeanDefinitionParserUtils.java | 107 ++++++ .../security/config/http/SecurityFilters.java | 6 +- ...artyRegistrationsBeanDefinitionParser.java | 335 ++++++++++++++++++ .../security/config/spring-security-5.7.rnc | 122 ++++++- .../security/config/spring-security-5.7.xsd | 280 +++++++++++++++ .../security/config/spring-security.xsl | 2 +- .../Saml2LoginBeanDefinitionParserTests.java | 310 ++++++++++++++++ ...egistrationsBeanDefinitionParserTests.java | 223 ++++++++++++ ...serTests-MultiRelyingPartyRegistration.xml | 34 ++ ...WithCustomAuthenticationFailureHandler.xml | 38 ++ ...erTests-SingleRelyingPartyRegistration.xml | 58 +++ ...gUrl-WithCustomAuthenticationConverter.xml | 38 ++ ...rserTests-WithCustomLoginProcessingUrl.xml | 34 ++ ...tory-WithCustomAuthenticationConverter.xml | 40 +++ ...sitory-WithCustomAuthenticationManager.xml | 40 +++ ...ithCustomAuthenticationRequestResolver.xml | 41 +++ ...itory-WithCustomAuthnRequestRepository.xml | 40 +++ ...Tests-WithCustomRelyingPartyRepository.xml | 43 +++ ...efinitionParserTests-MultiRegistration.xml | 66 ++++ ...finitionParserTests-SingleRegistration.xml | 47 +++ .../saml2/google-custom-registration.xml | 64 ++++ .../config/saml2/google-registration.xml | 45 +++ .../security/config/saml2/idp-certificate.crt | 24 ++ .../security/config/saml2/rp-certificate.crt | 16 + .../security/config/saml2/rp-private.key | 16 + .../servlet/appendix/namespace/http.adoc | 218 ++++++++++++ etc/nohttp/allowlist.lines | 3 + .../RelyingPartyRegistration.java | 4 +- 32 files changed, 2655 insertions(+), 9 deletions(-) create mode 100644 config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParser.java create mode 100644 config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserUtils.java create mode 100644 config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java create mode 100644 config/src/test/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests.java create mode 100644 config/src/test/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests.java create mode 100644 config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-MultiRelyingPartyRegistration.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration-WithCustomAuthenticationFailureHandler.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomLoginProcessingUrl-WithCustomAuthenticationConverter.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomLoginProcessingUrl.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationConverter.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationManager.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationRequestResolver.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthnRequestRepository.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository.xml create mode 100644 config/src/test/resources/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests-MultiRegistration.xml create mode 100644 config/src/test/resources/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests-SingleRegistration.xml create mode 100644 config/src/test/resources/org/springframework/security/config/saml2/google-custom-registration.xml create mode 100644 config/src/test/resources/org/springframework/security/config/saml2/google-registration.xml create mode 100644 config/src/test/resources/org/springframework/security/config/saml2/idp-certificate.crt create mode 100644 config/src/test/resources/org/springframework/security/config/saml2/rp-certificate.crt create mode 100644 config/src/test/resources/org/springframework/security/config/saml2/rp-private.key 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 0b79c47d65d..d57782b7f93 100644 --- a/config/src/main/java/org/springframework/security/config/Elements.java +++ b/config/src/main/java/org/springframework/security/config/Elements.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. @@ -138,4 +138,8 @@ public abstract class Elements { public static final String PASSWORD_MANAGEMENT = "password-management"; + public static final String RELYING_PARTY_REGISTRATIONS = "relying-party-registrations"; + + public static final String SAML2_LOGIN = "saml2-login"; + } diff --git a/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java b/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java index 98ec11e35c5..0887baf2715 100644 --- a/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java +++ b/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2020 the original author or authors. + * Copyright 2009-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. @@ -47,6 +47,7 @@ import org.springframework.security.config.method.MethodSecurityBeanDefinitionParser; import org.springframework.security.config.method.MethodSecurityMetadataSourceBeanDefinitionParser; import org.springframework.security.config.oauth2.client.ClientRegistrationsBeanDefinitionParser; +import org.springframework.security.config.saml2.RelyingPartyRegistrationsBeanDefinitionParser; import org.springframework.security.config.websocket.WebSocketMessageBrokerSecurityBeanDefinitionParser; import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.util.ClassUtils; @@ -190,6 +191,7 @@ private void loadWebParsers() { this.parsers.put(Elements.FILTER_CHAIN, new FilterChainBeanDefinitionParser()); this.filterChainMapBDD = new FilterChainMapBeanDefinitionDecorator(); this.parsers.put(Elements.CLIENT_REGISTRATIONS, new ClientRegistrationsBeanDefinitionParser()); + this.parsers.put(Elements.RELYING_PARTY_REGISTRATIONS, new RelyingPartyRegistrationsBeanDefinitionParser()); } private void loadWebSocketParsers() { 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 d3c0ce32f4b..be623a1dc4f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -202,6 +202,14 @@ final class AuthenticationConfigBuilder { private BeanDefinition oauth2LoginLinks; + private BeanDefinition saml2AuthenticationUrlToProviderName; + + private BeanDefinition saml2AuthorizationRequestFilter; + + private String saml2AuthenticationFilterId; + + private String saml2AuthenticationRequestFilterId; + private boolean oauth2ClientEnabled; private BeanDefinition authorizationRequestRedirectFilter; @@ -238,6 +246,7 @@ final class AuthenticationConfigBuilder { createFormLoginFilter(sessionStrategy, authenticationManager); createOAuth2ClientFilters(sessionStrategy, requestCache, authenticationManager); createOpenIDLoginFilter(sessionStrategy, authenticationManager); + createSaml2LoginFilter(authenticationManager); createX509Filter(authenticationManager); createJeeFilter(authenticationManager); createLogoutFilter(); @@ -412,6 +421,29 @@ void createOpenIDLoginFilter(BeanReference sessionStrategy, BeanReference authMa } } + private void createSaml2LoginFilter(BeanReference authenticationManager) { + Element saml2LoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.SAML2_LOGIN); + if (saml2LoginElt == null) { + return; + } + Saml2LoginBeanDefinitionParser parser = new Saml2LoginBeanDefinitionParser(this.csrfIgnoreRequestMatchers, + this.portMapper, this.portResolver, this.requestCache, this.allowSessionCreation, authenticationManager, + this.authenticationProviders, this.defaultEntryPointMappings); + BeanDefinition saml2WebSsoAuthenticationFilter = parser.parse(saml2LoginElt, this.pc); + this.saml2AuthorizationRequestFilter = parser.getSaml2WebSsoAuthenticationRequestFilter(); + + this.saml2AuthenticationFilterId = this.pc.getReaderContext().generateBeanName(saml2WebSsoAuthenticationFilter); + this.saml2AuthenticationRequestFilterId = this.pc.getReaderContext() + .generateBeanName(this.saml2AuthorizationRequestFilter); + this.saml2AuthenticationUrlToProviderName = parser.getSaml2AuthenticationUrlToProviderName(); + + // register the component + this.pc.registerBeanComponent( + new BeanComponentDefinition(saml2WebSsoAuthenticationFilter, this.saml2AuthenticationFilterId)); + this.pc.registerBeanComponent(new BeanComponentDefinition(this.saml2AuthorizationRequestFilter, + this.saml2AuthenticationRequestFilterId)); + } + /** * Parses OpenID 1.0 and 2.0 - related parts of configuration xmls * @param sessionStrategy sessionStrategy @@ -666,6 +698,12 @@ void createLoginPageFilterIfNeeded() { loginPageFilter.addPropertyValue("Oauth2LoginEnabled", true); loginPageFilter.addPropertyValue("Oauth2AuthenticationUrlToClientName", this.oauth2LoginLinks); } + if (this.saml2AuthenticationFilterId != null) { + loginPageFilter.addConstructorArgReference(this.saml2AuthenticationFilterId); + loginPageFilter.addPropertyValue("saml2LoginEnabled", true); + loginPageFilter.addPropertyValue("saml2AuthenticationUrlToProviderName", + this.saml2AuthenticationUrlToProviderName); + } this.loginPageGenerationFilter = loginPageFilter.getBeanDefinition(); this.logoutPageGenerationFilter = logoutPageFilter.getBeanDefinition(); } @@ -840,7 +878,8 @@ private BeanMetadataElement selectEntryPoint() { if (formLoginElt != null && this.oauth2LoginEntryPoint != null) { return this.formEntryPoint; } - // If form login was enabled through auto-config, and Oauth2 login was not + // If form login was enabled through auto-config, and Oauth2 login & Saml2 + // login was not // enabled then use form login if (this.oauth2LoginEntryPoint == null) { return this.formEntryPoint; @@ -923,6 +962,12 @@ List getFilters() { filters.add(new OrderDecorator(this.authorizationCodeGrantFilter, SecurityFilters.OAUTH2_AUTHORIZATION_CODE_GRANT_FILTER)); } + if (this.saml2AuthenticationFilterId != null) { + filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2AuthenticationFilterId), + SecurityFilters.SAML2_AUTHENTICATION_FILTER)); + filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2AuthenticationRequestFilterId), + SecurityFilters.SAML2_AUTHENTICATION_REQUEST_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/Saml2LoginBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParser.java new file mode 100644 index 00000000000..53a2b0946b5 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParser.java @@ -0,0 +1,311 @@ +/* + * 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.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.w3c.dom.Element; + +import org.springframework.beans.BeanMetadataElement; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanReference; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.ResolvableType; +import org.springframework.security.config.Elements; +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.servlet.filter.Saml2WebSsoAuthenticationFilter; +import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; + +/** + * SAML 2.0 Login {@link BeanDefinitionParser} + * + * @author Marcus da Coregio + * @since 5.7 + */ +final class Saml2LoginBeanDefinitionParser implements BeanDefinitionParser { + + private static final String DEFAULT_LOGIN_URI = DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL; + + private static final String DEFAULT_AUTHENTICATION_REQUEST_PROCESSING_URL = "/saml2/authenticate/{registrationId}"; + + private static final String ATT_LOGIN_PROCESSING_URL = "login-processing-url"; + + private static final String ATT_LOGIN_PAGE = "login-page"; + + private static final String ELT_RELYING_PARTY_REGISTRATION = "relying-party-registration"; + + private static final String ELT_REGISTRATION_ID = "registration-id"; + + private static final String ATT_AUTHENTICATION_FAILURE_HANDLER_REF = "authentication-failure-handler-ref"; + + private static final String ATT_AUTHENTICATION_SUCCESS_HANDLER_REF = "authentication-success-handler-ref"; + + private static final String ATT_AUTHENTICATION_MANAGER_REF = "authentication-manager-ref"; + + private final List csrfIgnoreRequestMatchers; + + private final BeanReference portMapper; + + private final BeanReference portResolver; + + private final BeanReference requestCache; + + private final boolean allowSessionCreation; + + private final BeanReference authenticationManager; + + private final List authenticationProviders; + + private final Map entryPoints; + + private String loginProcessingUrl = Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI; + + private BeanDefinition saml2WebSsoAuthenticationRequestFilter; + + private BeanDefinition saml2AuthenticationUrlToProviderName; + + Saml2LoginBeanDefinitionParser(List csrfIgnoreRequestMatchers, BeanReference portMapper, + BeanReference portResolver, BeanReference requestCache, boolean allowSessionCreation, + BeanReference authenticationManager, List authenticationProviders, + Map entryPoints) { + this.csrfIgnoreRequestMatchers = csrfIgnoreRequestMatchers; + this.portMapper = portMapper; + this.portResolver = portResolver; + this.requestCache = requestCache; + this.allowSessionCreation = allowSessionCreation; + this.authenticationManager = authenticationManager; + this.authenticationProviders = authenticationProviders; + this.entryPoints = entryPoints; + } + + @Override + public BeanDefinition parse(Element element, ParserContext pc) { + String loginProcessingUrl = element.getAttribute(ATT_LOGIN_PROCESSING_URL); + if (StringUtils.hasText(loginProcessingUrl)) { + this.loginProcessingUrl = loginProcessingUrl; + } + BeanDefinition saml2LoginBeanConfig = BeanDefinitionBuilder.rootBeanDefinition(Saml2LoginBeanConfig.class) + .getBeanDefinition(); + String saml2LoginBeanConfigId = pc.getReaderContext().generateBeanName(saml2LoginBeanConfig); + pc.registerBeanComponent(new BeanComponentDefinition(saml2LoginBeanConfig, saml2LoginBeanConfigId)); + registerDefaultCsrfOverride(); + BeanMetadataElement relyingPartyRegistrationRepository = Saml2LoginBeanDefinitionParserUtils + .getRelyingPartyRegistrationRepository(element); + BeanMetadataElement authenticationRequestRepository = Saml2LoginBeanDefinitionParserUtils + .getAuthenticationRequestRepository(element); + BeanMetadataElement authenticationRequestResolver = Saml2LoginBeanDefinitionParserUtils + .getAuthenticationRequestResolver(element); + if (authenticationRequestResolver == null) { + authenticationRequestResolver = Saml2LoginBeanDefinitionParserUtils + .createDefaultAuthenticationRequestResolver(relyingPartyRegistrationRepository); + } + BeanMetadataElement authenticationConverter = Saml2LoginBeanDefinitionParserUtils + .getAuthenticationConverter(element); + if (authenticationConverter == null) { + if (!this.loginProcessingUrl.contains("{registrationId}")) { + pc.getReaderContext().error("loginProcessingUrl must contain {registrationId} path variable", element); + } + authenticationConverter = Saml2LoginBeanDefinitionParserUtils + .createDefaultAuthenticationConverter(relyingPartyRegistrationRepository); + } + // Configure the Saml2WebSsoAuthenticationFilter + BeanDefinitionBuilder saml2WebSsoAuthenticationFilterBuilder = BeanDefinitionBuilder + .rootBeanDefinition(Saml2WebSsoAuthenticationFilter.class) + .addConstructorArgValue(authenticationConverter).addConstructorArgValue(this.loginProcessingUrl) + .addPropertyValue("authenticationRequestRepository", authenticationRequestRepository); + resolveLoginPage(element, pc); + resolveAuthenticationSuccessHandler(element, saml2WebSsoAuthenticationFilterBuilder); + resolveAuthenticationFailureHandler(element, saml2WebSsoAuthenticationFilterBuilder); + resolveAuthenticationManager(element, saml2WebSsoAuthenticationFilterBuilder); + // Configure the Saml2WebSsoAuthenticationRequestFilter + this.saml2WebSsoAuthenticationRequestFilter = BeanDefinitionBuilder + .rootBeanDefinition(Saml2WebSsoAuthenticationRequestFilter.class) + .addConstructorArgValue(authenticationRequestResolver) + .addPropertyValue("authenticationRequestRepository", authenticationRequestRepository) + .getBeanDefinition(); + BeanDefinition saml2AuthenticationProvider = Saml2LoginBeanDefinitionParserUtils.createAuthenticationProvider(); + this.authenticationProviders.add( + new RuntimeBeanReference(pc.getReaderContext().registerWithGeneratedName(saml2AuthenticationProvider))); + this.saml2AuthenticationUrlToProviderName = BeanDefinitionBuilder.rootBeanDefinition(Map.class) + .setFactoryMethodOnBean("getAuthenticationUrlToProviderName", saml2LoginBeanConfigId) + .getBeanDefinition(); + return saml2WebSsoAuthenticationFilterBuilder.getBeanDefinition(); + } + + private void resolveAuthenticationManager(Element element, + BeanDefinitionBuilder saml2WebSsoAuthenticationFilterBuilder) { + String authenticationManagerRef = element.getAttribute(ATT_AUTHENTICATION_MANAGER_REF); + if (StringUtils.hasText(authenticationManagerRef)) { + saml2WebSsoAuthenticationFilterBuilder.addPropertyReference("authenticationManager", + authenticationManagerRef); + } + else { + saml2WebSsoAuthenticationFilterBuilder.addPropertyValue("authenticationManager", + this.authenticationManager); + } + } + + private void resolveLoginPage(Element element, ParserContext parserContext) { + String loginPage = element.getAttribute(ATT_LOGIN_PAGE); + Object source = parserContext.extractSource(element); + BeanDefinition saml2LoginAuthenticationEntryPoint = null; + if (StringUtils.hasText(loginPage)) { + WebConfigUtils.validateHttpRedirect(loginPage, parserContext, source); + saml2LoginAuthenticationEntryPoint = BeanDefinitionBuilder + .rootBeanDefinition(LoginUrlAuthenticationEntryPoint.class).addConstructorArgValue(loginPage) + .addPropertyValue("portMapper", this.portMapper).addPropertyValue("portResolver", this.portResolver) + .getBeanDefinition(); + } + else { + Map identityProviderUrlMap = getIdentityProviderUrlMap(element); + if (identityProviderUrlMap.size() == 1) { + String loginUrl = identityProviderUrlMap.entrySet().iterator().next().getKey(); + saml2LoginAuthenticationEntryPoint = BeanDefinitionBuilder + .rootBeanDefinition(LoginUrlAuthenticationEntryPoint.class).addConstructorArgValue(loginUrl) + .addPropertyValue("portMapper", this.portMapper) + .addPropertyValue("portResolver", this.portResolver).getBeanDefinition(); + } + } + if (saml2LoginAuthenticationEntryPoint != null) { + BeanDefinitionBuilder requestMatcherBuilder = BeanDefinitionBuilder + .rootBeanDefinition(AntPathRequestMatcher.class); + requestMatcherBuilder.addConstructorArgValue(this.loginProcessingUrl); + BeanDefinition requestMatcher = requestMatcherBuilder.getBeanDefinition(); + this.entryPoints.put(requestMatcher, saml2LoginAuthenticationEntryPoint); + } + } + + private void resolveAuthenticationFailureHandler(Element element, + BeanDefinitionBuilder saml2WebSsoAuthenticationFilterBuilder) { + String authenticationFailureHandlerRef = element.getAttribute(ATT_AUTHENTICATION_FAILURE_HANDLER_REF); + if (StringUtils.hasText(authenticationFailureHandlerRef)) { + saml2WebSsoAuthenticationFilterBuilder.addPropertyReference("authenticationFailureHandler", + authenticationFailureHandlerRef); + } + else { + BeanDefinitionBuilder failureHandlerBuilder = BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"); + failureHandlerBuilder.addConstructorArgValue( + DEFAULT_LOGIN_URI + "?" + DefaultLoginPageGeneratingFilter.ERROR_PARAMETER_NAME); + failureHandlerBuilder.addPropertyValue("allowSessionCreation", this.allowSessionCreation); + saml2WebSsoAuthenticationFilterBuilder.addPropertyValue("authenticationFailureHandler", + failureHandlerBuilder.getBeanDefinition()); + } + } + + private void resolveAuthenticationSuccessHandler(Element element, + BeanDefinitionBuilder saml2WebSsoAuthenticationFilterBuilder) { + String authenticationSuccessHandlerRef = element.getAttribute(ATT_AUTHENTICATION_SUCCESS_HANDLER_REF); + if (StringUtils.hasText(authenticationSuccessHandlerRef)) { + saml2WebSsoAuthenticationFilterBuilder.addPropertyReference("authenticationSuccessHandler", + authenticationSuccessHandlerRef); + } + else { + BeanDefinitionBuilder successHandlerBuilder = BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler") + .addPropertyValue("requestCache", this.requestCache); + saml2WebSsoAuthenticationFilterBuilder.addPropertyValue("authenticationSuccessHandler", + successHandlerBuilder.getBeanDefinition()); + } + } + + private void registerDefaultCsrfOverride() { + BeanDefinitionBuilder requestMatcherBuilder = BeanDefinitionBuilder + .rootBeanDefinition(AntPathRequestMatcher.class); + requestMatcherBuilder.addConstructorArgValue(this.loginProcessingUrl); + BeanDefinition requestMatcher = requestMatcherBuilder.getBeanDefinition(); + this.csrfIgnoreRequestMatchers.add(requestMatcher); + } + + private Map getIdentityProviderUrlMap(Element element) { + Map idps = new LinkedHashMap<>(); + Element relyingPartyRegistrationsElt = DomUtils.getChildElementByTagName( + element.getOwnerDocument().getDocumentElement(), Elements.RELYING_PARTY_REGISTRATIONS); + String authenticationRequestProcessingUrl = DEFAULT_AUTHENTICATION_REQUEST_PROCESSING_URL; + if (relyingPartyRegistrationsElt != null) { + List relyingPartyRegList = DomUtils.getChildElementsByTagName(relyingPartyRegistrationsElt, + ELT_RELYING_PARTY_REGISTRATION); + for (Element relyingPartyReg : relyingPartyRegList) { + String registrationId = relyingPartyReg.getAttribute(ELT_REGISTRATION_ID); + idps.put(authenticationRequestProcessingUrl.replace("{registrationId}", registrationId), + registrationId); + } + } + return idps; + } + + BeanDefinition getSaml2WebSsoAuthenticationRequestFilter() { + return this.saml2WebSsoAuthenticationRequestFilter; + } + + BeanDefinition getSaml2AuthenticationUrlToProviderName() { + return this.saml2AuthenticationUrlToProviderName; + } + + /** + * Wrapper bean class to provide configuration from applicationContext + */ + public static class Saml2LoginBeanConfig implements ApplicationContextAware { + + private ApplicationContext context; + + @SuppressWarnings({ "unchecked", "unused" }) + Map getAuthenticationUrlToProviderName() { + Iterable relyingPartyRegistrations = null; + RelyingPartyRegistrationRepository relyingPartyRegistrationRepository = this.context + .getBean(RelyingPartyRegistrationRepository.class); + ResolvableType type = ResolvableType.forInstance(relyingPartyRegistrationRepository).as(Iterable.class); + if (type != ResolvableType.NONE + && RelyingPartyRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) { + relyingPartyRegistrations = (Iterable) relyingPartyRegistrationRepository; + } + if (relyingPartyRegistrations == null) { + return Collections.emptyMap(); + } + String authenticationRequestProcessingUrl = DEFAULT_AUTHENTICATION_REQUEST_PROCESSING_URL; + Map saml2AuthenticationUrlToProviderName = new HashMap<>(); + relyingPartyRegistrations.forEach((registration) -> saml2AuthenticationUrlToProviderName.put( + authenticationRequestProcessingUrl.replace("{registrationId}", registration.getRegistrationId()), + registration.getRegistrationId())); + return saml2AuthenticationUrlToProviderName; + } + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException { + this.context = context; + } + + } + +} diff --git a/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserUtils.java b/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserUtils.java new file mode 100644 index 00000000000..225bcbe0870 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserUtils.java @@ -0,0 +1,107 @@ +/* + * 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.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver; +import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository; +import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter; +import org.springframework.util.StringUtils; + +/** + * @author Marcus da Coregio + * @since 5.7 + */ +final class Saml2LoginBeanDefinitionParserUtils { + + private static final String ATT_RELYING_PARTY_REGISTRATION_REPOSITORY_REF = "relying-party-registration-repository-ref"; + + private static final String ATT_AUTHENTICATION_REQUEST_REPOSITORY_REF = "authentication-request-repository-ref"; + + private static final String ATT_AUTHENTICATION_REQUEST_RESOLVER_REF = "authentication-request-resolver-ref"; + + private static final String ATT_AUTHENTICATION_CONVERTER = "authentication-converter-ref"; + + private Saml2LoginBeanDefinitionParserUtils() { + } + + 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 getAuthenticationRequestRepository(Element element) { + String authenticationRequestRepositoryRef = element.getAttribute(ATT_AUTHENTICATION_REQUEST_REPOSITORY_REF); + if (StringUtils.hasText(authenticationRequestRepositoryRef)) { + return new RuntimeBeanReference(authenticationRequestRepositoryRef); + } + return BeanDefinitionBuilder.rootBeanDefinition(HttpSessionSaml2AuthenticationRequestRepository.class) + .getBeanDefinition(); + } + + static BeanMetadataElement getAuthenticationRequestResolver(Element element) { + String authenticationRequestContextResolver = element.getAttribute(ATT_AUTHENTICATION_REQUEST_RESOLVER_REF); + if (StringUtils.hasText(authenticationRequestContextResolver)) { + return new RuntimeBeanReference(authenticationRequestContextResolver); + } + return null; + } + + static BeanMetadataElement createDefaultAuthenticationRequestResolver( + BeanMetadataElement relyingPartyRegistrationRepository) { + BeanMetadataElement defaultRelyingPartyRegistrationResolver = BeanDefinitionBuilder + .rootBeanDefinition(DefaultRelyingPartyRegistrationResolver.class) + .addConstructorArgValue(relyingPartyRegistrationRepository).getBeanDefinition(); + return BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver") + .addConstructorArgValue(defaultRelyingPartyRegistrationResolver).getBeanDefinition(); + } + + static BeanDefinition createAuthenticationProvider() { + return BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider") + .getBeanDefinition(); + } + + static BeanMetadataElement getAuthenticationConverter(Element element) { + String authenticationConverter = element.getAttribute(ATT_AUTHENTICATION_CONVERTER); + if (StringUtils.hasText(authenticationConverter)) { + return new RuntimeBeanReference(authenticationConverter); + } + return null; + } + + static BeanDefinition createDefaultAuthenticationConverter(BeanMetadataElement relyingPartyRegistrationRepository) { + AbstractBeanDefinition resolver = BeanDefinitionBuilder + .rootBeanDefinition(DefaultRelyingPartyRegistrationResolver.class) + .addConstructorArgValue(relyingPartyRegistrationRepository).getBeanDefinition(); + return BeanDefinitionBuilder.rootBeanDefinition(Saml2AuthenticationTokenConverter.class) + .addConstructorArgValue(resolver).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 c9b053a3b66..b8c6529e617 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 @@ -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. @@ -47,6 +47,8 @@ enum SecurityFilters { OAUTH2_AUTHORIZATION_REQUEST_FILTER, + SAML2_AUTHENTICATION_REQUEST_FILTER, + X509_FILTER, PRE_AUTH_FILTER, @@ -55,6 +57,8 @@ enum SecurityFilters { OAUTH2_LOGIN_FILTER, + SAML2_AUTHENTICATION_FILTER, + FORM_LOGIN_FILTER, OPENID_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 new file mode 100644 index 00000000000..a35f0ba00fe --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java @@ -0,0 +1,335 @@ +/* + * 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.saml2; + +import java.io.InputStream; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.security.converter.RsaKeyConverters; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; + +/** + * @author Marcus da Coregio + * @since 5.7 + */ +public final class RelyingPartyRegistrationsBeanDefinitionParser implements BeanDefinitionParser { + + private static final String ELT_RELYING_PARTY_REGISTRATION = "relying-party-registration"; + + private static final String ELT_SIGNING_CREDENTIAL = "signing-credential"; + + private static final String ELT_DECRYPTION_CREDENTIAL = "decryption-credential"; + + private static final String ELT_ASSERTING_PARTY = "asserting-party"; + + private static final String ELT_VERIFICATION_CREDENTIAL = "verification-credential"; + + private static final String ELT_ENCRYPTION_CREDENTIAL = "encryption-credential"; + + private static final String ATT_REGISTRATION_ID = "registration-id"; + + private static final String ATT_ASSERTING_PARTY_ID = "asserting-party-id"; + + private static final String ATT_ENTITY_ID = "entity-id"; + + private static final String ATT_METADATA_LOCATION = "metadata-location"; + + private static final String ATT_ASSERTION_CONSUMER_SERVICE_LOCATION = "assertion-consumer-service-location"; + + private static final String ATT_ASSERTION_CONSUMER_SERVICE_BINDING = "assertion-consumer-service-binding"; + + private static final String ATT_PRIVATE_KEY_LOCATION = "private-key-location"; + + private static final String ATT_CERTIFICATE_LOCATION = "certificate-location"; + + private static final String ATT_WANT_AUTHN_REQUESTS_SIGNED = "want-authn-requests-signed"; + + private static final String ATT_SINGLE_SIGN_ON_SERVICE_LOCATION = "single-sign-on-service-location"; + + private static final String ATT_SINGLE_SIGN_ON_SERVICE_BINDING = "single-sign-on-service-binding"; + + private static final String ATT_SIGNING_ALGORITHMS = "signing-algorithms"; + + private static final ResourceLoader resourceLoader = new DefaultResourceLoader(); + + @Override + public BeanDefinition parse(Element element, ParserContext parserContext) { + CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), + parserContext.extractSource(element)); + parserContext.pushContainingComponent(compositeDef); + Map> assertingParties = getAssertingParties(element); + List relyingPartyRegistrations = getRelyingPartyRegistrations(element, + assertingParties, parserContext); + BeanDefinition relyingPartyRegistrationRepositoryBean = BeanDefinitionBuilder + .rootBeanDefinition(InMemoryRelyingPartyRegistrationRepository.class) + .addConstructorArgValue(relyingPartyRegistrations).getBeanDefinition(); + String relyingPartyRegistrationRepositoryId = parserContext.getReaderContext() + .generateBeanName(relyingPartyRegistrationRepositoryBean); + parserContext.registerBeanComponent(new BeanComponentDefinition(relyingPartyRegistrationRepositoryBean, + relyingPartyRegistrationRepositoryId)); + parserContext.popAndRegisterContainingComponent(); + return null; + } + + private static Map> getAssertingParties(Element element) { + List assertingPartyElts = DomUtils.getChildElementsByTagName(element, ELT_ASSERTING_PARTY); + Map> providers = new HashMap<>(); + for (Element assertingPartyElt : assertingPartyElts) { + Map assertingParty = new HashMap<>(); + String assertingPartyId = assertingPartyElt.getAttribute(ATT_ASSERTING_PARTY_ID); + String entityId = assertingPartyElt.getAttribute(ATT_ENTITY_ID); + String wantAuthnRequestsSigned = assertingPartyElt.getAttribute(ATT_WANT_AUTHN_REQUESTS_SIGNED); + 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); + 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); + addVerificationCredentials(assertingPartyElt, assertingParty); + addEncryptionCredentials(assertingPartyElt, assertingParty); + providers.put(assertingPartyId, assertingParty); + } + return providers; + } + + private static void addVerificationCredentials(Map assertingParty, + RelyingPartyRegistration.AssertingPartyDetails.Builder builder) { + List verificationCertificateLocations = (List) assertingParty.get(ELT_VERIFICATION_CREDENTIAL); + List verificationCredentials = new ArrayList<>(); + for (String certificateLocation : verificationCertificateLocations) { + verificationCredentials.add(getSaml2VerificationCredential(certificateLocation)); + } + builder.verificationX509Credentials((credentials) -> credentials.addAll(verificationCredentials)); + } + + private static void addEncryptionCredentials(Map assertingParty, + RelyingPartyRegistration.AssertingPartyDetails.Builder builder) { + List encryptionCertificateLocations = (List) assertingParty.get(ELT_ENCRYPTION_CREDENTIAL); + List encryptionCredentials = new ArrayList<>(); + for (String certificateLocation : encryptionCertificateLocations) { + encryptionCredentials.add(getSaml2EncryptionCredential(certificateLocation)); + } + builder.encryptionX509Credentials((credentials) -> credentials.addAll(encryptionCredentials)); + } + + private static void addVerificationCredentials(Element assertingPartyElt, Map assertingParty) { + List verificationCertificateLocations = new ArrayList<>(); + List verificationCredentialElts = DomUtils.getChildElementsByTagName(assertingPartyElt, + ELT_VERIFICATION_CREDENTIAL); + for (Element verificationCredentialElt : verificationCredentialElts) { + String certificateLocation = verificationCredentialElt.getAttribute(ATT_CERTIFICATE_LOCATION); + verificationCertificateLocations.add(certificateLocation); + } + assertingParty.put(ELT_VERIFICATION_CREDENTIAL, verificationCertificateLocations); + } + + private static void addEncryptionCredentials(Element assertingPartyElt, Map assertingParty) { + List encryptionCertificateLocations = new ArrayList<>(); + List encryptionCredentialElts = DomUtils.getChildElementsByTagName(assertingPartyElt, + ELT_VERIFICATION_CREDENTIAL); + for (Element encryptionCredentialElt : encryptionCredentialElts) { + String certificateLocation = encryptionCredentialElt.getAttribute(ATT_CERTIFICATE_LOCATION); + encryptionCertificateLocations.add(certificateLocation); + } + assertingParty.put(ELT_ENCRYPTION_CREDENTIAL, encryptionCertificateLocations); + } + + private List getRelyingPartyRegistrations(Element element, + Map> assertingParties, ParserContext parserContext) { + List relyingPartyRegistrationElts = DomUtils.getChildElementsByTagName(element, + ELT_RELYING_PARTY_REGISTRATION); + List relyingPartyRegistrations = new ArrayList<>(); + for (Element relyingPartyRegistrationElt : relyingPartyRegistrationElts) { + RelyingPartyRegistration.Builder builder = getBuilderFromMetadataLocationIfPossible( + relyingPartyRegistrationElt, assertingParties, parserContext); + addSigningCredentials(relyingPartyRegistrationElt, builder); + addDecryptionCredentials(relyingPartyRegistrationElt, builder); + relyingPartyRegistrations.add(builder.build()); + } + return relyingPartyRegistrations; + } + + private static RelyingPartyRegistration.Builder getBuilderFromMetadataLocationIfPossible( + Element relyingPartyRegistrationElt, Map> assertingParties, + ParserContext parserContext) { + String registrationId = relyingPartyRegistrationElt.getAttribute(ATT_REGISTRATION_ID); + String metadataLocation = relyingPartyRegistrationElt.getAttribute(ATT_METADATA_LOCATION); + if (StringUtils.hasText(metadataLocation)) { + return RelyingPartyRegistrations.fromMetadataLocation(metadataLocation).registrationId(registrationId); + } + String entityId = relyingPartyRegistrationElt.getAttribute(ATT_ENTITY_ID); + String assertionConsumerServiceLocation = relyingPartyRegistrationElt + .getAttribute(ATT_ASSERTION_CONSUMER_SERVICE_LOCATION); + Saml2MessageBinding assertionConsumerServiceBinding = getAssertionConsumerServiceBinding( + relyingPartyRegistrationElt); + return RelyingPartyRegistration.withRegistrationId(registrationId).entityId(entityId) + .assertionConsumerServiceLocation(assertionConsumerServiceLocation) + .assertionConsumerServiceBinding(assertionConsumerServiceBinding) + .assertingPartyDetails((builder) -> buildAssertingParty(relyingPartyRegistrationElt, assertingParties, + builder, parserContext)); + } + + private static void buildAssertingParty(Element relyingPartyElt, Map> assertingParties, + RelyingPartyRegistration.AssertingPartyDetails.Builder builder, ParserContext parserContext) { + String assertingPartyId = relyingPartyElt.getAttribute(ATT_ASSERTING_PARTY_ID); + if (!assertingParties.containsKey(assertingPartyId)) { + Object source = parserContext.extractSource(relyingPartyElt); + parserContext.getReaderContext() + .error(String.format("Could not find asserting party with id %s", assertingPartyId), source); + } + Map assertingParty = assertingParties.get(assertingPartyId); + String entityId = getAsString(assertingParty, ATT_ENTITY_ID); + String wantAuthnRequestsSigned = getAsString(assertingParty, ATT_WANT_AUTHN_REQUESTS_SIGNED); + String singleSignOnServiceLocation = getAsString(assertingParty, ATT_SINGLE_SIGN_ON_SERVICE_LOCATION); + String singleSignOnServiceBinding = getAsString(assertingParty, ATT_SINGLE_SIGN_ON_SERVICE_BINDING); + Saml2MessageBinding saml2MessageBinding = StringUtils.hasText(singleSignOnServiceBinding) + ? Saml2MessageBinding.valueOf(singleSignOnServiceBinding) : Saml2MessageBinding.REDIRECT; + builder.entityId(entityId).wantAuthnRequestsSigned(Boolean.parseBoolean(wantAuthnRequestsSigned)) + .singleSignOnServiceLocation(singleSignOnServiceLocation) + .singleSignOnServiceBinding(saml2MessageBinding); + addSigningAlgorithms(assertingParty, builder); + addVerificationCredentials(assertingParty, builder); + addEncryptionCredentials(assertingParty, builder); + } + + private static void addSigningAlgorithms(Map assertingParty, + RelyingPartyRegistration.AssertingPartyDetails.Builder builder) { + String signingAlgorithmsAttr = getAsString(assertingParty, ATT_SIGNING_ALGORITHMS); + if (StringUtils.hasText(signingAlgorithmsAttr)) { + List signingAlgorithms = Arrays.asList(signingAlgorithmsAttr.split(",")); + builder.signingAlgorithms((s) -> s.addAll(signingAlgorithms)); + } + } + + private static void addSigningCredentials(Element relyingPartyRegistrationElt, + RelyingPartyRegistration.Builder builder) { + List credentialElts = DomUtils.getChildElementsByTagName(relyingPartyRegistrationElt, + ELT_SIGNING_CREDENTIAL); + for (Element credentialElt : credentialElts) { + String privateKeyLocation = credentialElt.getAttribute(ATT_PRIVATE_KEY_LOCATION); + String certificateLocation = credentialElt.getAttribute(ATT_CERTIFICATE_LOCATION); + builder.signingX509Credentials( + (c) -> c.add(getSaml2SigningCredential(privateKeyLocation, certificateLocation))); + } + } + + private static void addDecryptionCredentials(Element relyingPartyRegistrationElt, + RelyingPartyRegistration.Builder builder) { + List credentialElts = DomUtils.getChildElementsByTagName(relyingPartyRegistrationElt, + ELT_DECRYPTION_CREDENTIAL); + for (Element credentialElt : credentialElts) { + String privateKeyLocation = credentialElt.getAttribute(ATT_PRIVATE_KEY_LOCATION); + String certificateLocation = credentialElt.getAttribute(ATT_CERTIFICATE_LOCATION); + Saml2X509Credential credential = getSaml2DecryptionCredential(privateKeyLocation, certificateLocation); + builder.decryptionX509Credentials((c) -> c.add(credential)); + } + } + + private static String getAsString(Map assertingParty, String key) { + return (String) assertingParty.get(key); + } + + private static Saml2MessageBinding getAssertionConsumerServiceBinding(Element relyingPartyRegistrationElt) { + String assertionConsumerServiceBinding = relyingPartyRegistrationElt + .getAttribute(ATT_ASSERTION_CONSUMER_SERVICE_BINDING); + if (StringUtils.hasText(assertionConsumerServiceBinding)) { + return Saml2MessageBinding.valueOf(assertionConsumerServiceBinding); + } + return Saml2MessageBinding.REDIRECT; + } + + private static Saml2X509Credential getSaml2VerificationCredential(String certificateLocation) { + return getSaml2Credential(certificateLocation, Saml2X509Credential.Saml2X509CredentialType.VERIFICATION); + } + + private static Saml2X509Credential getSaml2EncryptionCredential(String certificateLocation) { + return getSaml2Credential(certificateLocation, Saml2X509Credential.Saml2X509CredentialType.ENCRYPTION); + } + + private static Saml2X509Credential getSaml2SigningCredential(String privateKeyLocation, + String certificateLocation) { + return getSaml2Credential(privateKeyLocation, certificateLocation, + Saml2X509Credential.Saml2X509CredentialType.SIGNING); + } + + private static Saml2X509Credential getSaml2DecryptionCredential(String privateKeyLocation, + String certificateLocation) { + return getSaml2Credential(privateKeyLocation, certificateLocation, + Saml2X509Credential.Saml2X509CredentialType.DECRYPTION); + } + + private static Saml2X509Credential getSaml2Credential(String privateKeyLocation, String certificateLocation, + Saml2X509Credential.Saml2X509CredentialType credentialType) { + RSAPrivateKey privateKey = readPrivateKey(privateKeyLocation); + X509Certificate certificate = readCertificate(certificateLocation); + return new Saml2X509Credential(privateKey, certificate, credentialType); + } + + private static Saml2X509Credential getSaml2Credential(String certificateLocation, + Saml2X509Credential.Saml2X509CredentialType credentialType) { + X509Certificate certificate = readCertificate(certificateLocation); + return new Saml2X509Credential(certificate, credentialType); + } + + private static RSAPrivateKey readPrivateKey(String privateKeyLocation) { + Resource privateKey = resourceLoader.getResource(privateKeyLocation); + try (InputStream inputStream = privateKey.getInputStream()) { + return RsaKeyConverters.pkcs8().convert(inputStream); + } + catch (Exception ex) { + throw new IllegalArgumentException(ex); + } + } + + private static X509Certificate readCertificate(String certificateLocation) { + Resource certificate = resourceLoader.getResource(certificateLocation); + try (InputStream inputStream = certificate.getInputStream()) { + return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(inputStream); + } + catch (Exception ex) { + throw new IllegalArgumentException(ex); + } + } + +} diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.7.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.7.rnc index 42b1349c0e2..4a65f9e18f7 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.7.rnc +++ b/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? & 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? & 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}? @@ -659,6 +659,124 @@ openid-attribute.attlist &= ## Specifies the number of attributes that you wish to get back. For example, return 3 emails. The default value is 1. attribute count {xsd:int}? +saml2-login = + ## Configures authentication support for SAML 2.0 Login + element saml2-login {saml2-login.attlist} +saml2-login.attlist &= + ## Reference to the RelyingPartyRegistrationRepository + attribute relying-party-registration-repository-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the Saml2AuthenticationRequestRepository + attribute authentication-request-repository-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the Saml2AuthenticationRequestResolver + attribute authentication-request-resolver-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the AuthenticationConverter + attribute authentication-converter-ref {xsd:token}? +saml2-login.attlist &= + ## The URI where the filter processes authentication requests + attribute login-processing-url {xsd:token}? +saml2-login.attlist &= + ## The URI to send users to login + attribute login-page {xsd:token}? +saml2-login.attlist &= + ## Reference to the AuthenticationSuccessHandler + attribute authentication-success-handler-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the AuthenticationFailureHandler + attribute authentication-failure-handler-ref {xsd:token}? +saml2-login.attlist &= + ## Reference to the AuthenticationManager + attribute authentication-manager-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*} + +relying-party-registration = + ## Represents a relying party registered with a SAML 2.0 identity provider + element relying-party-registration {relying-party-registration.attlist, signing-credential*, decryption-credential*} +relying-party-registration.attlist &= + ## The ID that uniquely identifies the relying party registration. + attribute registration-id {xsd:token} +relying-party-registration.attlist &= + ## The location of the Identity Provider's metadata. + attribute metadata-location {xsd:token}? +relying-party-registration.attlist &= + ## The relying party's EntityID + attribute entity-id {xsd:token}? +relying-party-registration.attlist &= + ## The Assertion Consumer Service Location + attribute assertion-consumer-service-location {xsd:token}? +relying-party-registration.attlist &= + ## The Assertion Consumer Service Binding + attribute assertion-consumer-service-binding {xsd:token}? +relying-party-registration.attlist &= + ## A reference to the associated asserting party. + attribute asserting-party-id {xsd:token}? + +signing-credential = + ## The relying party's signing credential + element signing-credential {signing-credential.attlist} +signing-credential.attlist &= + ## The private key location + attribute private-key-location {xsd:token} +signing-credential.attlist &= + ## The certificate location + attribute certificate-location {xsd:token} + +decryption-credential = + ## The relying party's decryption credential + element decryption-credential {decryption-credential.attlist} +decryption-credential.attlist &= + ## The private key location + attribute private-key-location {xsd:token} +decryption-credential.attlist &= + ## The certificate location + attribute certificate-location {xsd:token} + +asserting-party = + ## The configuration metadata of the Asserting party + element asserting-party {asserting-party.attlist, verification-credential*, encryption-credential*} +asserting-party.attlist &= + ## A unique identifier of the asserting party. + attribute asserting-party-id {xsd:token} +asserting-party.attlist &= + ## The asserting party's EntityID. + attribute entity-id {xsd:token} +asserting-party.attlist &= + ## Indicates the asserting party's preference that relying parties should sign the AuthnRequest before sending + attribute want-authn-requests-signed {xsd:token}? +asserting-party.attlist &= + ## The SingleSignOnService Location. + attribute single-sign-on-service-location {xsd:token} +asserting-party.attlist &= + ## The SingleSignOnService Binding. + attribute single-sign-on-service-binding {xsd:token}? +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}? + +verification-credential = + ## The relying party's verification credential + element verification-credential {verification-credential.attlist} +verification-credential.attlist &= + ## The private key location + attribute private-key-location {xsd:token} +verification-credential.attlist &= + ## The certificate location + attribute certificate-location {xsd:token} + +encryption-credential = + ## The asserting party's encryption credential + element encryption-credential {encryption-credential.attlist} +encryption-credential.attlist &= + ## The private key location + attribute private-key-location {xsd:token} +encryption-credential.attlist &= + ## The certificate location + attribute certificate-location {xsd:token} filter-chain-map = ## Used to explicitly configure a FilterChainProxy instance with a FilterChainMap @@ -1148,4 +1266,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" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "OAUTH2_LOGIN_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" | "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" diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd index 201953d2cf1..fc3d3ccd3f0 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd @@ -1037,6 +1037,15 @@ + + + Configures authentication support for SAML 2.0 Login + + + + + + Adds support for X.509 client authentication. @@ -2009,6 +2018,275 @@ + + + + + Reference to the RelyingPartyRegistrationRepository + + + + + + Reference to the Saml2AuthenticationRequestRepository + + + + + + Reference to the Saml2AuthenticationRequestResolver + + + + + + Reference to the AuthenticationConverter + + + + + + The URI where the filter processes authentication requests + + + + + + The URI to send users to login + + + + + + Reference to the AuthenticationSuccessHandler + + + + + + Reference to the AuthenticationFailureHandler + + + + + + Reference to the AuthenticationManager + + + + + + + Container element for relying party(ies) registered with a SAML 2.0 identity provider + + + + + + + + + + + + Represents a relying party registered with a SAML 2.0 identity provider + + + + + + + + + + + + + + The ID that uniquely identifies the relying party registration. + + + + + + The location of the Identity Provider's metadata. + + + + + + The relying party's EntityID + + + + + + The Assertion Consumer Service Location + + + + + + The Assertion Consumer Service Binding + + + + + + A reference to the associated asserting party. + + + + + + + The relying party's signing credential + + + + + + + + + + The private key location + + + + + + The certificate location + + + + + + + The relying party's decryption credential + + + + + + + + + + The private key location + + + + + + The certificate location + + + + + + + The configuration metadata of the Asserting party + + + + + + + + + + + + + + A unique identifier of the asserting party. + + + + + + The asserting party's EntityID. + + + + + + Indicates the asserting party's preference that relying parties should sign the + AuthnRequest before sending + + + + + + The <a + href="https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.5%20Endpoint">SingleSignOnService</a> + Location. + + + + + + The <a + href="https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.5%20Endpoint">SingleSignOnService</a> + Binding. + + + + + + A comma separated list of org.opensaml.saml.ext.saml2alg.SigningMethod Algorithms for this + asserting party, in preference order. + + + + + + + The relying party's verification credential + + + + + + + + + + The private key location + + + + + + The certificate location + + + + + + + The asserting party's encryption credential + + + + + + + + + + The private key location + + + + + + The certificate location + + + + Used to explicitly configure a FilterChainProxy instance with a FilterChainMap @@ -3330,10 +3608,12 @@ + + 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 46b9e89acc0..5642c8301d9 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,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,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/http/Saml2LoginBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests.java new file mode 100644 index 00000000000..d4ebbd68023 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests.java @@ -0,0 +1,310 @@ +/* + * 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 javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.event.AuthenticationSuccessEvent; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.saml2.core.Saml2ParameterNames; +import org.springframework.security.saml2.core.Saml2Utils; +import org.springframework.security.saml2.core.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken; +import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.TestSaml2AuthenticationRequestContexts; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; +import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository; +import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver; +import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.savedrequest.RequestCache; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +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.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for {@link Saml2LoginBeanDefinitionParser} + * + * @author Marcus da Coregio + */ +@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class }) +@SecurityTestExecutionListeners +public class Saml2LoginBeanDefinitionParserTests { + + private static final String CONFIG_LOCATION_PREFIX = "classpath:org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests"; + + private static final String SIGNED_RESPONSE = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9ycC5leGFtcGxlLm9yZy9hY3MiIElEPSJfYzE3MzM2YTAtNTM1My00MTQ5LWI3MmMtMDNkOWY5YWYzMDdlIiBJc3N1ZUluc3RhbnQ9IjIwMjAtMDgtMDRUMjI6MDQ6NDUuMDE2WiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5hcC1lbnRpdHktaWQ8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KPGRzOlNpZ25lZEluZm8+CjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+CjxkczpSZWZlcmVuY2UgVVJJPSIjX2MxNzMzNmEwLTUzNTMtNDE0OS1iNzJjLTAzZDlmOWFmMzA3ZSI+CjxkczpUcmFuc2Zvcm1zPgo8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz4KPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPgo8L2RzOlRyYW5zZm9ybXM+CjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz4KPGRzOkRpZ2VzdFZhbHVlPjYzTmlyenFzaDVVa0h1a3NuRWUrM0hWWU5aYWFsQW1OQXFMc1lGMlRuRDA9PC9kczpEaWdlc3RWYWx1ZT4KPC9kczpSZWZlcmVuY2U+CjwvZHM6U2lnbmVkSW5mbz4KPGRzOlNpZ25hdHVyZVZhbHVlPgpLMVlvWWJVUjBTclY4RTdVMkhxTTIvZUNTOTNoV25mOExnNnozeGZWMUlyalgzSXhWYkNvMVlYcnRBSGRwRVdvYTJKKzVOMmFNbFBHJiMxMzsKN2VpbDBZRC9xdUVRamRYbTNwQTBjZmEvY25pa2RuKzVhbnM0ZWQwanU1amo2dkpvZ2w2Smt4Q25LWUpwTU9HNzhtampmb0phengrWCYjMTM7CkM2NktQVStBYUdxeGVwUEQ1ZlhRdTFKSy9Jb3lBaitaa3k4Z2Jwc3VyZHFCSEJLRWxjdnVOWS92UGY0OGtBeFZBKzdtRGhNNUMvL1AmIzEzOwp0L084Y3NZYXB2UjZjdjZrdk45QXZ1N3FRdm9qVk1McHVxZWNJZDJwTUVYb0NSSnE2Nkd4MStNTUVPeHVpMWZZQlRoMEhhYjRmK3JyJiMxMzsKOEY2V1NFRC8xZllVeHliRkJqZ1Q4d2lEWHFBRU8wSVY4ZWRQeEE9PQo8L2RzOlNpZ25hdHVyZVZhbHVlPgo8L2RzOlNpZ25hdHVyZT48c2FtbDI6QXNzZXJ0aW9uIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0iQWUzZjQ5OGI4LTliMTctNDA3OC05ZDM1LTg2YTA4NDA4NDk5NSIgSXNzdWVJbnN0YW50PSIyMDIwLTA4LTA0VDIyOjA0OjQ1LjA3N1oiIFZlcnNpb249IjIuMCI+PHNhbWwyOklzc3Vlcj5hcC1lbnRpdHktaWQ8L3NhbWwyOklzc3Vlcj48c2FtbDI6U3ViamVjdD48c2FtbDI6TmFtZUlEPnRlc3RAc2FtbC51c2VyPC9zYW1sMjpOYW1lSUQ+PHNhbWwyOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90QmVmb3JlPSIyMDIwLTA4LTA0VDIxOjU5OjQ1LjA5MFoiIE5vdE9uT3JBZnRlcj0iMjA0MC0wNy0zMFQyMjowNTowNi4wODhaIiBSZWNpcGllbnQ9Imh0dHBzOi8vcnAuZXhhbXBsZS5vcmcvYWNzIi8+PC9zYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDI6U3ViamVjdD48c2FtbDI6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMjAtMDgtMDRUMjE6NTk6NDUuMDgwWiIgTm90T25PckFmdGVyPSIyMDQwLTA3LTMwVDIyOjA1OjA2LjA4N1oiLz48L3NhbWwyOkFzc2VydGlvbj48L3NhbWwycDpSZXNwb25zZT4="; + + private static final String IDP_SSO_URL = "https://sso-url.example.com/IDP/SSO"; + + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired(required = false) + private RequestCache requestCache; + + @Autowired(required = false) + private AuthenticationFailureHandler authenticationFailureHandler; + + @Autowired(required = false) + private AuthenticationSuccessHandler authenticationSuccessHandler; + + @Autowired(required = false) + private RelyingPartyRegistrationRepository repository; + + @Autowired(required = false) + private ApplicationListener authenticationSuccessListener; + + @Autowired(required = false) + private AuthenticationConverter authenticationConverter; + + @Autowired(required = false) + private Saml2AuthenticationRequestResolver authenticationRequestResolver; + + @Autowired(required = false) + private Saml2AuthenticationRequestRepository authenticationRequestRepository; + + @Autowired(required = false) + private ApplicationContext applicationContext; + + @Autowired + private MockMvc mvc; + + @Test + public void requestWhenSingleRelyingPartyRegistrationThenAutoRedirect() throws Exception { + this.spring.configLocations(this.xml("SingleRelyingPartyRegistration")).autowire(); + // @formatter:off + this.mvc.perform(get("/")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/saml2/authenticate/one")); + // @formatter:on + verify(this.requestCache).saveRequest(any(), any()); + } + + @Test + public void requestWhenMultiRelyingPartyRegistrationThenRedirectToLoginWithRelyingParties() throws Exception { + this.spring.configLocations(this.xml("MultiRelyingPartyRegistration")).autowire(); + // @formatter:off + this.mvc.perform(get("/")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/login")); + // @formatter:on + } + + @Test + public void requestLoginWhenMultiRelyingPartyRegistrationThenReturnLoginPageWithRelyingParties() throws Exception { + this.spring.configLocations(this.xml("MultiRelyingPartyRegistration")).autowire(); + // @formatter:off + MvcResult mvcResult = this.mvc.perform(get("/login")) + .andExpect(status().is2xxSuccessful()) + .andReturn(); + // @formatter:on + String pageContent = mvcResult.getResponse().getContentAsString(); + assertThat(pageContent).contains("two"); + assertThat(pageContent).contains("one"); + } + + @Test + public void authenticateWhenAuthenticationResponseNotValidThenThrowAuthenticationException() throws Exception { + this.spring.configLocations(this.xml("SingleRelyingPartyRegistration-WithCustomAuthenticationFailureHandler")) + .autowire(); + this.mvc.perform(get("/login/saml2/sso/one").param(Saml2ParameterNames.SAML_RESPONSE, "samlResponse123")); + ArgumentCaptor exceptionCaptor = ArgumentCaptor + .forClass(AuthenticationException.class); + verify(this.authenticationFailureHandler).onAuthenticationFailure(any(), any(), exceptionCaptor.capture()); + AuthenticationException exception = exceptionCaptor.getValue(); + assertThat(exception).isInstanceOf(Saml2AuthenticationException.class); + assertThat(((Saml2AuthenticationException) exception).getSaml2Error().getErrorCode()) + .isEqualTo("invalid_response"); + } + + @Test + public void authenticateWhenAuthenticationResponseValidThenAuthenticate() throws Exception { + this.spring.configLocations(this.xml("WithCustomRelyingPartyRepository")).autowire(); + RelyingPartyRegistration relyingPartyRegistration = relyingPartyRegistrationWithVerifyingCredential(); + // @formatter:off + this.mvc.perform(post("/login/saml2/sso/" + relyingPartyRegistration.getRegistrationId()).param(Saml2ParameterNames.SAML_RESPONSE, SIGNED_RESPONSE)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().is2xxSuccessful()); + // @formatter:on + ArgumentCaptor authenticationCaptor = ArgumentCaptor.forClass(Authentication.class); + verify(this.authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), authenticationCaptor.capture()); + Authentication authentication = authenticationCaptor.getValue(); + assertThat(authentication.getPrincipal()).isInstanceOf(Saml2AuthenticatedPrincipal.class); + } + + @Test + public void authenticateWhenAuthenticationResponseValidThenAuthenticationSuccessEventPublished() throws Exception { + this.spring.configLocations(this.xml("WithCustomRelyingPartyRepository")).autowire(); + RelyingPartyRegistration relyingPartyRegistration = relyingPartyRegistrationWithVerifyingCredential(); + // @formatter:off + this.mvc.perform(post("/login/saml2/sso/" + relyingPartyRegistration.getRegistrationId()).param(Saml2ParameterNames.SAML_RESPONSE, SIGNED_RESPONSE)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().is2xxSuccessful()); + // @formatter:on + verify(this.authenticationSuccessListener).onApplicationEvent(any(AuthenticationSuccessEvent.class)); + } + + @Test + public void authenticateWhenCustomAuthenticationConverterThenUses() throws Exception { + this.spring.configLocations(this.xml("WithCustomRelyingPartyRepository-WithCustomAuthenticationConverter")) + .autowire(); + RelyingPartyRegistration relyingPartyRegistration = relyingPartyRegistrationWithVerifyingCredential(); + String response = new String(Saml2Utils.samlDecode(SIGNED_RESPONSE)); + given(this.authenticationConverter.convert(any(HttpServletRequest.class))) + .willReturn(new Saml2AuthenticationToken(relyingPartyRegistration, response)); + // @formatter:off + MockHttpServletRequestBuilder request = post("/login/saml2/sso/" + relyingPartyRegistration.getRegistrationId()) + .param("SAMLResponse", SIGNED_RESPONSE); + // @formatter:on + this.mvc.perform(request).andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("/")); + verify(this.authenticationConverter).convert(any(HttpServletRequest.class)); + } + + @Test + public void authenticateWhenCustomAuthenticationManagerThenUses() throws Exception { + this.spring.configLocations(this.xml("WithCustomRelyingPartyRepository-WithCustomAuthenticationManager")) + .autowire(); + RelyingPartyRegistration relyingPartyRegistration = relyingPartyRegistrationWithVerifyingCredential(); + AuthenticationManager authenticationManager = this.applicationContext.getBean("customAuthenticationManager", + AuthenticationManager.class); + String response = new String(Saml2Utils.samlDecode(SIGNED_RESPONSE)); + given(authenticationManager.authenticate(any())) + .willReturn(new Saml2AuthenticationToken(relyingPartyRegistration, response)); + // @formatter:off + MockHttpServletRequestBuilder request = post("/login/saml2/sso/" + relyingPartyRegistration.getRegistrationId()) + .param("SAMLResponse", SIGNED_RESPONSE); + // @formatter:on + this.mvc.perform(request).andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("/")); + verify(authenticationManager).authenticate(any()); + } + + @Test + public void authenticationRequestWhenCustomAuthenticationRequestContextResolverThenUses() throws Exception { + this.spring + .configLocations(this.xml("WithCustomRelyingPartyRepository-WithCustomAuthenticationRequestResolver")) + .autowire(); + Saml2RedirectAuthenticationRequest request = Saml2RedirectAuthenticationRequest + .withAuthenticationRequestContext( + TestSaml2AuthenticationRequestContexts.authenticationRequestContext().build()) + .samlRequest("request").authenticationRequestUri(IDP_SSO_URL).build(); + given(this.authenticationRequestResolver.resolve(any(HttpServletRequest.class))).willReturn(request); + this.mvc.perform(get("/saml2/authenticate/registration-id")).andExpect(status().isFound()); + verify(this.authenticationRequestResolver).resolve(any(HttpServletRequest.class)); + } + + @Test + public void authenticationRequestWhenCustomAuthnRequestRepositoryThenUses() throws Exception { + this.spring.configLocations(this.xml("WithCustomRelyingPartyRepository-WithCustomAuthnRequestRepository")) + .autowire(); + given(this.repository.findByRegistrationId(anyString())) + .willReturn(TestRelyingPartyRegistrations.relyingPartyRegistration().build()); + MockHttpServletRequestBuilder request = get("/saml2/authenticate/registration-id"); + this.mvc.perform(request).andExpect(status().isFound()); + verify(this.authenticationRequestRepository).saveAuthenticationRequest( + any(AbstractSaml2AuthenticationRequest.class), any(HttpServletRequest.class), + any(HttpServletResponse.class)); + } + + @Test + public void authenticateWhenCustomAuthnRequestRepositoryThenUses() throws Exception { + this.spring.configLocations(this.xml("WithCustomRelyingPartyRepository-WithCustomAuthnRequestRepository")) + .autowire(); + RelyingPartyRegistrationRepository repository = mock(RelyingPartyRegistrationRepository.class); + given(this.repository.findByRegistrationId(anyString())) + .willReturn(TestRelyingPartyRegistrations.relyingPartyRegistration().build()); + MockHttpServletRequestBuilder request = post("/login/saml2/sso/registration-id").param("SAMLResponse", + SIGNED_RESPONSE); + this.mvc.perform(request); + verify(this.authenticationRequestRepository).loadAuthenticationRequest(any(HttpServletRequest.class)); + verify(this.authenticationRequestRepository).removeAuthenticationRequest(any(HttpServletRequest.class), + any(HttpServletResponse.class)); + } + + @Test + public void saml2LoginWhenLoginProcessingUrlWithoutRegistrationIdAndDefaultAuthenticationConverterThenValidates() { + assertThatExceptionOfType(BeanDefinitionParsingException.class) + .isThrownBy(() -> this.spring.configLocations(this.xml("WithCustomLoginProcessingUrl")).autowire()) + .withMessageContaining("loginProcessingUrl must contain {registrationId} path variable"); + } + + @Test + public void authenticateWhenCustomLoginProcessingUrlAndCustomAuthenticationConverterThenAuthenticate() + throws Exception { + this.spring.configLocations(this.xml("WithCustomLoginProcessingUrl-WithCustomAuthenticationConverter")) + .autowire(); + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials() + .assertingPartyDetails((party) -> party.verificationX509Credentials( + (c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) + .build(); + String response = new String(Saml2Utils.samlDecode(SIGNED_RESPONSE)); + given(this.authenticationConverter.convert(any(HttpServletRequest.class))) + .willReturn(new Saml2AuthenticationToken(relyingPartyRegistration, response)); + // @formatter:off + MockHttpServletRequestBuilder request = post("/my/custom/url").param("SAMLResponse", SIGNED_RESPONSE); + // @formatter:on + this.mvc.perform(request).andExpect(redirectedUrl("/")); + verify(this.authenticationConverter).convert(any(HttpServletRequest.class)); + } + + private RelyingPartyRegistration relyingPartyRegistrationWithVerifyingCredential() { + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials() + .assertingPartyDetails((party) -> party.verificationX509Credentials( + (c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) + .build(); + given(this.repository.findByRegistrationId(anyString())).willReturn(relyingPartyRegistration); + return relyingPartyRegistration; + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } + +} diff --git a/config/src/test/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests.java new file mode 100644 index 00000000000..f90a2be1b5f --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests.java @@ -0,0 +1,223 @@ +/* + * 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.saml2; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link RelyingPartyRegistrationsBeanDefinitionParser}. + * + * @author Marcus da Coregio + */ +@ExtendWith(SpringTestContextExtension.class) +public class RelyingPartyRegistrationsBeanDefinitionParserTests { + + private static final String CONFIG_LOCATION_PREFIX = "classpath:org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests"; + + // @formatter:off + private static final String METADATA_LOCATION_XML_CONFIG = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + "\n"; + // @formatter:on + + // @formatter:off + private static final String METADATA_RESPONSE = "\n" + + "\n" + + " \n" + + " \n" + + " qIGOB+m2Kuq9Vp6F9qs/EFvFzuo6qEGukjICPyVAkjk=NgKak4k9LBAqbi8Za8ALUXW1l4npZ4+MOf8jhmpePDP3msbzjeKkkWFgxx+ILLJYwZzVWd3l028xm2l+SBOwoYRKJ670NgcdSdj6plBTGiZ5NXsXrX5M0zmgvAShREgjth/BKTUct5UVJOTqIxOPwBuCnj+Nn1+QUtY9ekPLrM0O2i+g1wckKaP6D7N+uVBwNgZGoOj5bZ082G7QXRX6Jo0925uKczAIKdIiBbMeKa/0phS2L97AkgQRGi2+j8V66TaDWuDSwd9hA2qzCwjsNui4DVLBwP0/LvgUdcu8g7JBIZ1yTddfByefOTVsU7UuZXkYEn4jU2ouk+u5klSo3Q==\n" + + "MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYDVQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwXc2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0BwaXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAaBgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQDDBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlrQHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduOnRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+vZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLuxbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6zV9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYDVQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwXc2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0BwaXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAaBgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQDDBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlrQHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduOnRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+vZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLuxbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6zV9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYDVQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwXc2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0BwaXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAaBgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQDDBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlrQHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduOnRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+vZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLuxbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6zV9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n" + + " \n" + + " \n" + + " \n" + + " John\n" + + " Doe\n" + + " john@doe.com\n" + + " \n" + + "\n"; + // @formatter:on + + @Autowired + private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; + + public final SpringTestContext spring = new SpringTestContext(this); + + private MockWebServer server; + + @AfterEach + void cleanup() throws Exception { + if (this.server != null) { + this.server.shutdown(); + } + } + + @Test + public void parseWhenMetadataLocationConfiguredThenRequestMetadataFromLocation() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + String serverUrl = this.server.url("/").toString(); + this.server.enqueue(xmlResponse(METADATA_RESPONSE)); + String metadataConfig = METADATA_LOCATION_XML_CONFIG.replace("${metadata-location}", serverUrl); + this.spring.context(metadataConfig).autowire(); + assertThat(this.relyingPartyRegistrationRepository) + .isInstanceOf(InMemoryRelyingPartyRegistrationRepository.class); + RelyingPartyRegistration relyingPartyRegistration = this.relyingPartyRegistrationRepository + .findByRegistrationId("one"); + RelyingPartyRegistration.AssertingPartyDetails assertingPartyDetails = relyingPartyRegistration + .getAssertingPartyDetails(); + assertThat(relyingPartyRegistration).isNotNull(); + assertThat(relyingPartyRegistration.getRegistrationId()).isEqualTo("one"); + assertThat(relyingPartyRegistration.getEntityId()) + .isEqualTo("{baseUrl}/saml2/service-provider-metadata/{registrationId}"); + assertThat(relyingPartyRegistration.getAssertionConsumerServiceLocation()) + .isEqualTo("{baseUrl}/login/saml2/sso/{registrationId}"); + assertThat(relyingPartyRegistration.getAssertionConsumerServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(assertingPartyDetails.getEntityId()) + .isEqualTo("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php"); + assertThat(assertingPartyDetails.getWantAuthnRequestsSigned()).isFalse(); + assertThat(assertingPartyDetails.getVerificationX509Credentials()).hasSize(1); + assertThat(assertingPartyDetails.getEncryptionX509Credentials()).hasSize(1); + assertThat(assertingPartyDetails.getSingleSignOnServiceLocation()) + .isEqualTo("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php"); + assertThat(assertingPartyDetails.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); + assertThat(assertingPartyDetails.getSigningAlgorithms()) + .containsExactly("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"); + } + + @Test + public void parseWhenSingleRelyingPartyRegistrationThenAvailableInRepository() { + this.spring.configLocations(xml("SingleRegistration")).autowire(); + assertThat(this.relyingPartyRegistrationRepository) + .isInstanceOf(InMemoryRelyingPartyRegistrationRepository.class); + RelyingPartyRegistration relyingPartyRegistration = this.relyingPartyRegistrationRepository + .findByRegistrationId("one"); + RelyingPartyRegistration.AssertingPartyDetails assertingPartyDetails = relyingPartyRegistration + .getAssertingPartyDetails(); + assertThat(relyingPartyRegistration).isNotNull(); + assertThat(relyingPartyRegistration.getRegistrationId()).isEqualTo("one"); + assertThat(relyingPartyRegistration.getEntityId()) + .isEqualTo("{baseUrl}/saml2/service-provider-metadata/{registrationId}"); + assertThat(relyingPartyRegistration.getAssertionConsumerServiceLocation()) + .isEqualTo("{baseUrl}/login/saml2/sso/{registrationId}"); + assertThat(relyingPartyRegistration.getAssertionConsumerServiceBinding()) + .isEqualTo(Saml2MessageBinding.REDIRECT); + assertThat(assertingPartyDetails.getEntityId()).isEqualTo("https://accounts.google.com/o/saml2/idp/entity-id"); + assertThat(assertingPartyDetails.getWantAuthnRequestsSigned()).isTrue(); + assertThat(assertingPartyDetails.getSingleSignOnServiceLocation()) + .isEqualTo("https://accounts.google.com/o/saml2/idp/sso-url"); + assertThat(assertingPartyDetails.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(assertingPartyDetails.getVerificationX509Credentials()).hasSize(1); + assertThat(assertingPartyDetails.getEncryptionX509Credentials()).hasSize(1); + assertThat(assertingPartyDetails.getSigningAlgorithms()) + .containsExactly("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"); + } + + @Test + public void parseWhenMultiRelyingPartyRegistrationThenAvailableInRepository() { + this.spring.configLocations(xml("MultiRegistration")).autowire(); + assertThat(this.relyingPartyRegistrationRepository) + .isInstanceOf(InMemoryRelyingPartyRegistrationRepository.class); + RelyingPartyRegistration one = this.relyingPartyRegistrationRepository.findByRegistrationId("one"); + RelyingPartyRegistration.AssertingPartyDetails google = one.getAssertingPartyDetails(); + RelyingPartyRegistration two = this.relyingPartyRegistrationRepository.findByRegistrationId("two"); + RelyingPartyRegistration.AssertingPartyDetails simpleSaml = two.getAssertingPartyDetails(); + assertThat(one).isNotNull(); + assertThat(one.getRegistrationId()).isEqualTo("one"); + assertThat(one.getEntityId()).isEqualTo("{baseUrl}/saml2/service-provider-metadata/{registrationId}"); + assertThat(one.getAssertionConsumerServiceLocation()).isEqualTo("{baseUrl}/login/saml2/sso/{registrationId}"); + assertThat(one.getAssertionConsumerServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); + assertThat(google.getEntityId()).isEqualTo("https://accounts.google.com/o/saml2/idp/entity-id"); + assertThat(google.getWantAuthnRequestsSigned()).isTrue(); + assertThat(google.getSingleSignOnServiceLocation()) + .isEqualTo("https://accounts.google.com/o/saml2/idp/sso-url"); + assertThat(google.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(google.getVerificationX509Credentials()).hasSize(1); + assertThat(google.getEncryptionX509Credentials()).hasSize(1); + assertThat(google.getSigningAlgorithms()).containsExactly("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"); + assertThat(two).isNotNull(); + assertThat(two.getRegistrationId()).isEqualTo("two"); + assertThat(two.getEntityId()).isEqualTo("{baseUrl}/saml2/service-provider-metadata/{registrationId}"); + assertThat(two.getAssertionConsumerServiceLocation()).isEqualTo("{baseUrl}/login/saml2/sso/{registrationId}"); + assertThat(two.getAssertionConsumerServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(simpleSaml.getEntityId()) + .isEqualTo("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/metadata.php"); + assertThat(simpleSaml.getWantAuthnRequestsSigned()).isFalse(); + assertThat(simpleSaml.getSingleSignOnServiceLocation()) + .isEqualTo("https://simplesaml-for-spring-saml.apps.pcfone.io/saml2/idp/SSOService.php"); + assertThat(simpleSaml.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(simpleSaml.getVerificationX509Credentials()).hasSize(1); + assertThat(simpleSaml.getEncryptionX509Credentials()).hasSize(1); + assertThat(simpleSaml.getSigningAlgorithms()).containsExactly( + "http://www.w3.org/2001/04/xmldsig-more#rsa-sha224", + "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"); + } + + private static MockResponse xmlResponse(String xml) { + return new MockResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE).setBody(xml); + } + + private static String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } + +} diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-MultiRelyingPartyRegistration.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-MultiRelyingPartyRegistration.xml new file mode 100644 index 00000000000..45349131c78 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-MultiRelyingPartyRegistration.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration-WithCustomAuthenticationFailureHandler.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration-WithCustomAuthenticationFailureHandler.xml new file mode 100644 index 00000000000..fcd568996fc --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration-WithCustomAuthenticationFailureHandler.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration.xml new file mode 100644 index 00000000000..d694c40b12f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomLoginProcessingUrl-WithCustomAuthenticationConverter.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomLoginProcessingUrl-WithCustomAuthenticationConverter.xml new file mode 100644 index 00000000000..687f2700b0c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomLoginProcessingUrl-WithCustomAuthenticationConverter.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomLoginProcessingUrl.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomLoginProcessingUrl.xml new file mode 100644 index 00000000000..64b1e70b6c9 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomLoginProcessingUrl.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationConverter.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationConverter.xml new file mode 100644 index 00000000000..2c8c8d620c4 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationConverter.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationManager.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationManager.xml new file mode 100644 index 00000000000..af5de241c3f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationManager.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationRequestResolver.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationRequestResolver.xml new file mode 100644 index 00000000000..806a6fdb528 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationRequestResolver.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthnRequestRepository.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthnRequestRepository.xml new file mode 100644 index 00000000000..91e270febc0 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthnRequestRepository.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository.xml new file mode 100644 index 00000000000..5fc49103dc1 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests-MultiRegistration.xml b/config/src/test/resources/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests-MultiRegistration.xml new file mode 100644 index 00000000000..dd274db7eb2 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests-MultiRegistration.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests-SingleRegistration.xml b/config/src/test/resources/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests-SingleRegistration.xml new file mode 100644 index 00000000000..2b97e6f1c13 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParserTests-SingleRegistration.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/saml2/google-custom-registration.xml b/config/src/test/resources/org/springframework/security/config/saml2/google-custom-registration.xml new file mode 100644 index 00000000000..2e4b358083c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/saml2/google-custom-registration.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/saml2/google-registration.xml b/config/src/test/resources/org/springframework/security/config/saml2/google-registration.xml new file mode 100644 index 00000000000..445d1e5d64f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/saml2/google-registration.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/saml2/idp-certificate.crt b/config/src/test/resources/org/springframework/security/config/saml2/idp-certificate.crt new file mode 100644 index 00000000000..9c4ee078e22 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/saml2/idp-certificate.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYD +VQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYD +VQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwX +c2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0Bw +aXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJ +BgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAa +BgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQD +DBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlr +QHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62 +E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz +2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWW +RDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQ +nX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5 +cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gph +iJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5 +ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTAD +AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduO +nRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+v +ZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLu +xbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6z +V9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3 +lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk +-----END CERTIFICATE----- diff --git a/config/src/test/resources/org/springframework/security/config/saml2/rp-certificate.crt b/config/src/test/resources/org/springframework/security/config/saml2/rp-certificate.crt new file mode 100644 index 00000000000..b907e2fffde --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/saml2/rp-certificate.crt @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBhMC +VVMxEzARBgNVBAgMCldhc2hpbmd0b24xEjAQBgNVBAcMCVZhbmNvdXZlcjEdMBsG +A1UECgwUU3ByaW5nIFNlY3VyaXR5IFNBTUwxCzAJBgNVBAsMAnNwMSAwHgYDVQQD +DBdzcC5zcHJpbmcuc2VjdXJpdHkuc2FtbDAeFw0xODA1MTQxNDMwNDRaFw0yODA1 +MTExNDMwNDRaMIGEMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjES +MBAGA1UEBwwJVmFuY291dmVyMR0wGwYDVQQKDBRTcHJpbmcgU2VjdXJpdHkgU0FN +TDELMAkGA1UECwwCc3AxIDAeBgNVBAMMF3NwLnNwcmluZy5zZWN1cml0eS5zYW1s +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDRu7/EI0BlNzMEBFVAcbx+lLos +vzIWU+01dGTY8gBdhMQNYKZ92lMceo2CuVJ66cUURPym3i7nGGzoSnAxAre+0YIM ++U0razrWtAUE735bkcqELZkOTZLelaoOztmWqRbe5OuEmpewH7cx+kNgcVjdctOG +y3Q6x+I4qakY/9qhBQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAAeViTvHOyQopWEi +XOfI2Z9eukwrSknDwq/zscR0YxwwqDBMt/QdAODfSwAfnciiYLkmEjlozWRtOeN+ +qK7UFgP1bRl5qksrYX5S0z2iGJh0GvonLUt3e20Ssfl5tTEDDnAEUMLfBkyaxEHD +RZ/nbTJ7VTeZOSyRoVn5XHhpuJ0B +-----END CERTIFICATE----- diff --git a/config/src/test/resources/org/springframework/security/config/saml2/rp-private.key b/config/src/test/resources/org/springframework/security/config/saml2/rp-private.key new file mode 100644 index 00000000000..73196e020c5 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/saml2/rp-private.key @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANG7v8QjQGU3MwQE +VUBxvH6Uuiy/MhZT7TV0ZNjyAF2ExA1gpn3aUxx6jYK5UnrpxRRE/KbeLucYbOhK +cDECt77Rggz5TStrOta0BQTvfluRyoQtmQ5Nkt6Vqg7O2ZapFt7k64Sal7AftzH6 +Q2BxWN1y04bLdDrH4jipqRj/2qEFAgMBAAECgYEAj4ExY1jjdN3iEDuOwXuRB+Nn +x7pC4TgntE2huzdKvLJdGvIouTArce8A6JM5NlTBvm69mMepvAHgcsiMH1zGr5J5 +wJz23mGOyhM1veON41/DJTVG+cxq4soUZhdYy3bpOuXGMAaJ8QLMbQQoivllNihd +vwH0rNSK8LTYWWPZYIECQQDxct+TFX1VsQ1eo41K0T4fu2rWUaxlvjUGhK6HxTmY +8OMJptunGRJL1CUjIb45Uz7SP8TPz5FwhXWsLfS182kRAkEA3l+Qd9C9gdpUh1uX +oPSNIxn5hFUrSTW1EwP9QH9vhwb5Vr8Jrd5ei678WYDLjUcx648RjkjhU9jSMzIx +EGvYtQJBAMm/i9NR7IVyyNIgZUpz5q4LI21rl1r4gUQuD8vA36zM81i4ROeuCly0 +KkfdxR4PUfnKcQCX11YnHjk9uTFj75ECQEFY/gBnxDjzqyF35hAzrYIiMPQVfznt +YX/sDTE2AdVBVGaMj1Cb51bPHnNC6Q5kXKQnj/YrLqRQND09Q7ParX0CQQC5NxZr +9jKqhHj8yQD6PlXTsY4Occ7DH6/IoDenfdEVD5qlet0zmd50HatN2Jiqm5ubN7CM +INrtuLp4YHbgk1mi +-----END PRIVATE KEY----- diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc index 2191efcc06f..12e71362f92 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. * <> * <> * <> +* <> * <> * <> @@ -1290,6 +1291,165 @@ The Client Id to use for client authentication against the provided `introspecti * **client-secret** The Client Secret to use for client authentication against the provided `introspection-uri`. + +[[nsa-relying-party-registrations]] +== +The container element for relying party(ies) registered (xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[ClientRegistration]) with a SAML 2.0 Identity Provider. + + +[[nsa-relying-party-registrations-children]] +=== Child Elements of + +* <> +* <> + + +[[nsa-relying-party-registration]] +== +Represents a relying party registered with a SAML 2.0 Identity Provider + + +[[nsa-relying-party-registration-parents]] +=== Parent Elements of + +* <> + + +[[nsa-relying-party-registration-attributes]] +=== Attributes + + +[[nsa-relying-party-registration-registration-id]] +* **registration-id** +The ID that uniquely identifies the `RelyingPartyRegistration`. + +[[nsa-relying-party-registration-metadata-location]] +* **metadata-location** +The asserting party metadata location. + +[[nsa-relying-party-registration-entity-id]] +* **client-id** +The relying party's https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.9%20EntityDescriptor[EntityID]. + + +[[nsa-relying-party-registration-assertion-consumer-service-location]] +* **assertion-consumer-service-location** +The AssertionConsumerService Location. Equivalent to the value found in `<AssertionConsumerService Location="..."/>` in the relying party's `<SPSSODescriptor>`. + + +[[nsa-relying-party-registration-assertion-consumer-service-binding]] +* **assertion-consumer-service-binding** +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-asserting-party-id]] +* **asserting-party-id** +A reference to the associated asserting party. Must reference an `` element. + + +[[nsa-asserting-party]] +== +The configuration information for a SAML 2.0 Asserting Party. + + +[[nsa-asserting-party-parents]] +=== Parent Elements of + +* <> + + +[[nsa-asserting-party-attributes]] +=== Attributes + + +[[nsa-asserting-party-asserting-party-id]] +* **asserting-party-id** +The ID that uniquely identifies the asserting party. + + +[[nsa-asserting-party-entity-id]] +* **entity-id** +The EntityID of the Asserting Party + + +[[nsa-asserting-party-want-authn-requests-signed]] +* **want-authn-requests-signed** +The `WantAuthnRequestsSigned` setting, indicating the asserting party's preference that relying parties should sign the `AuthnRequest` before sending. + + +[[nsa-asserting-party-single-sign-on-service-location]] +* **single-sign-on-service-location** +The https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.5%20Endpoint[SingleSignOnService] Location. + + +[[nsa-asserting-party-single-sign-on-service-binding]] +* **single-sign-on-service-binding** +The https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.5%20Endpoint[SingleSignOnService] Binding. +The supported values are *POST* and *REDIRECT*. + + +[[nsa-asserting-party-signing-algorithms]] +* **signing-algorithms** +The list of `org.opensaml.saml.ext.saml2alg.SigningMethod` Algorithms for this asserting party, in preference order. + + +[[nsa-asserting-party-children]] +=== Child Elements of + +* <> +* <> + + +[[nsa-encryption-credential]] +== +The encryption credentials associated with the asserting party. + + +[[nsa-encryption-credential-parents]] +=== Parent Elements of + +* <> + + +[[nsa-encryption-credential-attributes]] +=== Attributes + + +[[nsa-encryption-credential-certificate-location]] +* **certificate-location** +The location to get the certificate + +[[nsa-encryption-credential-private-key-location]] +* **private-key-location** +The location to get the Relying Party's private key + + +[[nsa-verification-credential]] +== +The verification credentials associated with the asserting party. + + +[[nsa-verification-credential-parents]] +=== Parent Elements of + +* <> + + +[[nsa-verification-credential-attributes]] +=== Attributes + + +[[nsa-verification-credential-certificate-location]] +* **certificate-location** +The location to get this certificate + +[[nsa-verification-credential-private-key-location]] +* **private-key-location** +The location to get the Relying Party's private key + + + [[nsa-http-basic]] == Adds a `BasicAuthenticationFilter` and `BasicAuthenticationEntryPoint` to the configuration. @@ -1576,6 +1736,64 @@ Defaults to "username". * <> +[[nsa-saml2-login]] +== +The xref:servlet/saml2/login/index.adoc#servlet-saml2login[SAML 2.0 Login] feature configures authentication support using an SAML 2.0 Service Provider. + + +[[nsa-saml2-login-parents]] +=== Parent Elements of + +* <> + +[[nsa-saml2-login-attributes]] +=== Attributes + + +[[nsa-saml2-login-relying-party-registration-repository-ref]] +* **relying-party-registration-repository-ref** +Reference to the `RelyingPartyRegistrationRepository`. + + +[[nsa-saml2-login-authentication-request-repository-ref]] +* **authentication-request-repository-ref** +Reference to the `Saml2AuthenticationRequestRepository`. + + +[[nsa-saml2-login-authentication-request-resolver-ref]] +* **authentication-request-context-resolver-ref** +Reference to the `Saml2AuthenticationRequestResolver`. + + +[[nsa-saml2-login-authentication-converter-ref]] +* **authentication-converter-ref** +Reference to the `AuthenticationConverter`. + + +[[nsa-saml2-login-login-processing-url]] +* **login-processing-url** +The URI where the filter processes authentication requests. + + +[[nsa-saml2-login-login-page]] +* **login-page** +The URI to send users to login. + + +[[nsa-saml2-login-authentication-success-handler-ref]] +* **authentication-success-handler-ref** +Reference to the `AuthenticationSuccessHandler`. + + +[[nsa-saml2-login-authentication-failure-handler-ref]] +* **authentication-failure-handler-ref** +Reference to the `AuthenticationFailureHandler`. + + +[[nsa-saml2-login-authentication-manager-ref]] +* **authentication-manager-ref** +Reference to the `AuthenticationManager`. + [[nsa-attribute-exchange]] == diff --git a/etc/nohttp/allowlist.lines b/etc/nohttp/allowlist.lines index a9898c86daa..6d24f3a623b 100644 --- a/etc/nohttp/allowlist.lines +++ b/etc/nohttp/allowlist.lines @@ -5,3 +5,6 @@ ^http://lists.webappsec.org/.* ^http://webblaze.cs.berkeley.edu/.* ^http://www.w3.org/2000/09/xmldsig.* +^http://www.w3.org/2001/10/xml-exc-c14n +^http://www.w3.org/2001/04/xmldsig-more +^http://www.w3.org/2001/04/xmlenc \ No newline at end of file diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java index b62e8df79fa..ad6a9c9163b 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java @@ -111,7 +111,9 @@ private RelyingPartyRegistration(String registrationId, String entityId, String Assert.isTrue(singleLogoutServiceLocation == null || singleLogoutServiceBinding != null, "singleLogoutServiceBinding cannot be null when singleLogoutServiceLocation is set"); Assert.notNull(providerDetails, "providerDetails cannot be null"); - Assert.notEmpty(credentials, "credentials cannot be empty"); + Assert.isTrue( + !credentials.isEmpty() || (decryptionX509Credentials.isEmpty() && signingX509Credentials.isEmpty()), + "credentials cannot be empty"); for (org.springframework.security.saml2.credentials.Saml2X509Credential c : credentials) { Assert.notNull(c, "credentials cannot contain null elements"); } From 61274fff795b98bdeb81584d0d5a69f8b1675fc7 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Thu, 17 Feb 2022 15:52:41 -0300 Subject: [PATCH 2/2] Add SAML 2.0 Single Logout XML Support Closes gh-10842 --- .../security/config/Elements.java | 2 + .../http/AuthenticationConfigBuilder.java | 41 +++ .../http/LogoutBeanDefinitionParser.java | 7 + .../http/Saml2LogoutBeanDefinitionParser.java | 236 +++++++++++++ .../Saml2LogoutBeanDefinitionParserUtils.java | 104 ++++++ .../security/config/http/SecurityFilters.java | 6 + ...artyRegistrationsBeanDefinitionParser.java | 45 ++- .../security/config/spring-security-5.7.rnc | 53 ++- .../security/config/spring-security-5.7.xsd | 117 +++++++ .../security/config/spring-security.xsl | 2 +- .../saml2/Saml2LogoutConfigurerTests.java | 2 +- .../Saml2LogoutBeanDefinitionParserTests.java | 327 ++++++++++++++++++ ...-CsrfDisabled-MockLogoutSuccessHandler.xml | 41 +++ ...DefinitionParserTests-CustomComponents.xml | 56 +++ ...ogoutBeanDefinitionParserTests-Default.xml | 35 ++ ...nitionParserTests-LogoutSuccessHandler.xml | 40 +++ .../config/saml2/logout-registrations.xml | 83 +++++ .../servlet/appendix/namespace/http.adoc | 145 ++++++++ 18 files changed, 1336 insertions(+), 6 deletions(-) create mode 100644 config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParser.java create mode 100644 config/src/main/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserUtils.java create mode 100644 config/src/test/java/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests.java create mode 100644 config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CsrfDisabled-MockLogoutSuccessHandler.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CustomComponents.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-Default.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-LogoutSuccessHandler.xml create mode 100644 config/src/test/resources/org/springframework/security/config/saml2/logout-registrations.xml 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 d57782b7f93..ac6cb1fe633 100644 --- a/config/src/main/java/org/springframework/security/config/Elements.java +++ b/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"; + } 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 be623a1dc4f..6f450f0b61d 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 @@ -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 @@ void createLogoutFilter() { 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 @@ List getFilters() { 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 65c1b3b931a..51d9462d5e0 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 @@ public BeanDefinition parse(Element element, ParserContext pc) { pc.extractSource(element)); } builder.addConstructorArgReference(successHandlerRef); + this.logoutSuccessHandler = new RuntimeBeanReference(successHandlerRef); } else { // Use the logout URL if no handler set @@ -137,4 +140,8 @@ ManagedList getLogoutHandlers() { 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 00000000000..cb3bc26355f --- /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 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 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 00000000000..96ca597889d --- /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 b8c6529e617..2967441b960 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 a35f0ba00fe..ab55ad0df82 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 @@ private static Map> getAssertingParties(Element elem 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 @@ private static RelyingPartyRegistration.Builder getBuilderFromMetadataLocationIf 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 @@ private static RelyingPartyRegistration.Builder getBuilderFromMetadataLocationIf 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 @@ private static void buildAssertingParty(Element relyingPartyElt, Map 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 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 @@ -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 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 @@ -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" diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd index fc3d3ccd3f0..b359ca7a13c 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd @@ -1046,6 +1046,15 @@ + + + Configures SAML 2.0 Single Logout support + + + + + + Adds support for X.509 client authentication. @@ -2075,6 +2084,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 @@ -2137,6 +2203,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> + + + @@ -2240,6 +2330,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> + + + @@ -3605,7 +3719,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 5642c8301d9..77ee9251486 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 051327548ae..2d00a18a767 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 00000000000..2473c80d51a --- /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 00000000000..7caa59eb922 --- /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 00000000000..66068ed58d6 --- /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 00000000000..79ad59b7a6f --- /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 00000000000..9c4b1c6497a --- /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 00000000000..c96d06ac7cc --- /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 12e71362f92..6b47cb75f23 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc @@ -165,6 +165,7 @@ The default value is true. * <> * <> * <> +* <> * <> * <> @@ -1342,11 +1343,78 @@ 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** A reference to the associated asserting party. Must reference an `` element. +[[nsa-relying-party-registration-children]] +=== Child Elements of + +* <> +* <> + + +[[nsa-decryption-credential]] +== +The decryption credentials associated with the relying party. + + +[[nsa-decryption-credential-parents]] +=== Parent Elements of + +* <> + + +[[nsa-decryption-credential-attributes]] +=== 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]] +== +The signing credentials associated with the relying party. + + +[[nsa-signing-credential-parents]] +=== Parent Elements of + +* <> + + +[[nsa-signing-credential-attributes]] +=== 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]] == @@ -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 <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 @@ -1795,6 +1879,67 @@ 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-attribute-exchange]] == The `attribute-exchange` element defines the list of attributes which should be requested from the identity provider.