From d0d0a8d958f5e2b250c84319cf902556f4430a68 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 7 Dec 2020 12:28:19 -0700 Subject: [PATCH] Add OpenSAML 4 Support Closes gh-9095 --- config/spring-security-config.gradle | 10 +- .../saml2/Saml2LoginConfigurer.java | 20 +- .../saml2/Saml2LoginConfigurerTests.java | 14 +- .../core/saml2-service-provider-core.gradle | 53 ++ .../security/saml2/Saml2Exception.java | 0 .../core/OpenSamlInitializationService.java | 0 .../security/saml2/core/Saml2Error.java | 0 .../security/saml2/core/Saml2ErrorCodes.java | 0 .../core/Saml2ResponseValidatorResult.java | 0 .../saml2/core/Saml2X509Credential.java | 0 .../SpringSecurityAuthnRequestBuilder.java | 80 ++ .../credentials/Saml2X509Credential.java | 0 .../AbstractSaml2AuthenticationRequest.java | 0 .../DefaultSaml2AuthenticatedPrincipal.java | 0 .../OpenSamlDecryptionUtils.java | 113 +++ .../authentication/OpenSamlSigningUtils.java | 173 ++++ .../OpenSamlVerificationUtils.java | 217 +++++ .../Saml2AuthenticatedPrincipal.java | 0 .../authentication/Saml2Authentication.java | 0 .../Saml2AuthenticationException.java | 0 .../Saml2AuthenticationRequest.java | 0 .../Saml2AuthenticationRequestContext.java | 0 .../Saml2AuthenticationRequestFactory.java | 0 .../Saml2AuthenticationToken.java | 0 .../service/authentication/Saml2Error.java | 0 .../authentication/Saml2ErrorCodes.java | 0 .../Saml2PostAuthenticationRequest.java | 0 .../Saml2RedirectAuthenticationRequest.java | 0 .../service/authentication/Saml2Utils.java | 0 .../metadata/OpenSamlMetadataResolver.java | 0 .../metadata/Saml2MetadataResolver.java | 0 ...oryRelyingPartyRegistrationRepository.java | 0 ...enSamlAssertingPartyMetadataConverter.java | 0 ...gistrationBuilderHttpMessageConverter.java | 0 .../RelyingPartyRegistration.java | 0 .../RelyingPartyRegistrationRepository.java | 0 .../RelyingPartyRegistrations.java | 0 .../registration/Saml2MessageBinding.java | 1 + .../Saml2WebSsoAuthenticationFilter.java | 0 ...aml2WebSsoAuthenticationRequestFilter.java | 20 +- ...faultRelyingPartyRegistrationResolver.java | 0 ...2AuthenticationRequestContextResolver.java | 0 ...2AuthenticationRequestContextResolver.java | 0 .../Saml2AuthenticationTokenConverter.java | 0 .../service/web/Saml2MetadataFilter.java | 0 .../OpenSamlInitializationServiceTests.java | 0 .../Saml2ResponseValidatorResultTests.java | 0 .../security/saml2/core/Saml2Utils.java | 0 .../saml2/core/Saml2X509CredentialTests.java | 0 .../saml2/core/TestSaml2X509Credentials.java | 0 .../credentials/Saml2X509CredentialTests.java | 0 .../credentials/TestSaml2X509Credentials.java | 0 ...faultSaml2AuthenticatedPrincipalTests.java | 0 ...aml2AuthenticationRequestFactoryTests.java | 0 .../authentication/TestOpenSamlObjects.java | 21 +- ...estSaml2AuthenticationRequestContexts.java | 0 .../OpenSamlMetadataResolverTests.java | 0 ...lAssertingPartyMetadataConverterTests.java | 0 ...ationBuilderHttpMessageConverterTests.java | 0 .../RelyingPartyRegistrationTests.java | 0 .../RelyingPartyRegistrationsTests.java | 0 .../TestRelyingPartyRegistrations.java | 0 .../Saml2WebSsoAuthenticationFilterTests.java | 0 ...ebSsoAuthenticationRequestFilterTests.java | 141 ++-- ...RelyingPartyRegistrationResolverTests.java | 0 ...enticationRequestContextResolverTests.java | 0 ...aml2AuthenticationTokenConverterTests.java | 0 .../service/web/Saml2MetadataFilterTests.java | 0 .../src/test/resources/logback-test.xml | 0 .../saml2-response-sso-circle.encoded | 0 .../src/test/resources/test-metadata.xml | 0 .../saml2-service-provider-opensaml3.gradle | 53 ++ .../OpenSamlAuthenticationProvider.java | 162 +--- .../OpenSamlAuthenticationRequestFactory.java | 203 +++++ .../OpenSamlAuthenticationProviderTests.java | 141 ++-- ...SamlAuthenticationRequestFactoryTests.java | 13 +- .../saml2-service-provider-opensaml4.gradle | 58 ++ .../OpenSaml4AuthenticationProvider.java | 770 ++++++++++++++++++ ...OpenSaml4AuthenticationRequestFactory.java | 180 ++++ .../OpenSaml4AuthenticationProviderTests.java | 661 +++++++++++++++ ...aml4AuthenticationRequestFactoryTests.java | 274 +++++++ ...ing-security-saml2-service-provider.gradle | 50 +- .../OpenSamlAuthenticationRequestFactory.java | 311 ------- ...ng-security-samples-boot-saml2login.gradle | 8 +- ...urity-samples-javaconfig-saml2login.gradle | 6 +- 85 files changed, 3138 insertions(+), 615 deletions(-) create mode 100644 saml2/saml2-service-provider/core/saml2-service-provider-core.gradle rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/Saml2Exception.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/core/OpenSamlInitializationService.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/core/Saml2Error.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/core/Saml2ErrorCodes.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/core/Saml2ResponseValidatorResult.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/core/Saml2X509Credential.java (100%) create mode 100644 saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/core/SpringSecurityAuthnRequestBuilder.java rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/credentials/Saml2X509Credential.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipal.java (100%) create mode 100644 saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlDecryptionUtils.java create mode 100644 saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlSigningUtils.java create mode 100644 saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlVerificationUtils.java rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationException.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequest.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestContext.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationToken.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Error.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ErrorCodes.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequest.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequest.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2MetadataResolver.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepository.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverter.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationRepository.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrations.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/registration/Saml2MessageBinding.java (99%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java (93%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultRelyingPartyRegistrationResolver.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolver.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationRequestContextResolver.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverter.java (100%) rename saml2/saml2-service-provider/{ => core}/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilter.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/core/OpenSamlInitializationServiceTests.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/core/Saml2ResponseValidatorResultTests.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/core/Saml2Utils.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/core/Saml2X509CredentialTests.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/core/TestSaml2X509Credentials.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/credentials/Saml2X509CredentialTests.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/credentials/TestSaml2X509Credentials.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipalTests.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactoryTests.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java (92%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2AuthenticationRequestContexts.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverterTests.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverterTests.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationsTests.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/provider/service/registration/TestRelyingPartyRegistrations.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilterTests.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java (58%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/provider/service/web/DefaultRelyingPartyRegistrationResolverTests.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolverTests.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverterTests.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilterTests.java (100%) rename saml2/saml2-service-provider/{ => core}/src/test/resources/logback-test.xml (100%) rename saml2/saml2-service-provider/{ => core}/src/test/resources/saml2-response-sso-circle.encoded (100%) rename saml2/saml2-service-provider/{ => core}/src/test/resources/test-metadata.xml (100%) create mode 100644 saml2/saml2-service-provider/opensaml3/saml2-service-provider-opensaml3.gradle rename saml2/saml2-service-provider/{ => opensaml3}/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java (82%) create mode 100644 saml2/saml2-service-provider/opensaml3/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java rename saml2/saml2-service-provider/{ => opensaml3}/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java (88%) rename saml2/saml2-service-provider/{ => opensaml3}/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactoryTests.java (98%) create mode 100644 saml2/saml2-service-provider/opensaml4/saml2-service-provider-opensaml4.gradle create mode 100644 saml2/saml2-service-provider/opensaml4/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java create mode 100644 saml2/saml2-service-provider/opensaml4/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactory.java create mode 100644 saml2/saml2-service-provider/opensaml4/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java create mode 100644 saml2/saml2-service-provider/opensaml4/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactoryTests.java delete mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 67024437f6..03cfeddff8 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -4,6 +4,10 @@ apply plugin: 'io.spring.convention.spring-module' apply plugin: 'trang' apply plugin: 'kotlin' +repositories { + maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" } +} + dependencies { // NB: Don't add other compile time dependencies to the config module as this breaks tooling compile project(':spring-security-core') @@ -14,7 +18,8 @@ dependencies { optional project(':spring-security-ldap') optional project(':spring-security-messaging') - optional project(':spring-security-saml2-service-provider') + optional project(':saml2-service-provider-opensaml3') + optional project(':saml2-service-provider-opensaml4') optional project(':spring-security-oauth2-client') optional project(':spring-security-oauth2-jose') optional project(':spring-security-oauth2-resource-server') @@ -42,7 +47,8 @@ dependencies { testCompile project(path : ':spring-security-ldap', configuration : 'tests') testCompile project(path : ':spring-security-oauth2-client', configuration : 'tests') testCompile project(path : ':spring-security-oauth2-resource-server', configuration : 'tests') - testCompile project(path : ':spring-security-saml2-service-provider', configuration : 'tests') + testCompile project(path : ':saml2-service-provider-core', configuration : 'tests') + testCompile project(path : ':saml2-service-provider-opensaml4', configuration : 'tests') testCompile project(path : ':spring-security-web', configuration : 'tests') testCompile apachedsDependencies testCompile powerMock2Dependencies diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java index 4adad193d6..196878c578 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java @@ -21,15 +21,20 @@ import java.util.Map; import javax.servlet.Filter; +import org.opensaml.core.Version; + import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; import org.springframework.security.core.Authentication; +import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; +import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationRequestFactory; import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider; import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestFactory; @@ -190,7 +195,7 @@ public final class Saml2LoginConfigurer> *
  • The {@code loginProcessingUrl} is set
  • *
  • A custom login page is configured, or
  • *
  • A default login page with all SAML 2.0 Identity Providers is configured
  • - *
  • An {@link OpenSamlAuthenticationProvider} is configured
  • + *
  • An {@link AuthenticationProvider} is configured
  • * */ @Override @@ -256,8 +261,12 @@ public final class Saml2LoginConfigurer> } private void registerDefaultAuthenticationProvider(B http) { - OpenSamlAuthenticationProvider provider = postProcess(new OpenSamlAuthenticationProvider()); - http.authenticationProvider(provider); + if (Version.getVersion().startsWith("4")) { + http.authenticationProvider(postProcess(new OpenSaml4AuthenticationProvider())); + } + else { + http.authenticationProvider(postProcess(new OpenSamlAuthenticationProvider())); + } } private void registerDefaultCsrfOverride(B http) { @@ -337,7 +346,10 @@ public final class Saml2LoginConfigurer> private Saml2AuthenticationRequestFactory getResolver(B http) { Saml2AuthenticationRequestFactory resolver = getSharedOrBean(http, Saml2AuthenticationRequestFactory.class); if (resolver == null) { - resolver = new OpenSamlAuthenticationRequestFactory(); + if (Version.getVersion().startsWith("4")) { + return new OpenSaml4AuthenticationRequestFactory(); + } + return new OpenSamlAuthenticationRequestFactory(); } return resolver; } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java index 4d79181b3f..a04c2bbb22 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java @@ -19,6 +19,7 @@ package org.springframework.security.config.annotation.web.configurers.saml2; import java.io.IOException; import java.net.URLDecoder; import java.time.Duration; +import java.time.Instant; import java.util.Base64; import java.util.Collection; import java.util.Collections; @@ -61,8 +62,9 @@ import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMap import org.springframework.security.saml2.core.Saml2ErrorCodes; import org.springframework.security.saml2.core.Saml2Utils; import org.springframework.security.saml2.core.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; +import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationRequestFactory; import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider; -import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory; import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestContext; @@ -235,11 +237,8 @@ public class Saml2LoginConfigurerTests { "authenticationManager"); ProviderManager pm = (ProviderManager) manager; AuthenticationProvider provider = pm.getProviders().stream() - .filter((p) -> p instanceof OpenSamlAuthenticationProvider).findFirst().get(); - Assert.assertSame(AUTHORITIES_EXTRACTOR, ReflectionTestUtils.getField(provider, "authoritiesExtractor")); - Assert.assertSame(AUTHORITIES_MAPPER, ReflectionTestUtils.getField(provider, "authoritiesMapper")); - Assert.assertSame(RESPONSE_TIME_VALIDATION_SKEW, - ReflectionTestUtils.getField(provider, "responseTimeValidationSkew")); + .filter((p) -> p instanceof OpenSaml4AuthenticationProvider).findFirst().get(); + assertThat(provider).isNotNull(); } private Saml2WebSsoAuthenticationFilter getSaml2SsoFilter(FilterChainProxy chain) { @@ -370,9 +369,10 @@ public class Saml2LoginConfigurerTests { @Bean Saml2AuthenticationRequestFactory authenticationRequestFactory() { - OpenSamlAuthenticationRequestFactory authenticationRequestFactory = new OpenSamlAuthenticationRequestFactory(); + OpenSaml4AuthenticationRequestFactory authenticationRequestFactory = new OpenSaml4AuthenticationRequestFactory(); authenticationRequestFactory.setAuthenticationRequestContextConverter((context) -> { AuthnRequest authnRequest = TestOpenSamlObjects.authnRequest(); + authnRequest.setIssueInstant(Instant.now()); authnRequest.setForceAuthn(true); return authnRequest; }); diff --git a/saml2/saml2-service-provider/core/saml2-service-provider-core.gradle b/saml2/saml2-service-provider/core/saml2-service-provider-core.gradle new file mode 100644 index 0000000000..6a64caa2fc --- /dev/null +++ b/saml2/saml2-service-provider/core/saml2-service-provider-core.gradle @@ -0,0 +1,53 @@ +buildscript { + repositories { + maven { url 'https://repo.spring.io/plugins-release' } + } + dependencies { + classpath 'io.spring.gradle:propdeps-plugin:0.0.10.RELEASE' + } +} + +plugins { + id 'java-library' + id 'io.spring.convention.repository' + id 'io.spring.convention.springdependencymangement' + id 'io.spring.convention.dependency-set' + id 'io.spring.convention.checkstyle' + id 'io.spring.convention.tests-configuration' + id 'io.spring.convention.integration-test' + id 'propdeps' +} + +configurations { + classesOnlyElements { + canBeConsumed = true + canBeResolved = false + } +} + +artifacts { + classesOnlyElements(compileJava.destinationDir) +} + +repositories { + maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" } +} + +dependencies { + constraints { + management("org.opensaml:opensaml-core:3.+") + management("org.opensaml:opensaml-saml-api:3.+") + management("org.opensaml:opensaml-saml-impl:3.+") + } + + compile project(':spring-security-core') + compile project(':spring-security-web') + + provided("org.opensaml:opensaml-core") + provided("org.opensaml:opensaml-saml-api") + provided("org.opensaml:opensaml-saml-impl") + + provided 'javax.servlet:javax.servlet-api' + + testCompile 'com.squareup.okhttp3:mockwebserver' +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/Saml2Exception.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/Saml2Exception.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/Saml2Exception.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/Saml2Exception.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/OpenSamlInitializationService.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/core/OpenSamlInitializationService.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/OpenSamlInitializationService.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/core/OpenSamlInitializationService.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/Saml2Error.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/core/Saml2Error.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/Saml2Error.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/core/Saml2Error.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/Saml2ErrorCodes.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/core/Saml2ErrorCodes.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/Saml2ErrorCodes.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/core/Saml2ErrorCodes.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/Saml2ResponseValidatorResult.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/core/Saml2ResponseValidatorResult.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/Saml2ResponseValidatorResult.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/core/Saml2ResponseValidatorResult.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/Saml2X509Credential.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/core/Saml2X509Credential.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/Saml2X509Credential.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/core/Saml2X509Credential.java diff --git a/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/core/SpringSecurityAuthnRequestBuilder.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/core/SpringSecurityAuthnRequestBuilder.java new file mode 100644 index 0000000000..c8cd19399f --- /dev/null +++ b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/core/SpringSecurityAuthnRequestBuilder.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2020 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.saml2.core; + +import java.time.Clock; +import java.time.Instant; +import java.util.UUID; + +import org.joda.time.DateTime; +import org.opensaml.core.xml.XMLObjectBuilder; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.saml2.core.AuthnRequest; +import org.opensaml.saml.saml2.core.impl.AuthnRequestBuilder; + +/** + * A {@link AuthnRequestBuilder} that gives each {@link AuthnRequest} some reasonable + * defaults. + * + * @author Josh Cummings + * @since 5.5 + */ +public final class SpringSecurityAuthnRequestBuilder extends AuthnRequestBuilder { + + private final XMLObjectBuilder builder; + + private Clock clock = Clock.systemUTC(); + + SpringSecurityAuthnRequestBuilder(XMLObjectBuilder builder) { + this.builder = builder; + } + + /** {@inheritDoc} */ + @Override + public AuthnRequest buildObject(final String namespaceURI, final String localName, final String namespacePrefix) { + AuthnRequest authnRequest = this.builder.buildObject(namespaceURI, localName, namespacePrefix); + setDefaults(authnRequest); + return authnRequest; + } + + /** + * Use this {@link Clock} with {@link Instant#now()} for generating timestamps + * @param clock + */ + public void setClock(Clock clock) { + this.clock = clock; + } + + private void setDefaults(AuthnRequest authnRequest) { + if (authnRequest.getID() == null) { + authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1)); + } + if (authnRequest.getIssueInstant() == null) { + authnRequest.setIssueInstant(new DateTime(this.clock.millis())); + } + if (authnRequest.isForceAuthn() == null) { + authnRequest.setForceAuthn(Boolean.FALSE); + } + if (authnRequest.isPassive() == null) { + authnRequest.setIsPassive(Boolean.FALSE); + } + if (authnRequest.getProtocolBinding() == null) { + authnRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI); + } + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/credentials/Saml2X509Credential.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/credentials/Saml2X509Credential.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/credentials/Saml2X509Credential.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/credentials/Saml2X509Credential.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipal.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipal.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipal.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipal.java diff --git a/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlDecryptionUtils.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlDecryptionUtils.java new file mode 100644 index 0000000000..e80bd0619b --- /dev/null +++ b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlDecryptionUtils.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.authentication; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +import org.opensaml.saml.saml2.core.Assertion; +import org.opensaml.saml.saml2.core.Attribute; +import org.opensaml.saml.saml2.core.AttributeStatement; +import org.opensaml.saml.saml2.core.EncryptedAssertion; +import org.opensaml.saml.saml2.core.EncryptedAttribute; +import org.opensaml.saml.saml2.core.NameID; +import org.opensaml.saml.saml2.core.Response; +import org.opensaml.saml.saml2.encryption.Decrypter; +import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver; +import org.opensaml.security.credential.Credential; +import org.opensaml.security.credential.CredentialSupport; +import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver; +import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver; +import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver; + +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; + +/** + * Utility methods for decrypting SAML components with OpenSAML + * + * For internal use only. + * + * @author Josh Cummings + */ +final class OpenSamlDecryptionUtils { + + private static final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver( + Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(), + new SimpleRetrievalMethodEncryptedKeyResolver())); + + static void decryptResponseElements(Response response, RelyingPartyRegistration registration) { + Decrypter decrypter = decrypter(registration); + for (EncryptedAssertion encryptedAssertion : response.getEncryptedAssertions()) { + try { + Assertion assertion = decrypter.decrypt(encryptedAssertion); + response.getAssertions().add(assertion); + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + } + + static void decryptAssertionElements(Assertion assertion, RelyingPartyRegistration registration) { + Decrypter decrypter = decrypter(registration); + for (AttributeStatement statement : assertion.getAttributeStatements()) { + for (EncryptedAttribute encryptedAttribute : statement.getEncryptedAttributes()) { + try { + Attribute attribute = decrypter.decrypt(encryptedAttribute); + statement.getAttributes().add(attribute); + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + } + if (assertion.getSubject() == null) { + return; + } + if (assertion.getSubject().getEncryptedID() == null) { + return; + } + try { + assertion.getSubject().setNameID((NameID) decrypter.decrypt(assertion.getSubject().getEncryptedID())); + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + + private static Decrypter decrypter(RelyingPartyRegistration registration) { + Collection credentials = new ArrayList<>(); + for (Saml2X509Credential key : registration.getDecryptionX509Credentials()) { + Credential cred = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey()); + credentials.add(cred); + } + KeyInfoCredentialResolver resolver = new CollectionKeyInfoCredentialResolver(credentials); + Decrypter decrypter = new Decrypter(null, resolver, encryptedKeyResolver); + decrypter.setRootInNewDocument(true); + return decrypter; + } + + private OpenSamlDecryptionUtils() { + } + +} diff --git a/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlSigningUtils.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlSigningUtils.java new file mode 100644 index 0000000000..f177a866b4 --- /dev/null +++ b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlSigningUtils.java @@ -0,0 +1,173 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.authentication; + +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; +import net.shibboleth.utilities.java.support.xml.SerializeSupport; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.core.xml.io.Marshaller; +import org.opensaml.core.xml.io.MarshallingException; +import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver; +import org.opensaml.security.SecurityException; +import org.opensaml.security.credential.BasicCredential; +import org.opensaml.security.credential.Credential; +import org.opensaml.security.credential.CredentialSupport; +import org.opensaml.security.credential.UsageType; +import org.opensaml.xmlsec.SignatureSigningParameters; +import org.opensaml.xmlsec.SignatureSigningParametersResolver; +import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion; +import org.opensaml.xmlsec.crypto.XMLSigningUtil; +import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration; +import org.opensaml.xmlsec.signature.SignableXMLObject; +import org.opensaml.xmlsec.signature.support.SignatureConstants; +import org.opensaml.xmlsec.signature.support.SignatureSupport; +import org.w3c.dom.Element; + +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.util.Assert; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; + +/** + * Utility methods for signing SAML components with OpenSAML + * + * For internal use only. + * + * @author Josh Cummings + */ +final class OpenSamlSigningUtils { + + static String serialize(XMLObject object) { + try { + Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object); + Element element = marshaller.marshall(object); + return SerializeSupport.nodeToString(element); + } + catch (MarshallingException ex) { + throw new Saml2Exception(ex); + } + } + + static O sign(O object, RelyingPartyRegistration relyingPartyRegistration) { + SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration); + try { + SignatureSupport.signObject(object, parameters); + return object; + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + + static QueryParametersPartial sign(RelyingPartyRegistration registration) { + return new QueryParametersPartial(registration); + } + + private static SignatureSigningParameters resolveSigningParameters( + RelyingPartyRegistration relyingPartyRegistration) { + List credentials = resolveSigningCredentials(relyingPartyRegistration); + List algorithms = relyingPartyRegistration.getAssertingPartyDetails().getSigningAlgorithms(); + List digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256); + String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS; + SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver(); + CriteriaSet criteria = new CriteriaSet(); + BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration(); + signingConfiguration.setSigningCredentials(credentials); + signingConfiguration.setSignatureAlgorithms(algorithms); + signingConfiguration.setSignatureReferenceDigestMethods(digests); + signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization); + criteria.add(new SignatureSigningConfigurationCriterion(signingConfiguration)); + try { + SignatureSigningParameters parameters = resolver.resolveSingle(criteria); + Assert.notNull(parameters, "Failed to resolve any signing credential"); + return parameters; + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + + private static List resolveSigningCredentials(RelyingPartyRegistration relyingPartyRegistration) { + List credentials = new ArrayList<>(); + for (Saml2X509Credential x509Credential : relyingPartyRegistration.getSigningX509Credentials()) { + X509Certificate certificate = x509Credential.getCertificate(); + PrivateKey privateKey = x509Credential.getPrivateKey(); + BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey); + credential.setEntityId(relyingPartyRegistration.getEntityId()); + credential.setUsageType(UsageType.SIGNING); + credentials.add(credential); + } + return credentials; + } + + static class QueryParametersPartial { + + final RelyingPartyRegistration registration; + + final Map components = new LinkedHashMap<>(); + + QueryParametersPartial(RelyingPartyRegistration registration) { + this.registration = registration; + } + + QueryParametersPartial param(String key, String value) { + this.components.put(key, value); + return this; + } + + Map parameters() { + SignatureSigningParameters parameters = resolveSigningParameters(this.registration); + Credential credential = parameters.getSigningCredential(); + String algorithmUri = parameters.getSignatureAlgorithm(); + this.components.put("SigAlg", algorithmUri); + UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); + for (Map.Entry component : this.components.entrySet()) { + builder.queryParam(component.getKey(), + UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1)); + } + String queryString = builder.build(true).toString().substring(1); + try { + byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri, + queryString.getBytes(StandardCharsets.UTF_8)); + String b64Signature = Saml2Utils.samlEncode(rawSignature); + this.components.put("Signature", b64Signature); + } + catch (SecurityException ex) { + throw new Saml2Exception(ex); + } + return this.components; + } + + } + + private OpenSamlSigningUtils() { + + } + +} diff --git a/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlVerificationUtils.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlVerificationUtils.java new file mode 100644 index 0000000000..00dbfd7af4 --- /dev/null +++ b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlVerificationUtils.java @@ -0,0 +1,217 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.authentication; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; +import org.opensaml.core.criterion.EntityIdCriterion; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.criterion.ProtocolCriterion; +import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion; +import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.RequestAbstractType; +import org.opensaml.saml.saml2.core.StatusResponseType; +import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; +import org.opensaml.security.credential.Credential; +import org.opensaml.security.credential.CredentialResolver; +import org.opensaml.security.credential.UsageType; +import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion; +import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion; +import org.opensaml.security.credential.impl.CollectionCredentialResolver; +import org.opensaml.security.criteria.UsageCriterion; +import org.opensaml.security.x509.BasicX509Credential; +import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap; +import org.opensaml.xmlsec.signature.Signature; +import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; +import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine; + +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.core.Saml2ErrorCodes; +import org.springframework.security.saml2.core.Saml2ResponseValidatorResult; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.web.util.UriUtils; + +/** + * Utility methods for verifying SAML component signatures with OpenSAML + * + * For internal use only. + * + * @author Josh Cummings + */ + +final class OpenSamlVerificationUtils { + + static VerifierPartial verifySignature(StatusResponseType object, RelyingPartyRegistration registration) { + return new VerifierPartial(object, registration); + } + + static VerifierPartial verifySignature(RequestAbstractType object, RelyingPartyRegistration registration) { + return new VerifierPartial(object, registration); + } + + static SignatureTrustEngine trustEngine(RelyingPartyRegistration registration) { + Set credentials = new HashSet<>(); + Collection keys = registration.getAssertingPartyDetails().getVerificationX509Credentials(); + for (Saml2X509Credential key : keys) { + BasicX509Credential cred = new BasicX509Credential(key.getCertificate()); + cred.setUsageType(UsageType.SIGNING); + cred.setEntityId(registration.getAssertingPartyDetails().getEntityId()); + credentials.add(cred); + } + CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials); + return new ExplicitKeySignatureTrustEngine(credentialsResolver, + DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver()); + } + + static class VerifierPartial { + + private final String id; + + private final CriteriaSet criteria; + + private final SignatureTrustEngine trustEngine; + + VerifierPartial(StatusResponseType object, RelyingPartyRegistration registration) { + this.id = object.getID(); + this.criteria = verificationCriteria(object.getIssuer()); + this.trustEngine = trustEngine(registration); + } + + VerifierPartial(RequestAbstractType object, RelyingPartyRegistration registration) { + this.id = object.getID(); + this.criteria = verificationCriteria(object.getIssuer()); + this.trustEngine = trustEngine(registration); + } + + Saml2ResponseValidatorResult redirect(HttpServletRequest request, String objectParameterName) { + RedirectSignature signature = new RedirectSignature(request, objectParameterName); + if (signature.getAlgorithm() == null) { + return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Missing signature algorithm for object [" + this.id + "]")); + } + if (!signature.hasSignature()) { + return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Missing signature for object [" + this.id + "]")); + } + Collection errors = new ArrayList<>(); + String algorithmUri = signature.getAlgorithm(); + try { + if (!this.trustEngine.validate(signature.getSignature(), signature.getContent(), algorithmUri, + this.criteria, null)) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for object [" + this.id + "]")); + } + } + catch (Exception ex) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for object [" + this.id + "]: ")); + } + return Saml2ResponseValidatorResult.failure(errors); + } + + Saml2ResponseValidatorResult post(Signature signature) { + Collection errors = new ArrayList<>(); + SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator(); + try { + profileValidator.validate(signature); + } + catch (Exception ex) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for object [" + this.id + "]: ")); + } + + try { + if (!this.trustEngine.validate(signature, this.criteria)) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for object [" + this.id + "]")); + } + } + catch (Exception ex) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for object [" + this.id + "]: ")); + } + + return Saml2ResponseValidatorResult.failure(errors); + } + + private CriteriaSet verificationCriteria(Issuer issuer) { + CriteriaSet criteria = new CriteriaSet(); + criteria.add(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer.getValue()))); + criteria.add(new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS))); + criteria.add(new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING))); + return criteria; + } + + private static class RedirectSignature { + + private final HttpServletRequest request; + + private final String objectParameterName; + + RedirectSignature(HttpServletRequest request, String objectParameterName) { + this.request = request; + this.objectParameterName = objectParameterName; + } + + String getAlgorithm() { + return this.request.getParameter("SigAlg"); + } + + byte[] getContent() { + if (this.request.getParameter("RelayState") != null) { + return String.format("%s=%s&RelayState=%s&SigAlg=%s", this.objectParameterName, + UriUtils.encode(this.request.getParameter(this.objectParameterName), + StandardCharsets.ISO_8859_1), + UriUtils.encode(this.request.getParameter("RelayState"), StandardCharsets.ISO_8859_1), + UriUtils.encode(getAlgorithm(), StandardCharsets.ISO_8859_1)) + .getBytes(StandardCharsets.UTF_8); + } + else { + return String + .format("%s=%s&SigAlg=%s", this.objectParameterName, + UriUtils.encode(this.request.getParameter(this.objectParameterName), + StandardCharsets.ISO_8859_1), + UriUtils.encode(getAlgorithm(), StandardCharsets.ISO_8859_1)) + .getBytes(StandardCharsets.UTF_8); + } + } + + byte[] getSignature() { + return Saml2Utils.samlDecode(this.request.getParameter("Signature")); + } + + boolean hasSignature() { + return this.request.getParameter("Signature") != null; + } + + } + + } + + private OpenSamlVerificationUtils() { + + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationException.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationException.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationException.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationException.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequest.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequest.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequest.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequest.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestContext.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestContext.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestContext.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestContext.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactory.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationToken.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationToken.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationToken.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationToken.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Error.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Error.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Error.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Error.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ErrorCodes.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ErrorCodes.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ErrorCodes.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2ErrorCodes.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequest.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequest.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequest.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequest.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequest.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequest.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequest.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequest.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Utils.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2MetadataResolver.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2MetadataResolver.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2MetadataResolver.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2MetadataResolver.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepository.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepository.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepository.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepository.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverter.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverter.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverter.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverter.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationRepository.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationRepository.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationRepository.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationRepository.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrations.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrations.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrations.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrations.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/Saml2MessageBinding.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/registration/Saml2MessageBinding.java similarity index 99% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/Saml2MessageBinding.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/registration/Saml2MessageBinding.java index 50309e504d..015aa81d96 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/Saml2MessageBinding.java +++ b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/registration/Saml2MessageBinding.java @@ -57,4 +57,5 @@ public enum Saml2MessageBinding { } return null; } + } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java similarity index 93% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java index 731c6a3c66..026296e9e8 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java +++ b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java @@ -24,6 +24,8 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.opensaml.core.Version; + import org.springframework.http.MediaType; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestContext; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestFactory; @@ -39,6 +41,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher.MatchResult; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.HtmlUtils; @@ -88,8 +91,21 @@ public class Saml2WebSsoAuthenticationRequestFilter extends OncePerRequestFilter public Saml2WebSsoAuthenticationRequestFilter( RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) { this(new DefaultSaml2AuthenticationRequestContextResolver( - new DefaultRelyingPartyRegistrationResolver(relyingPartyRegistrationRepository)), - new org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory()); + new DefaultRelyingPartyRegistrationResolver(relyingPartyRegistrationRepository)), requestFactory()); + } + + private static Saml2AuthenticationRequestFactory requestFactory() { + String opensamlClassName = "org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory"; + if (Version.getVersion().startsWith("4")) { + opensamlClassName = "org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationRequestFactory"; + } + try { + return (Saml2AuthenticationRequestFactory) ClassUtils.forName(opensamlClassName, null) + .getDeclaredConstructor().newInstance(); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } } /** diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultRelyingPartyRegistrationResolver.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultRelyingPartyRegistrationResolver.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultRelyingPartyRegistrationResolver.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultRelyingPartyRegistrationResolver.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolver.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolver.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolver.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolver.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationRequestContextResolver.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationRequestContextResolver.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationRequestContextResolver.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationRequestContextResolver.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverter.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverter.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverter.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverter.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilter.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilter.java similarity index 100% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilter.java rename to saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilter.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/OpenSamlInitializationServiceTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/core/OpenSamlInitializationServiceTests.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/OpenSamlInitializationServiceTests.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/core/OpenSamlInitializationServiceTests.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/Saml2ResponseValidatorResultTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/core/Saml2ResponseValidatorResultTests.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/Saml2ResponseValidatorResultTests.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/core/Saml2ResponseValidatorResultTests.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/Saml2Utils.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/core/Saml2Utils.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/Saml2Utils.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/core/Saml2Utils.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/Saml2X509CredentialTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/core/Saml2X509CredentialTests.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/Saml2X509CredentialTests.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/core/Saml2X509CredentialTests.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/TestSaml2X509Credentials.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/core/TestSaml2X509Credentials.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/core/TestSaml2X509Credentials.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/core/TestSaml2X509Credentials.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/credentials/Saml2X509CredentialTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/credentials/Saml2X509CredentialTests.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/credentials/Saml2X509CredentialTests.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/credentials/Saml2X509CredentialTests.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/credentials/TestSaml2X509Credentials.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/credentials/TestSaml2X509Credentials.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/credentials/TestSaml2X509Credentials.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/credentials/TestSaml2X509Credentials.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipalTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipalTests.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipalTests.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/authentication/DefaultSaml2AuthenticatedPrincipalTests.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactoryTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactoryTests.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactoryTests.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactoryTests.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java similarity index 92% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java index 40e1d8bfe1..d7a1fcdc34 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java +++ b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java @@ -28,21 +28,17 @@ import javax.crypto.spec.SecretKeySpec; import javax.xml.namespace.QName; import org.apache.xml.security.encryption.XMLCipherParameters; -import org.joda.time.DateTime; -import org.joda.time.Duration; import org.opensaml.core.xml.XMLObject; import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; import org.opensaml.core.xml.io.MarshallingException; import org.opensaml.core.xml.schema.XSAny; import org.opensaml.core.xml.schema.XSBoolean; import org.opensaml.core.xml.schema.XSBooleanValue; -import org.opensaml.core.xml.schema.XSDateTime; import org.opensaml.core.xml.schema.XSInteger; import org.opensaml.core.xml.schema.XSString; import org.opensaml.core.xml.schema.XSURI; import org.opensaml.core.xml.schema.impl.XSAnyBuilder; import org.opensaml.core.xml.schema.impl.XSBooleanBuilder; -import org.opensaml.core.xml.schema.impl.XSDateTimeBuilder; import org.opensaml.core.xml.schema.impl.XSIntegerBuilder; import org.opensaml.core.xml.schema.impl.XSStringBuilder; import org.opensaml.core.xml.schema.impl.XSURIBuilder; @@ -114,7 +110,6 @@ public final class TestOpenSamlObjects { static Response response(String destination, String issuerEntityId) { Response response = build(Response.DEFAULT_ELEMENT_NAME); response.setID("R" + UUID.randomUUID().toString()); - response.setIssueInstant(DateTime.now()); response.setVersion(SAMLVersion.VERSION_20); response.setID("_" + UUID.randomUUID().toString()); response.setDestination(destination); @@ -141,9 +136,7 @@ public final class TestOpenSamlObjects { static Assertion assertion(String username, String issuerEntityId, String recipientEntityId, String recipientUri) { Assertion assertion = build(Assertion.DEFAULT_ELEMENT_NAME); assertion.setID("A" + UUID.randomUUID().toString()); - assertion.setIssueInstant(DateTime.now()); assertion.setVersion(SAMLVersion.VERSION_20); - assertion.setIssueInstant(DateTime.now()); assertion.setIssuer(issuer(issuerEntityId)); assertion.setSubject(subject(username)); assertion.setConditions(conditions()); @@ -183,16 +176,11 @@ public final class TestOpenSamlObjects { static SubjectConfirmationData subjectConfirmationData(String recipient) { SubjectConfirmationData subject = build(SubjectConfirmationData.DEFAULT_ELEMENT_NAME); subject.setRecipient(recipient); - subject.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000))); - subject.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000))); return subject; } static Conditions conditions() { - Conditions conditions = build(Conditions.DEFAULT_ELEMENT_NAME); - conditions.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000))); - conditions.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000))); - return conditions; + return build(Conditions.DEFAULT_ELEMENT_NAME); } public static AuthnRequest authnRequest() { @@ -338,13 +326,6 @@ public final class TestOpenSamlObjects { registered.setValue(new XSBooleanValue(true, false)); registeredAttr.getAttributeValues().add(registered); attrStmt2.getAttributes().add(registeredAttr); - Attribute registeredDateAttr = attributeBuilder.buildObject(); - registeredDateAttr.setName("registeredDate"); - XSDateTime registeredDate = new XSDateTimeBuilder().buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, - XSDateTime.TYPE_NAME); - registeredDate.setValue(DateTime.parse("1970-01-01T00:00:00Z")); - registeredDateAttr.getAttributeValues().add(registeredDate); - attrStmt2.getAttributes().add(registeredDateAttr); attributeStatements.add(attrStmt2); return attributeStatements; } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2AuthenticationRequestContexts.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2AuthenticationRequestContexts.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2AuthenticationRequestContexts.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2AuthenticationRequestContexts.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverterTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverterTests.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverterTests.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverterTests.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverterTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverterTests.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverterTests.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverterTests.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationsTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationsTests.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationsTests.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationsTests.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/TestRelyingPartyRegistrations.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/registration/TestRelyingPartyRegistrations.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/TestRelyingPartyRegistrations.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/registration/TestRelyingPartyRegistrations.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilterTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilterTests.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilterTests.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilterTests.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java similarity index 58% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java index 6079de5bcb..3f0bf6cf84 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java +++ b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java @@ -28,8 +28,10 @@ import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.saml2.credentials.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestContext; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestFactory; import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; +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; @@ -68,7 +70,7 @@ public class Saml2WebSsoAuthenticationRequestFilterTests { @Before public void setup() { - this.filter = new Saml2WebSsoAuthenticationRequestFilter(this.repository); + this.filter = new Saml2WebSsoAuthenticationRequestFilter(this.resolver, this.factory); this.request = new MockHttpServletRequest(); this.response = new MockHttpServletResponse(); this.request.setPathInfo("/saml2/authenticate/registration-id"); @@ -81,25 +83,48 @@ public class Saml2WebSsoAuthenticationRequestFilterTests { @Test public void doFilterWhenNoRelayStateThenRedirectDoesNotContainParameter() throws ServletException, IOException { - given(this.repository.findByRegistrationId("registration-id")).willReturn(this.rpBuilder.build()); + Saml2AuthenticationRequestContext context = authenticationRequestContext().relayState(null).build(); + Saml2RedirectAuthenticationRequest request = redirectAuthenticationRequest(context).build(); + given(this.resolver.resolve(any())).willReturn(context); + given(this.factory.createRedirectAuthenticationRequest(any())).willReturn(request); this.filter.doFilterInternal(this.request, this.response, this.filterChain); assertThat(this.response.getHeader("Location")).doesNotContain("RelayState=").startsWith(IDP_SSO_URL); } + private static Saml2AuthenticationRequestContext.Builder authenticationRequestContext() { + return TestSaml2AuthenticationRequestContexts.authenticationRequestContext(); + } + + private static Saml2RedirectAuthenticationRequest.Builder redirectAuthenticationRequest( + Saml2AuthenticationRequestContext context) { + return Saml2RedirectAuthenticationRequest.withAuthenticationRequestContext(context).samlRequest("request") + .authenticationRequestUri(IDP_SSO_URL); + } + + private static Saml2PostAuthenticationRequest.Builder postAuthenticationRequest( + Saml2AuthenticationRequestContext context) { + return Saml2PostAuthenticationRequest.withAuthenticationRequestContext(context).samlRequest("request") + .authenticationRequestUri(IDP_SSO_URL); + } + @Test public void doFilterWhenRelayStateThenRedirectDoesContainParameter() throws ServletException, IOException { - given(this.repository.findByRegistrationId("registration-id")).willReturn(this.rpBuilder.build()); - this.request.setParameter("RelayState", "my-relay-state"); + Saml2AuthenticationRequestContext context = authenticationRequestContext().build(); + Saml2RedirectAuthenticationRequest request = redirectAuthenticationRequest(context).build(); + given(this.resolver.resolve(any())).willReturn(context); + given(this.factory.createRedirectAuthenticationRequest(any())).willReturn(request); this.filter.doFilterInternal(this.request, this.response, this.filterChain); - assertThat(this.response.getHeader("Location")).contains("RelayState=my-relay-state").startsWith(IDP_SSO_URL); + assertThat(this.response.getHeader("Location")).contains("RelayState=relayState").startsWith(IDP_SSO_URL); } @Test public void doFilterWhenRelayStateThatRequiresEncodingThenRedirectDoesContainsEncodedParameter() throws Exception { - given(this.repository.findByRegistrationId("registration-id")).willReturn(this.rpBuilder.build()); - final String relayStateValue = "https://my-relay-state.example.com?with=param&other=param"; - final String relayStateEncoded = UriUtils.encode(relayStateValue, StandardCharsets.ISO_8859_1); - this.request.setParameter("RelayState", relayStateValue); + String relayStateValue = "https://my-relay-state.example.com?with=param&other=param"; + String relayStateEncoded = UriUtils.encode(relayStateValue, StandardCharsets.ISO_8859_1); + Saml2AuthenticationRequestContext context = authenticationRequestContext().relayState(relayStateValue).build(); + Saml2RedirectAuthenticationRequest request = redirectAuthenticationRequest(context).build(); + given(this.resolver.resolve(any())).willReturn(context); + given(this.factory.createRedirectAuthenticationRequest(any())).willReturn(request); this.filter.doFilterInternal(this.request, this.response, this.filterChain); assertThat(this.response.getHeader("Location")).contains("RelayState=" + relayStateEncoded) .startsWith(IDP_SSO_URL); @@ -107,34 +132,39 @@ public class Saml2WebSsoAuthenticationRequestFilterTests { @Test public void doFilterWhenSimpleSignatureSpecifiedThenSignatureParametersAreInTheRedirectURL() throws Exception { - given(this.repository.findByRegistrationId("registration-id")).willReturn(this.rpBuilder.build()); - final String relayStateValue = "https://my-relay-state.example.com?with=param&other=param"; - final String relayStateEncoded = UriUtils.encode(relayStateValue, StandardCharsets.ISO_8859_1); - this.request.setParameter("RelayState", relayStateValue); + Saml2AuthenticationRequestContext context = authenticationRequestContext().build(); + Saml2RedirectAuthenticationRequest request = redirectAuthenticationRequest(context).sigAlg("sigalg") + .signature("signature").build(); + given(this.resolver.resolve(any())).willReturn(context); + given(this.factory.createRedirectAuthenticationRequest(any())).willReturn(request); this.filter.doFilterInternal(this.request, this.response, this.filterChain); - assertThat(this.response.getHeader("Location")).contains("RelayState=" + relayStateEncoded).contains("SigAlg=") - .contains("Signature=").startsWith(IDP_SSO_URL); + assertThat(this.response.getHeader("Location")).contains("SigAlg=").contains("Signature=") + .startsWith(IDP_SSO_URL); } @Test public void doFilterWhenSignatureIsDisabledThenSignatureParametersAreNotInTheRedirectURL() throws Exception { - given(this.repository.findByRegistrationId("registration-id")) - .willReturn(this.rpBuilder.providerDetails((c) -> c.signAuthNRequest(false)).build()); - final String relayStateValue = "https://my-relay-state.example.com?with=param&other=param"; - final String relayStateEncoded = UriUtils.encode(relayStateValue, StandardCharsets.ISO_8859_1); - this.request.setParameter("RelayState", relayStateValue); + Saml2AuthenticationRequestContext context = authenticationRequestContext().build(); + Saml2RedirectAuthenticationRequest request = redirectAuthenticationRequest(context).build(); + given(this.resolver.resolve(any())).willReturn(context); + given(this.factory.createRedirectAuthenticationRequest(any())).willReturn(request); this.filter.doFilterInternal(this.request, this.response, this.filterChain); - assertThat(this.response.getHeader("Location")).contains("RelayState=" + relayStateEncoded) - .doesNotContain("SigAlg=").doesNotContain("Signature=").startsWith(IDP_SSO_URL); + assertThat(this.response.getHeader("Location")).doesNotContain("SigAlg=").doesNotContain("Signature=") + .startsWith(IDP_SSO_URL); } @Test public void doFilterWhenPostFormDataIsPresent() throws Exception { - given(this.repository.findByRegistrationId("registration-id")) - .willReturn(this.rpBuilder.providerDetails((c) -> c.binding(Saml2MessageBinding.POST)).build()); - final String relayStateValue = "https://my-relay-state.example.com?with=param&other=param&javascript{alert('1');}"; - final String relayStateEncoded = HtmlUtils.htmlEscape(relayStateValue); - this.request.setParameter("RelayState", relayStateValue); + String relayStateValue = "https://my-relay-state.example.com?with=param&other=param&javascript{alert('1');}"; + String relayStateEncoded = HtmlUtils.htmlEscape(relayStateValue); + RelyingPartyRegistration registration = this.rpBuilder + .assertingPartyDetails((asserting) -> asserting.singleSignOnServiceBinding(Saml2MessageBinding.POST)) + .build(); + Saml2AuthenticationRequestContext context = authenticationRequestContext().relayState(relayStateValue) + .relyingPartyRegistration(registration).build(); + Saml2PostAuthenticationRequest request = postAuthenticationRequest(context).build(); + given(this.resolver.resolve(any())).willReturn(context); + given(this.factory.createPostAuthenticationRequest(any())).willReturn(request); this.filter.doFilterInternal(this.request, this.response, this.filterChain); assertThat(this.response.getHeader("Location")).isNull(); assertThat(this.response.getContentAsString()) @@ -145,66 +175,43 @@ public class Saml2WebSsoAuthenticationRequestFilterTests { @Test public void doFilterWhenSetAuthenticationRequestFactoryThenUses() throws Exception { - RelyingPartyRegistration relyingParty = this.rpBuilder - .providerDetails((c) -> c.binding(Saml2MessageBinding.POST)).build(); - Saml2PostAuthenticationRequest authenticationRequest = mock(Saml2PostAuthenticationRequest.class); - given(authenticationRequest.getAuthenticationRequestUri()).willReturn("uri"); - given(authenticationRequest.getRelayState()).willReturn("relay"); - given(authenticationRequest.getSamlRequest()).willReturn("saml"); - given(this.repository.findByRegistrationId("registration-id")).willReturn(relyingParty); - given(this.factory.createPostAuthenticationRequest(any())).willReturn(authenticationRequest); - Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.repository); - filter.setAuthenticationRequestFactory(this.factory); - filter.doFilterInternal(this.request, this.response, this.filterChain); - assertThat(this.response.getContentAsString()).contains("
    ") - .contains(" c.binding(Saml2MessageBinding.POST)).build(); - Saml2PostAuthenticationRequest authenticationRequest = mock(Saml2PostAuthenticationRequest.class); - given(authenticationRequest.getAuthenticationRequestUri()).willReturn("uri"); - given(authenticationRequest.getRelayState()).willReturn("relay"); - given(authenticationRequest.getSamlRequest()).willReturn("saml"); - given(this.resolver.resolve(this.request)).willReturn(TestSaml2AuthenticationRequestContexts - .authenticationRequestContext().relyingPartyRegistration(relyingParty).build()); - given(this.factory.createPostAuthenticationRequest(any())).willReturn(authenticationRequest); - Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.resolver, - this.factory); - filter.doFilterInternal(this.request, this.response, this.filterChain); - assertThat(this.response.getContentAsString()).contains("") - .contains(" filter.setRedirectMatcher(null)); } @Test public void setAuthenticationRequestFactoryWhenNullThenException() { - Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.repository); + Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.resolver, + this.factory); assertThatIllegalArgumentException().isThrownBy(() -> filter.setAuthenticationRequestFactory(null)); } @Test public void doFilterWhenRequestMatcherFailsThenSkipsFilter() throws Exception { - Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.repository); + Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.resolver, + this.factory); filter.setRedirectMatcher((request) -> false); filter.doFilter(this.request, this.response, this.filterChain); - verifyNoInteractions(this.repository); + verifyNoInteractions(this.resolver, this.factory); } @Test public void doFilterWhenRelyingPartyRegistrationNotFoundThenUnauthorized() throws Exception { - Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.repository); + Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.resolver, + this.factory); filter.doFilter(this.request, this.response, this.filterChain); assertThat(this.response.getStatus()).isEqualTo(401); } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/DefaultRelyingPartyRegistrationResolverTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/web/DefaultRelyingPartyRegistrationResolverTests.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/DefaultRelyingPartyRegistrationResolverTests.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/web/DefaultRelyingPartyRegistrationResolverTests.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolverTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolverTests.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolverTests.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolverTests.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverterTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverterTests.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverterTests.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationTokenConverterTests.java diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilterTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilterTests.java similarity index 100% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilterTests.java rename to saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilterTests.java diff --git a/saml2/saml2-service-provider/src/test/resources/logback-test.xml b/saml2/saml2-service-provider/core/src/test/resources/logback-test.xml similarity index 100% rename from saml2/saml2-service-provider/src/test/resources/logback-test.xml rename to saml2/saml2-service-provider/core/src/test/resources/logback-test.xml diff --git a/saml2/saml2-service-provider/src/test/resources/saml2-response-sso-circle.encoded b/saml2/saml2-service-provider/core/src/test/resources/saml2-response-sso-circle.encoded similarity index 100% rename from saml2/saml2-service-provider/src/test/resources/saml2-response-sso-circle.encoded rename to saml2/saml2-service-provider/core/src/test/resources/saml2-response-sso-circle.encoded diff --git a/saml2/saml2-service-provider/src/test/resources/test-metadata.xml b/saml2/saml2-service-provider/core/src/test/resources/test-metadata.xml similarity index 100% rename from saml2/saml2-service-provider/src/test/resources/test-metadata.xml rename to saml2/saml2-service-provider/core/src/test/resources/test-metadata.xml diff --git a/saml2/saml2-service-provider/opensaml3/saml2-service-provider-opensaml3.gradle b/saml2/saml2-service-provider/opensaml3/saml2-service-provider-opensaml3.gradle new file mode 100644 index 0000000000..02b7e0e849 --- /dev/null +++ b/saml2/saml2-service-provider/opensaml3/saml2-service-provider-opensaml3.gradle @@ -0,0 +1,53 @@ +buildscript { + repositories { + maven { url 'https://repo.spring.io/plugins-release' } + } + dependencies { + classpath 'io.spring.gradle:propdeps-plugin:0.0.10.RELEASE' + } +} + +plugins { + id 'java-library' + id 'io.spring.convention.repository' + id 'io.spring.convention.springdependencymangement' + id 'io.spring.convention.dependency-set' + id 'io.spring.convention.checkstyle' + id 'io.spring.convention.tests-configuration' + id 'io.spring.convention.integration-test' + id 'propdeps' +} + +configurations { + classesOnlyElements { + canBeConsumed = true + canBeResolved = false + } +} + +artifacts { + classesOnlyElements(compileJava.destinationDir) +} + +repositories { + maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" } +} + +dependencies { + constraints { + management("org.opensaml:opensaml-core:3.+") + management("org.opensaml:opensaml-saml-api:3.+") + management("org.opensaml:opensaml-saml-impl:3.+") + } + + compile project(':saml2-service-provider-core') + + compile("org.opensaml:opensaml-core") + compile("org.opensaml:opensaml-saml-api") + compile("org.opensaml:opensaml-saml-impl") + + provided 'javax.servlet:javax.servlet-api' + + testCompile 'com.squareup.okhttp3:mockwebserver' + testCompile project(path : ':saml2-service-provider-core', configuration : 'tests') +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java b/saml2/saml2-service-provider/opensaml3/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java similarity index 82% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java rename to saml2/saml2-service-provider/opensaml3/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java index 46bc579bbf..2a8c3a5f26 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java +++ b/saml2/saml2-service-provider/opensaml3/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java @@ -21,27 +21,21 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.function.Consumer; import javax.annotation.Nonnull; import javax.xml.namespace.QName; -import net.shibboleth.utilities.java.support.resolver.CriteriaSet; import net.shibboleth.utilities.java.support.xml.ParserPool; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.joda.time.DateTime; import org.opensaml.core.config.ConfigurationService; -import org.opensaml.core.criterion.EntityIdCriterion; import org.opensaml.core.xml.XMLObject; import org.opensaml.core.xml.config.XMLObjectProviderRegistry; import org.opensaml.core.xml.schema.XSAny; @@ -51,11 +45,9 @@ import org.opensaml.core.xml.schema.XSDateTime; import org.opensaml.core.xml.schema.XSInteger; import org.opensaml.core.xml.schema.XSString; import org.opensaml.core.xml.schema.XSURI; +import org.opensaml.saml.common.assertion.AssertionValidationException; import org.opensaml.saml.common.assertion.ValidationContext; import org.opensaml.saml.common.assertion.ValidationResult; -import org.opensaml.saml.common.xml.SAMLConstants; -import org.opensaml.saml.criterion.ProtocolCriterion; -import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion; import org.opensaml.saml.saml2.assertion.ConditionValidator; import org.opensaml.saml.saml2.assertion.SAML20AssertionValidator; import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters; @@ -69,35 +61,15 @@ import org.opensaml.saml.saml2.core.Attribute; import org.opensaml.saml.saml2.core.AttributeStatement; import org.opensaml.saml.saml2.core.Condition; import org.opensaml.saml.saml2.core.EncryptedAssertion; -import org.opensaml.saml.saml2.core.EncryptedAttribute; -import org.opensaml.saml.saml2.core.NameID; import org.opensaml.saml.saml2.core.OneTimeUse; import org.opensaml.saml.saml2.core.Response; import org.opensaml.saml.saml2.core.StatusCode; import org.opensaml.saml.saml2.core.SubjectConfirmation; import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller; import org.opensaml.saml.saml2.encryption.Decrypter; -import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver; import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; -import org.opensaml.security.credential.Credential; -import org.opensaml.security.credential.CredentialResolver; -import org.opensaml.security.credential.CredentialSupport; -import org.opensaml.security.credential.UsageType; -import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion; -import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion; -import org.opensaml.security.credential.impl.CollectionCredentialResolver; -import org.opensaml.security.criteria.UsageCriterion; -import org.opensaml.security.x509.BasicX509Credential; -import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap; -import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver; -import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver; -import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver; -import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver; -import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver; -import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver; import org.opensaml.xmlsec.signature.support.SignaturePrevalidator; import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; -import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -115,7 +87,7 @@ import org.springframework.security.saml2.core.OpenSamlInitializationService; import org.springframework.security.saml2.core.Saml2Error; import org.springframework.security.saml2.core.Saml2ErrorCodes; import org.springframework.security.saml2.core.Saml2ResponseValidatorResult; -import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -165,6 +137,8 @@ import org.springframework.util.StringUtils; * "https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf#page=38">SAML 2 * StatusResponse * @see OpenSAML 3 + * @deprecated Because OpenSAML 3 has reached End-of-Life, please update to + * {@link OpenSaml4AuthenticationProvider} */ public final class OpenSamlAuthenticationProvider implements AuthenticationProvider { @@ -201,10 +175,6 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi private Converter responseAuthenticationConverter = createCompatibleResponseAuthenticationConverter(); - private Converter signatureTrustEngineConverter = new SignatureTrustEngineConverter(); - - private Converter decrypterConverter = new DecrypterConverter(); - /** * Creates an {@link OpenSamlAuthenticationProvider} */ @@ -560,53 +530,23 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi private Converter createDefaultResponseSignatureValidator() { return (responseToken) -> { Response response = responseToken.getResponse(); - Saml2AuthenticationToken token = responseToken.getToken(); - Collection errors = new ArrayList<>(); - String issuer = response.getIssuer().getValue(); + RelyingPartyRegistration registration = responseToken.getToken().getRelyingPartyRegistration(); if (response.isSigned()) { - SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator(); - try { - profileValidator.validate(response.getSignature()); - } - catch (Exception ex) { - errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, - "Invalid signature for SAML Response [" + response.getID() + "]: ")); - } - - try { - CriteriaSet criteriaSet = new CriteriaSet(); - criteriaSet.add(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer))); - criteriaSet.add(new EvaluableProtocolRoleDescriptorCriterion( - new ProtocolCriterion(SAMLConstants.SAML20P_NS))); - criteriaSet.add(new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING))); - if (!this.signatureTrustEngineConverter.convert(token).validate(response.getSignature(), - criteriaSet)) { - errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, - "Invalid signature for SAML Response [" + response.getID() + "]")); - } - } - catch (Exception ex) { - errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, - "Invalid signature for SAML Response [" + response.getID() + "]: ")); - } + return OpenSamlVerificationUtils.verifySignature(response, registration).post(response.getSignature()); } - - return Saml2ResponseValidatorResult.failure(errors); + return Saml2ResponseValidatorResult.success(); }; } private Consumer createDefaultResponseElementsDecrypter() { return (responseToken) -> { - Decrypter decrypter = this.decrypterConverter.convert(responseToken.getToken()); Response response = responseToken.getResponse(); - for (EncryptedAssertion encryptedAssertion : responseToken.getResponse().getEncryptedAssertions()) { - try { - Assertion assertion = decrypter.decrypt(encryptedAssertion); - response.getAssertions().add(assertion); - } - catch (Exception ex) { - throw createAuthenticationException(Saml2ErrorCodes.DECRYPTION_ERROR, ex.getMessage(), ex); - } + RelyingPartyRegistration registration = responseToken.getToken().getRelyingPartyRegistration(); + try { + OpenSamlDecryptionUtils.decryptResponseElements(response, registration); + } + catch (Saml2Exception ex) { + throw createAuthenticationException(Saml2ErrorCodes.DECRYPTION_ERROR, ex.getMessage(), ex); } }; } @@ -656,7 +596,8 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi private Converter createDefaultAssertionSignatureValidator() { return createAssertionValidator(Saml2ErrorCodes.INVALID_SIGNATURE, (assertionToken) -> { - SignatureTrustEngine engine = this.signatureTrustEngineConverter.convert(assertionToken.token); + RelyingPartyRegistration registration = assertionToken.getToken().getRelyingPartyRegistration(); + SignatureTrustEngine engine = OpenSamlVerificationUtils.trustEngine(registration); return SAML20AssertionValidators.createSignatureValidator(engine); }, (assertionToken) -> new ValidationContext( Collections.singletonMap(SAML2AssertionValidationParameters.SIGNATURE_REQUIRED, false))); @@ -664,29 +605,12 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi private Consumer createDefaultAssertionElementsDecrypter() { return (assertionToken) -> { - Decrypter decrypter = this.decrypterConverter.convert(assertionToken.getToken()); Assertion assertion = assertionToken.getAssertion(); - for (AttributeStatement statement : assertion.getAttributeStatements()) { - for (EncryptedAttribute encryptedAttribute : statement.getEncryptedAttributes()) { - try { - Attribute attribute = decrypter.decrypt(encryptedAttribute); - statement.getAttributes().add(attribute); - } - catch (Exception ex) { - throw createAuthenticationException(Saml2ErrorCodes.DECRYPTION_ERROR, ex.getMessage(), ex); - } - } - } - if (assertion.getSubject() == null) { - return; - } - if (assertion.getSubject().getEncryptedID() == null) { - return; - } + RelyingPartyRegistration registration = assertionToken.getToken().getRelyingPartyRegistration(); try { - assertion.getSubject().setNameID((NameID) decrypter.decrypt(assertion.getSubject().getEncryptedID())); + OpenSamlDecryptionUtils.decryptAssertionElements(assertion, registration); } - catch (Exception ex) { + catch (Saml2Exception ex) { throw createAuthenticationException(Saml2ErrorCodes.DECRYPTION_ERROR, ex.getMessage(), ex); } }; @@ -765,8 +689,7 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi return (xsBooleanValue != null) ? xsBooleanValue.getValue() : null; } if (xmlObject instanceof XSDateTime) { - DateTime dateTime = ((XSDateTime) xmlObject).getValue(); - return (dateTime != null) ? Instant.ofEpochMilli(dateTime.getMillis()) : null; + return Instant.ofEpochMilli(((XSDateTime) xmlObject).getValue().getMillis()); } return null; } @@ -812,27 +735,6 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi return new ValidationContext(params); } - private static class SignatureTrustEngineConverter - implements Converter { - - @Override - public SignatureTrustEngine convert(Saml2AuthenticationToken token) { - Set credentials = new HashSet<>(); - Collection keys = token.getRelyingPartyRegistration().getAssertingPartyDetails() - .getVerificationX509Credentials(); - for (Saml2X509Credential key : keys) { - BasicX509Credential cred = new BasicX509Credential(key.getCertificate()); - cred.setUsageType(UsageType.SIGNING); - cred.setEntityId(token.getRelyingPartyRegistration().getAssertingPartyDetails().getEntityId()); - credentials.add(cred); - } - CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials); - return new ExplicitKeySignatureTrustEngine(credentialsResolver, - DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver()); - } - - } - private static class SAML20AssertionValidators { private static final Collection conditions = new ArrayList<>(); @@ -861,10 +763,9 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi } }); subjects.add(new BearerSubjectConfirmationValidator() { - @Nonnull @Override - protected ValidationResult validateAddress(@Nonnull SubjectConfirmation confirmation, - @Nonnull Assertion assertion, @Nonnull ValidationContext context) { + protected ValidationResult validateAddress(SubjectConfirmation confirmation, Assertion assertion, + ValidationContext context) throws AssertionValidationException { // applications should validate their own addresses - gh-7514 return ValidationResult.VALID; } @@ -906,27 +807,6 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi } - private static class DecrypterConverter implements Converter { - - private final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver( - Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(), - new SimpleRetrievalMethodEncryptedKeyResolver())); - - @Override - public Decrypter convert(Saml2AuthenticationToken token) { - Collection credentials = new ArrayList<>(); - for (Saml2X509Credential key : token.getRelyingPartyRegistration().getDecryptionX509Credentials()) { - Credential cred = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey()); - credentials.add(cred); - } - KeyInfoCredentialResolver resolver = new CollectionKeyInfoCredentialResolver(credentials); - Decrypter decrypter = new Decrypter(null, resolver, this.encryptedKeyResolver); - decrypter.setRootInNewDocument(true); - return decrypter; - } - - } - /** * A tuple containing an OpenSAML {@link Response} and its associated authentication * token. diff --git a/saml2/saml2-service-provider/opensaml3/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java b/saml2/saml2-service-provider/opensaml3/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java new file mode 100644 index 0000000000..30f4eeea02 --- /dev/null +++ b/saml2/saml2-service-provider/opensaml3/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java @@ -0,0 +1,203 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.authentication; + +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +import org.joda.time.DateTime; +import org.opensaml.core.config.ConfigurationService; +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.saml2.core.AuthnRequest; +import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.impl.AuthnRequestBuilder; +import org.opensaml.saml.saml2.core.impl.IssuerBuilder; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.provider.service.authentication.OpenSamlSigningUtils.QueryParametersPartial; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A {@link Saml2AuthenticationRequestFactory} that generates, signs, and serializes a + * SAML 2.0 AuthnRequest using OpenSAML 3 + * + * @author Filip Hanik + * @author Josh Cummings + * @since 5.2 + * @deprecated Because OpenSAML 3 has reached End-of-Life, please update to + * {@link OpenSaml4AuthenticationRequestFactory} + */ +public class OpenSamlAuthenticationRequestFactory implements Saml2AuthenticationRequestFactory { + + static { + OpenSamlInitializationService.initialize(); + } + + private AuthnRequestBuilder authnRequestBuilder; + + private IssuerBuilder issuerBuilder; + + private Clock clock = Clock.systemUTC(); + + private Converter protocolBindingResolver = (context) -> { + if (context == null) { + return Saml2MessageBinding.POST; + } + return context.getRelyingPartyRegistration().getAssertionConsumerServiceBinding(); + }; + + private Converter authenticationRequestContextConverter; + + /** + * Creates an {@link OpenSamlAuthenticationRequestFactory} + */ + public OpenSamlAuthenticationRequestFactory() { + this.authenticationRequestContextConverter = this::createAuthnRequest; + XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class); + this.authnRequestBuilder = (AuthnRequestBuilder) registry.getBuilderFactory() + .getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME); + this.issuerBuilder = (IssuerBuilder) registry.getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME); + } + + @Override + @Deprecated + public String createAuthenticationRequest(Saml2AuthenticationRequest request) { + Saml2MessageBinding binding = this.protocolBindingResolver.convert(null); + RelyingPartyRegistration registration = RelyingPartyRegistration.withRegistrationId("noId") + .assertionConsumerServiceBinding(binding) + .assertionConsumerServiceLocation(request.getAssertionConsumerServiceUrl()) + .entityId(request.getIssuer()).remoteIdpEntityId("noIssuer").idpWebSsoUrl("noUrl") + .credentials((credentials) -> credentials.addAll(request.getCredentials())).build(); + Saml2AuthenticationRequestContext context = Saml2AuthenticationRequestContext.builder() + .relyingPartyRegistration(registration).issuer(request.getIssuer()) + .assertionConsumerServiceUrl(request.getAssertionConsumerServiceUrl()).build(); + AuthnRequest authnRequest = this.authenticationRequestContextConverter.convert(context); + return OpenSamlSigningUtils.serialize(OpenSamlSigningUtils.sign(authnRequest, registration)); + } + + @Override + public Saml2PostAuthenticationRequest createPostAuthenticationRequest(Saml2AuthenticationRequestContext context) { + AuthnRequest authnRequest = this.authenticationRequestContextConverter.convert(context); + RelyingPartyRegistration registration = context.getRelyingPartyRegistration(); + if (registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) { + OpenSamlSigningUtils.sign(authnRequest, registration); + } + String xml = OpenSamlSigningUtils.serialize(authnRequest); + return Saml2PostAuthenticationRequest.withAuthenticationRequestContext(context) + .samlRequest(Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8))).build(); + } + + @Override + public Saml2RedirectAuthenticationRequest createRedirectAuthenticationRequest( + Saml2AuthenticationRequestContext context) { + AuthnRequest authnRequest = this.authenticationRequestContextConverter.convert(context); + RelyingPartyRegistration registration = context.getRelyingPartyRegistration(); + String xml = OpenSamlSigningUtils.serialize(authnRequest); + Saml2RedirectAuthenticationRequest.Builder result = Saml2RedirectAuthenticationRequest + .withAuthenticationRequestContext(context); + String deflatedAndEncoded = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(xml)); + result.samlRequest(deflatedAndEncoded).relayState(context.getRelayState()); + if (registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) { + QueryParametersPartial partial = OpenSamlSigningUtils.sign(registration).param("SAMLRequest", + deflatedAndEncoded); + if (StringUtils.hasText(context.getRelayState())) { + partial.param("RelayState", context.getRelayState()); + } + Map parameters = partial.parameters(); + return result.sigAlg(parameters.get("SigAlg")).signature(parameters.get("Signature")).build(); + } + return result.build(); + } + + private AuthnRequest createAuthnRequest(Saml2AuthenticationRequestContext context) { + String issuer = context.getIssuer(); + String destination = context.getDestination(); + String assertionConsumerServiceUrl = context.getAssertionConsumerServiceUrl(); + Saml2MessageBinding protocolBinding = this.protocolBindingResolver.convert(context); + AuthnRequest auth = this.authnRequestBuilder.buildObject(); + if (auth.getID() == null) { + auth.setID("ARQ" + UUID.randomUUID().toString().substring(1)); + } + if (auth.getIssueInstant() == null) { + auth.setIssueInstant(new DateTime(this.clock.millis())); + } + if (auth.isForceAuthn() == null) { + auth.setForceAuthn(Boolean.FALSE); + } + if (auth.isPassive() == null) { + auth.setIsPassive(Boolean.FALSE); + } + if (auth.getProtocolBinding() == null) { + auth.setProtocolBinding(protocolBinding.getUrn()); + } + Issuer iss = this.issuerBuilder.buildObject(); + iss.setValue(issuer); + auth.setIssuer(iss); + auth.setDestination(destination); + auth.setAssertionConsumerServiceURL(assertionConsumerServiceUrl); + return auth; + } + + /** + * Set the {@link AuthnRequest} post-processor resolver + * @param authenticationRequestContextConverter + * @since 5.4 + */ + public void setAuthenticationRequestContextConverter( + Converter authenticationRequestContextConverter) { + Assert.notNull(authenticationRequestContextConverter, "authenticationRequestContextConverter cannot be null"); + this.authenticationRequestContextConverter = authenticationRequestContextConverter; + } + + /** + * ' Use this {@link Clock} with {@link Instant#now()} for generating timestamps + * @param clock + */ + public void setClock(Clock clock) { + Assert.notNull(clock, "clock cannot be null"); + this.clock = clock; + } + + /** + * Sets the {@code protocolBinding} to use when generating authentication requests. + * Acceptable values are {@link SAMLConstants#SAML2_POST_BINDING_URI} and + * {@link SAMLConstants#SAML2_REDIRECT_BINDING_URI} The IDP will be reading this value + * in the {@code AuthNRequest} to determine how to send the Response/Assertion to the + * ACS URL, assertion consumer service URL. + * @param protocolBinding either {@link SAMLConstants#SAML2_POST_BINDING_URI} or + * {@link SAMLConstants#SAML2_REDIRECT_BINDING_URI} + * @throws IllegalArgumentException if the protocolBinding is not valid + * @deprecated Use + * {@link org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.Builder#assertionConsumerServiceBinding(Saml2MessageBinding)} + * instead + */ + @Deprecated + public void setProtocolBinding(String protocolBinding) { + Saml2MessageBinding binding = Saml2MessageBinding.from(protocolBinding); + Assert.notNull(binding, "Invalid protocol binding: " + protocolBinding); + this.protocolBindingResolver = (context) -> binding; + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java b/saml2/saml2-service-provider/opensaml3/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java similarity index 88% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java rename to saml2/saml2-service-provider/opensaml3/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java index 3e9f1995ba..93e263a2e3 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java +++ b/saml2/saml2-service-provider/opensaml3/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java @@ -38,10 +38,15 @@ import org.opensaml.core.xml.XMLObject; import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; import org.opensaml.core.xml.io.Marshaller; import org.opensaml.core.xml.io.MarshallingException; +import org.opensaml.core.xml.schema.XSDateTime; +import org.opensaml.core.xml.schema.impl.XSDateTimeBuilder; import org.opensaml.saml.common.assertion.ValidationContext; import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters; import org.opensaml.saml.saml2.core.Assertion; +import org.opensaml.saml.saml2.core.Attribute; import org.opensaml.saml.saml2.core.AttributeStatement; +import org.opensaml.saml.saml2.core.AttributeValue; +import org.opensaml.saml.saml2.core.Conditions; import org.opensaml.saml.saml2.core.EncryptedAssertion; import org.opensaml.saml.saml2.core.EncryptedAttribute; import org.opensaml.saml.saml2.core.EncryptedID; @@ -49,6 +54,9 @@ import org.opensaml.saml.saml2.core.NameID; import org.opensaml.saml.saml2.core.OneTimeUse; import org.opensaml.saml.saml2.core.Response; import org.opensaml.saml.saml2.core.StatusCode; +import org.opensaml.saml.saml2.core.SubjectConfirmation; +import org.opensaml.saml.saml2.core.SubjectConfirmationData; +import org.opensaml.saml.saml2.core.impl.AttributeBuilder; import org.opensaml.saml.saml2.core.impl.EncryptedAssertionBuilder; import org.opensaml.saml.saml2.core.impl.EncryptedIDBuilder; import org.opensaml.saml.saml2.core.impl.NameIDBuilder; @@ -134,8 +142,8 @@ public class OpenSamlAuthenticationProviderTests { @Test public void authenticateWhenInvalidDestinationThenThrowAuthenticationException() { - Response response = TestOpenSamlObjects.response(DESTINATION + "invalid", ASSERTING_PARTY_ENTITY_ID); - response.getAssertions().add(TestOpenSamlObjects.assertion()); + Response response = response(DESTINATION + "invalid", ASSERTING_PARTY_ENTITY_ID); + response.getAssertions().add(assertion()); TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); Saml2AuthenticationToken token = token(response, verifying(registration())); @@ -154,8 +162,8 @@ public class OpenSamlAuthenticationProviderTests { @Test public void authenticateWhenInvalidSignatureOnAssertionThenThrowAuthenticationException() { - Response response = TestOpenSamlObjects.response(); - response.getAssertions().add(TestOpenSamlObjects.assertion()); + Response response = response(); + response.getAssertions().add(assertion()); Saml2AuthenticationToken token = token(response, verifying(registration())); assertThatExceptionOfType(Saml2AuthenticationException.class) .isThrownBy(() -> this.provider.authenticate(token)) @@ -164,8 +172,8 @@ public class OpenSamlAuthenticationProviderTests { @Test public void authenticateWhenOpenSAMLValidationErrorThenThrowAuthenticationException() { - Response response = TestOpenSamlObjects.response(); - Assertion assertion = TestOpenSamlObjects.assertion(); + Response response = response(); + Assertion assertion = assertion(); assertion.getSubject().getSubjectConfirmations().get(0).getSubjectConfirmationData() .setNotOnOrAfter(DateTime.now().minus(Duration.standardDays(3))); TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), @@ -179,8 +187,8 @@ public class OpenSamlAuthenticationProviderTests { @Test public void authenticateWhenMissingSubjectThenThrowAuthenticationException() { - Response response = TestOpenSamlObjects.response(); - Assertion assertion = TestOpenSamlObjects.assertion(); + Response response = response(); + Assertion assertion = assertion(); assertion.setSubject(null); TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); @@ -193,8 +201,8 @@ public class OpenSamlAuthenticationProviderTests { @Test public void authenticateWhenUsernameMissingThenThrowAuthenticationException() { - Response response = TestOpenSamlObjects.response(); - Assertion assertion = TestOpenSamlObjects.assertion(); + Response response = response(); + Assertion assertion = assertion(); assertion.getSubject().getNameID().setValue(null); TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); @@ -207,8 +215,8 @@ public class OpenSamlAuthenticationProviderTests { @Test public void authenticateWhenAssertionContainsValidationAddressThenItSucceeds() { - Response response = TestOpenSamlObjects.response(); - Assertion assertion = TestOpenSamlObjects.assertion(); + Response response = response(); + Assertion assertion = assertion(); assertion.getSubject().getSubjectConfirmations() .forEach((sc) -> sc.getSubjectConfirmationData().setAddress("10.10.10.10")); TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), @@ -220,9 +228,9 @@ public class OpenSamlAuthenticationProviderTests { @Test public void authenticateWhenAssertionContainsAttributesThenItSucceeds() { - Response response = TestOpenSamlObjects.response(); - Assertion assertion = TestOpenSamlObjects.assertion(); - List attributes = TestOpenSamlObjects.attributeStatements(); + Response response = response(); + Assertion assertion = assertion(); + List attributes = attributeStatements(); assertion.getAttributeStatements().addAll(attributes); TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); @@ -244,8 +252,8 @@ public class OpenSamlAuthenticationProviderTests { @Test public void authenticateWhenEncryptedAssertionWithoutSignatureThenItFails() { - Response response = TestOpenSamlObjects.response(); - EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(TestOpenSamlObjects.assertion(), + Response response = response(); + EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(), TestSaml2X509Credentials.assertingPartyEncryptingCredential()); response.getEncryptedAssertions().add(encryptedAssertion); TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), @@ -258,8 +266,8 @@ public class OpenSamlAuthenticationProviderTests { @Test public void authenticateWhenEncryptedAssertionWithSignatureThenItSucceeds() { - Response response = TestOpenSamlObjects.response(); - Assertion assertion = TestOpenSamlObjects.signed(TestOpenSamlObjects.assertion(), + Response response = response(); + Assertion assertion = TestOpenSamlObjects.signed(assertion(), TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion, TestSaml2X509Credentials.assertingPartyEncryptingCredential()); @@ -272,8 +280,8 @@ public class OpenSamlAuthenticationProviderTests { @Test public void authenticateWhenEncryptedAssertionWithResponseSignatureThenItSucceeds() { - Response response = TestOpenSamlObjects.response(); - EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(TestOpenSamlObjects.assertion(), + Response response = response(); + EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(), TestSaml2X509Credentials.assertingPartyEncryptingCredential()); response.getEncryptedAssertions().add(encryptedAssertion); TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), @@ -284,8 +292,8 @@ public class OpenSamlAuthenticationProviderTests { @Test public void authenticateWhenEncryptedNameIdWithSignatureThenItSucceeds() { - Response response = TestOpenSamlObjects.response(); - Assertion assertion = TestOpenSamlObjects.assertion(); + Response response = response(); + Assertion assertion = assertion(); NameID nameId = assertion.getSubject().getNameID(); EncryptedID encryptedID = TestOpenSamlObjects.encrypted(nameId, TestSaml2X509Credentials.assertingPartyEncryptingCredential()); @@ -300,8 +308,8 @@ public class OpenSamlAuthenticationProviderTests { @Test public void authenticateWhenEncryptedAttributeThenDecrypts() { - Response response = TestOpenSamlObjects.response(); - Assertion assertion = TestOpenSamlObjects.assertion(); + Response response = response(); + Assertion assertion = assertion(); EncryptedAttribute attribute = TestOpenSamlObjects.encrypted("name", "value", TestSaml2X509Credentials.assertingPartyEncryptingCredential()); AttributeStatement statement = build(AttributeStatement.DEFAULT_ELEMENT_NAME); @@ -318,8 +326,8 @@ public class OpenSamlAuthenticationProviderTests { @Test public void authenticateWhenDecryptionKeysAreMissingThenThrowAuthenticationException() { - Response response = TestOpenSamlObjects.response(); - EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(TestOpenSamlObjects.assertion(), + Response response = response(); + EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(), TestSaml2X509Credentials.assertingPartyEncryptingCredential()); response.getEncryptedAssertions().add(encryptedAssertion); TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), @@ -332,8 +340,8 @@ public class OpenSamlAuthenticationProviderTests { @Test public void authenticateWhenDecryptionKeysAreWrongThenThrowAuthenticationException() { - Response response = TestOpenSamlObjects.response(); - EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(TestOpenSamlObjects.assertion(), + Response response = response(); + EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(), TestSaml2X509Credentials.assertingPartyEncryptingCredential()); response.getEncryptedAssertions().add(encryptedAssertion); TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), @@ -347,8 +355,8 @@ public class OpenSamlAuthenticationProviderTests { @Test public void writeObjectWhenTypeIsSaml2AuthenticationThenNoException() throws IOException { - Response response = TestOpenSamlObjects.response(); - Assertion assertion = TestOpenSamlObjects.signed(TestOpenSamlObjects.assertion(), + Response response = response(); + Assertion assertion = TestOpenSamlObjects.signed(assertion(), TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion, TestSaml2X509Credentials.assertingPartyEncryptingCredential()); @@ -384,8 +392,8 @@ public class OpenSamlAuthenticationProviderTests { .concat(new Saml2Error("wrong error", "wrong error")) ); // @formatter:on - Response response = TestOpenSamlObjects.response(); - Assertion assertion = TestOpenSamlObjects.assertion(); + Response response = response(); + Assertion assertion = assertion(); OneTimeUse oneTimeUse = build(OneTimeUse.DEFAULT_ELEMENT_NAME); assertion.getConditions().getConditions().add(oneTimeUse); response.getAssertions().add(assertion); @@ -410,8 +418,8 @@ public class OpenSamlAuthenticationProviderTests { .concat(validator.convert(assertionToken)) ); // @formatter:on - Response response = TestOpenSamlObjects.response(); - Assertion assertion = TestOpenSamlObjects.assertion(); + Response response = response(); + Assertion assertion = assertion(); response.getAssertions().add(assertion); TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), ASSERTING_PARTY_ENTITY_ID); @@ -426,8 +434,8 @@ public class OpenSamlAuthenticationProviderTests { public void authenticateWhenDefaultConditionValidatorNotUsedThenSignatureStillChecked() { OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider(); provider.setAssertionValidator((assertionToken) -> Saml2ResponseValidatorResult.success()); - Response response = TestOpenSamlObjects.response(); - Assertion assertion = TestOpenSamlObjects.assertion(); + Response response = response(); + Assertion assertion = assertion(); TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.relyingPartyDecryptingCredential(), RELYING_PARTY_ENTITY_ID); // broken // signature @@ -451,8 +459,8 @@ public class OpenSamlAuthenticationProviderTests { OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider(); provider.setAssertionValidator( OpenSamlAuthenticationProvider.createDefaultAssertionValidator((assertionToken) -> context)); - Response response = TestOpenSamlObjects.response(); - Assertion assertion = TestOpenSamlObjects.assertion(); + Response response = response(); + Assertion assertion = assertion(); response.getAssertions().add(assertion); TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), ASSERTING_PARTY_ENTITY_ID); @@ -467,8 +475,8 @@ public class OpenSamlAuthenticationProviderTests { @Test public void authenticateWithSHA1SignatureThenItSucceeds() throws Exception { - Response response = TestOpenSamlObjects.response(); - Assertion assertion = TestOpenSamlObjects.signed(TestOpenSamlObjects.assertion(), + Response response = response(); + Assertion assertion = TestOpenSamlObjects.signed(assertion(), TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID, SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1); response.getAssertions().add(assertion); @@ -525,8 +533,8 @@ public class OpenSamlAuthenticationProviderTests { @Test public void authenticateWhenCustomResponseElementsDecrypterThenDecryptsResponse() { - Response response = TestOpenSamlObjects.response(); - Assertion assertion = TestOpenSamlObjects.assertion(); + Response response = response(); + Assertion assertion = assertion(); TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); response.getEncryptedAssertions().add(new EncryptedAssertionBuilder().buildObject()); @@ -540,8 +548,8 @@ public class OpenSamlAuthenticationProviderTests { @Test public void authenticateWhenCustomAssertionElementsDecrypterThenDecryptsAssertion() { - Response response = TestOpenSamlObjects.response(); - Assertion assertion = TestOpenSamlObjects.assertion(); + Response response = response(); + Assertion assertion = assertion(); EncryptedID id = new EncryptedIDBuilder().buildObject(); id.setEncryptedData(new EncryptedDataBuilder().buildObject()); assertion.getSubject().setEncryptedID(id); @@ -600,13 +608,52 @@ public class OpenSamlAuthenticationProviderTests { return (ex) -> { assertThat(ex.getError().getErrorCode()).isEqualTo(errorCode); if (StringUtils.hasText(description)) { - assertThat(ex.getError().getDescription()).isEqualTo(description); + assertThat(ex.getError().getDescription()).contains(description); } }; } - private Saml2AuthenticationToken token() { + private Response response() { Response response = TestOpenSamlObjects.response(); + response.setIssueInstant(DateTime.now()); + return response; + } + + private Response response(String destination, String issuerEntityId) { + Response response = TestOpenSamlObjects.response(destination, issuerEntityId); + response.setIssueInstant(DateTime.now()); + return response; + } + + private Assertion assertion() { + Assertion assertion = TestOpenSamlObjects.assertion(); + assertion.setIssueInstant(DateTime.now()); + for (SubjectConfirmation confirmation : assertion.getSubject().getSubjectConfirmations()) { + SubjectConfirmationData data = confirmation.getSubjectConfirmationData(); + data.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000))); + data.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000))); + } + Conditions conditions = assertion.getConditions(); + conditions.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000))); + conditions.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000))); + return assertion; + } + + private List attributeStatements() { + List attributeStatements = TestOpenSamlObjects.attributeStatements(); + AttributeBuilder attributeBuilder = new AttributeBuilder(); + Attribute registeredDateAttr = attributeBuilder.buildObject(); + registeredDateAttr.setName("registeredDate"); + XSDateTime registeredDate = new XSDateTimeBuilder().buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, + XSDateTime.TYPE_NAME); + registeredDate.setValue(DateTime.parse("1970-01-01T00:00:00Z")); + registeredDateAttr.getAttributeValues().add(registeredDate); + attributeStatements.get(0).getAttributes().add(registeredDateAttr); + return attributeStatements; + } + + private Saml2AuthenticationToken token() { + Response response = response(); RelyingPartyRegistration registration = verifying(registration()).build(); return new Saml2AuthenticationToken(registration, serialize(response)); } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactoryTests.java b/saml2/saml2-service-provider/opensaml3/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactoryTests.java similarity index 98% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactoryTests.java rename to saml2/saml2-service-provider/opensaml3/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactoryTests.java index f0f03819fe..4b222b01d1 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactoryTests.java +++ b/saml2/saml2-service-provider/opensaml3/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactoryTests.java @@ -19,6 +19,7 @@ package org.springframework.security.saml2.provider.service.authentication; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; +import org.joda.time.DateTime; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -200,8 +201,7 @@ public class OpenSamlAuthenticationRequestFactoryTests { public void createPostAuthenticationRequestWhenAuthnRequestConsumerThenUses() { Converter authenticationRequestContextConverter = mock( Converter.class); - given(authenticationRequestContextConverter.convert(this.context)) - .willReturn(TestOpenSamlObjects.authnRequest()); + given(authenticationRequestContextConverter.convert(this.context)).willReturn(authnRequest()); this.factory.setAuthenticationRequestContextConverter(authenticationRequestContextConverter); this.factory.createPostAuthenticationRequest(this.context); @@ -212,8 +212,7 @@ public class OpenSamlAuthenticationRequestFactoryTests { public void createRedirectAuthenticationRequestWhenAuthnRequestConsumerThenUses() { Converter authenticationRequestContextConverter = mock( Converter.class); - given(authenticationRequestContextConverter.convert(this.context)) - .willReturn(TestOpenSamlObjects.authnRequest()); + given(authenticationRequestContextConverter.convert(this.context)).willReturn(authnRequest()); this.factory.setAuthenticationRequestContextConverter(authenticationRequestContextConverter); this.factory.createRedirectAuthenticationRequest(this.context); @@ -256,6 +255,12 @@ public class OpenSamlAuthenticationRequestFactoryTests { assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); } + private AuthnRequest authnRequest() { + AuthnRequest authnRequest = TestOpenSamlObjects.authnRequest(); + authnRequest.setIssueInstant(DateTime.now()); + return authnRequest; + } + private AuthnRequest getAuthNRequest(Saml2MessageBinding binding) { AbstractSaml2AuthenticationRequest result = (binding == Saml2MessageBinding.REDIRECT) ? this.factory.createRedirectAuthenticationRequest(this.context) diff --git a/saml2/saml2-service-provider/opensaml4/saml2-service-provider-opensaml4.gradle b/saml2/saml2-service-provider/opensaml4/saml2-service-provider-opensaml4.gradle new file mode 100644 index 0000000000..2b5deabaf5 --- /dev/null +++ b/saml2/saml2-service-provider/opensaml4/saml2-service-provider-opensaml4.gradle @@ -0,0 +1,58 @@ +buildscript { + repositories { + maven { url 'https://repo.spring.io/plugins-release' } + } + dependencies { + classpath 'io.spring.gradle:propdeps-plugin:0.0.10.RELEASE' + } +} + +plugins { + id 'java-library' + id 'io.spring.convention.repository' + id 'io.spring.convention.springdependencymangement' + id 'io.spring.convention.dependency-set' + id 'io.spring.convention.checkstyle' + id 'io.spring.convention.tests-configuration' + id 'io.spring.convention.integration-test' + id 'propdeps' +} + +configurations { + classesOnlyElements { + canBeConsumed = true + canBeResolved = false + attributes { + attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 11) + } + } +} + +artifacts { + classesOnlyElements(compileJava.destinationDir) +} + +sourceCompatibility = '11' + +repositories { + maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" } +} + +dependencies { + constraints { + management("org.opensaml:opensaml-core:4.+") + management("org.opensaml:opensaml-saml-api:4.+") + management("org.opensaml:opensaml-saml-impl:4.+") + } + + compile project(':saml2-service-provider-core') + + compile("org.opensaml:opensaml-core") + compile("org.opensaml:opensaml-saml-api") + compile("org.opensaml:opensaml-saml-impl") + + provided 'javax.servlet:javax.servlet-api' + + testCompile 'com.squareup.okhttp3:mockwebserver' + testCompile project(path : ':saml2-service-provider-core', configuration : 'tests') +} diff --git a/saml2/saml2-service-provider/opensaml4/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java b/saml2/saml2-service-provider/opensaml4/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java new file mode 100644 index 0000000000..a4a48c6dd6 --- /dev/null +++ b/saml2/saml2-service-provider/opensaml4/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java @@ -0,0 +1,770 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.authentication; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import javax.annotation.Nonnull; +import javax.xml.namespace.QName; + +import net.shibboleth.utilities.java.support.xml.ParserPool; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.opensaml.core.config.ConfigurationService; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.opensaml.core.xml.schema.XSAny; +import org.opensaml.core.xml.schema.XSBoolean; +import org.opensaml.core.xml.schema.XSBooleanValue; +import org.opensaml.core.xml.schema.XSDateTime; +import org.opensaml.core.xml.schema.XSInteger; +import org.opensaml.core.xml.schema.XSString; +import org.opensaml.core.xml.schema.XSURI; +import org.opensaml.saml.common.assertion.ValidationContext; +import org.opensaml.saml.common.assertion.ValidationResult; +import org.opensaml.saml.saml2.assertion.ConditionValidator; +import org.opensaml.saml.saml2.assertion.SAML20AssertionValidator; +import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters; +import org.opensaml.saml.saml2.assertion.StatementValidator; +import org.opensaml.saml.saml2.assertion.SubjectConfirmationValidator; +import org.opensaml.saml.saml2.assertion.impl.AudienceRestrictionConditionValidator; +import org.opensaml.saml.saml2.assertion.impl.BearerSubjectConfirmationValidator; +import org.opensaml.saml.saml2.assertion.impl.DelegationRestrictionConditionValidator; +import org.opensaml.saml.saml2.core.Assertion; +import org.opensaml.saml.saml2.core.Attribute; +import org.opensaml.saml.saml2.core.AttributeStatement; +import org.opensaml.saml.saml2.core.Condition; +import org.opensaml.saml.saml2.core.EncryptedAssertion; +import org.opensaml.saml.saml2.core.OneTimeUse; +import org.opensaml.saml.saml2.core.Response; +import org.opensaml.saml.saml2.core.SubjectConfirmation; +import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller; +import org.opensaml.saml.saml2.encryption.Decrypter; +import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; +import org.opensaml.xmlsec.signature.support.SignaturePrevalidator; +import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.log.LogMessage; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.core.Saml2ErrorCodes; +import org.springframework.security.saml2.core.Saml2ResponseValidatorResult; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * Implementation of {@link AuthenticationProvider} for SAML authentications when + * receiving a {@code Response} object containing an {@code Assertion}. This + * implementation uses the {@code OpenSAML 4} library. + * + *

    + * The {@link OpenSaml4AuthenticationProvider} supports {@link Saml2AuthenticationToken} + * objects that contain a SAML response in its decoded XML format + * {@link Saml2AuthenticationToken#getSaml2Response()} along with the information about + * the asserting party, the identity provider (IDP), as well as the relying party, the + * service provider (SP, this application). + *

    + *

    + * The {@link Saml2AuthenticationToken} will be processed into a SAML Response object. The + * SAML response object can be signed. If the Response is signed, a signature will not be + * required on the assertion. + *

    + *

    + * While a response object can contain a list of assertion, this provider will only + * leverage the first valid assertion for the purpose of authentication. Assertions that + * do not pass validation will be ignored. If no valid assertions are found a + * {@link Saml2AuthenticationException} is thrown. + *

    + *

    + * This provider supports two types of encrypted SAML elements + *

    + * If the assertion is encrypted, then signature validation on the assertion is no longer + * required. + *

    + *

    + * This provider does not perform an X509 certificate validation on the configured + * asserting party, IDP, verification certificates. + *

    + * + * @author Josh Cummings + * @since 5.5 + * @see SAML 2 + * StatusResponse + * @see OpenSAML 3 + */ +public final class OpenSaml4AuthenticationProvider implements AuthenticationProvider { + + static { + OpenSamlInitializationService.initialize(); + } + + private final Log logger = LogFactory.getLog(this.getClass()); + + private final ResponseUnmarshaller responseUnmarshaller; + + private final ParserPool parserPool; + + private final Converter responseSignatureValidator = createDefaultResponseSignatureValidator(); + + private Consumer responseElementsDecrypter = createDefaultResponseElementsDecrypter(); + + private final Converter responseValidator = createDefaultResponseValidator(); + + private final Converter assertionSignatureValidator = createDefaultAssertionSignatureValidator(); + + private Consumer assertionElementsDecrypter = createDefaultAssertionElementsDecrypter(); + + private Converter assertionValidator = createDefaultAssertionValidator(); + + private Converter responseAuthenticationConverter = createDefaultResponseAuthenticationConverter(); + + /** + * Creates an {@link OpenSaml4AuthenticationProvider} + */ + public OpenSaml4AuthenticationProvider() { + XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class); + this.responseUnmarshaller = (ResponseUnmarshaller) registry.getUnmarshallerFactory() + .getUnmarshaller(Response.DEFAULT_ELEMENT_NAME); + this.parserPool = registry.getParserPool(); + } + + /** + * Set the {@link Consumer} strategy to use for decrypting elements of a validated + * {@link Response}. The default strategy decrypts all {@link EncryptedAssertion}s + * using OpenSAML's {@link Decrypter}, adding the results to + * {@link Response#getAssertions()}. + * + * You can use this method to configure the {@link Decrypter} instance like so: + * + *
    +	 *	OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
    +	 *	provider.setResponseElementsDecrypter((responseToken) -> {
    +	 *	    DecrypterParameters parameters = new DecrypterParameters();
    +	 *	    // ... set parameters as needed
    +	 *	    Decrypter decrypter = new Decrypter(parameters);
    +	 *		Response response = responseToken.getResponse();
    +	 *  	EncryptedAssertion encrypted = response.getEncryptedAssertions().get(0);
    +	 *  	try {
    +	 *  		Assertion assertion = decrypter.decrypt(encrypted);
    +	 *  		response.getAssertions().add(assertion);
    +	 *  	} catch (Exception e) {
    +	 *  	 	throw new Saml2AuthenticationException(...);
    +	 *  	}
    +	 *	});
    +	 * 
    + * + * Or, in the event that you have your own custom decryption interface, the same + * pattern applies: + * + *
    +	 *	OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
    +	 *	Converter<EncryptedAssertion, Assertion> myService = ...
    +	 *	provider.setResponseDecrypter((responseToken) -> {
    +	 *	   Response response = responseToken.getResponse();
    +	 *	   response.getEncryptedAssertions().stream()
    +	 *	   		.map(service::decrypt).forEach(response.getAssertions()::add);
    +	 *	});
    +	 * 
    + * + * This is valuable when using an external service to perform the decryption. + * @param responseElementsDecrypter the {@link Consumer} for decrypting response + * elements + * @since 5.5 + */ + public void setResponseElementsDecrypter(Consumer responseElementsDecrypter) { + Assert.notNull(responseElementsDecrypter, "responseElementsDecrypter cannot be null"); + this.responseElementsDecrypter = responseElementsDecrypter; + } + + /** + * Set the {@link Converter} to use for validating each {@link Assertion} in the SAML + * 2.0 Response. + * + * You can still invoke the default validator by delgating to + * {@link #createAssertionValidator}, like so: + * + *
    +	 *	OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
    +	 *  provider.setAssertionValidator(assertionToken -> {
    +	 *		Saml2ResponseValidatorResult result = createDefaultAssertionValidator()
    +	 *			.convert(assertionToken)
    +	 *		return result.concat(myCustomValidator.convert(assertionToken));
    +	 *  });
    +	 * 
    + * + * You can also use this method to configure the provider to use a different + * {@link ValidationContext} from the default, like so: + * + *
    +	 *	OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
    +	 *	provider.setAssertionValidator(
    +	 *		createDefaultAssertionValidator(assertionToken -> {
    +	 *			Map<String, Object> params = new HashMap<>();
    +	 *			params.put(CLOCK_SKEW, 2 * 60 * 1000);
    +	 *			// other parameters
    +	 *			return new ValidationContext(params);
    +	 *		}));
    +	 * 
    + * + * Consider taking a look at {@link #createValidationContext} to see how it constructs + * a {@link ValidationContext}. + * + * It is not necessary to delegate to the default validator. You can safely replace it + * entirely with your own. Note that signature verification is performed as a separate + * step from this validator. + * @param assertionValidator the validator to use + * @since 5.4 + */ + public void setAssertionValidator(Converter assertionValidator) { + Assert.notNull(assertionValidator, "assertionValidator cannot be null"); + this.assertionValidator = assertionValidator; + } + + /** + * Set the {@link Consumer} strategy to use for decrypting elements of a validated + * {@link Assertion}. + * + * You can use this method to configure the {@link Decrypter} used like so: + * + *
    +	 *	OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
    +	 *	provider.setResponseDecrypter((assertionToken) -> {
    +	 *	    DecrypterParameters parameters = new DecrypterParameters();
    +	 *	    // ... set parameters as needed
    +	 *	    Decrypter decrypter = new Decrypter(parameters);
    +	 *		Assertion assertion = assertionToken.getAssertion();
    +	 *  	EncryptedID encrypted = assertion.getSubject().getEncryptedID();
    +	 *  	try {
    +	 *  		NameID name = decrypter.decrypt(encrypted);
    +	 *  		assertion.getSubject().setNameID(name);
    +	 *  	} catch (Exception e) {
    +	 *  	 	throw new Saml2AuthenticationException(...);
    +	 *  	}
    +	 *	});
    +	 * 
    + * + * Or, in the event that you have your own custom interface, the same pattern applies: + * + *
    +	 *	OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
    +	 *	MyDecryptionService myService = ...
    +	 *	provider.setResponseDecrypter((responseToken) -> {
    +	 *	   	Assertion assertion = assertionToken.getAssertion();
    +	 *	   	EncryptedID encrypted = assertion.getSubject().getEncryptedID();
    +	 *		NameID name = myService.decrypt(encrypted);
    +	 *		assertion.getSubject().setNameID(name);
    +	 *	});
    +	 * 
    + * @param assertionDecrypter the {@link Consumer} for decrypting assertion elements + * @since 5.5 + */ + public void setAssertionElementsDecrypter(Consumer assertionDecrypter) { + Assert.notNull(assertionDecrypter, "assertionDecrypter cannot be null"); + this.assertionElementsDecrypter = assertionDecrypter; + } + + /** + * Set the {@link Converter} to use for converting a validated {@link Response} into + * an {@link AbstractAuthenticationToken}. + * + * You can delegate to the default behavior by calling + * {@link #createDefaultResponseAuthenticationConverter()} like so: + * + *
    +	 *	OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
    +	 * 	Converter<ResponseToken, Saml2Authentication> authenticationConverter =
    +	 * 			createDefaultResponseAuthenticationConverter();
    +	 *	provider.setResponseAuthenticationConverter(responseToken -> {
    +	 *		Saml2Authentication authentication = authenticationConverter.convert(responseToken);
    +	 *		User user = myUserRepository.findByUsername(authentication.getName());
    +	 *		return new MyAuthentication(authentication, user);
    +	 *	});
    +	 * 
    + * @param responseAuthenticationConverter the {@link Converter} to use + * @since 5.4 + */ + public void setResponseAuthenticationConverter( + Converter responseAuthenticationConverter) { + Assert.notNull(responseAuthenticationConverter, "responseAuthenticationConverter cannot be null"); + this.responseAuthenticationConverter = responseAuthenticationConverter; + } + + /** + * Construct a default strategy for validating each SAML 2.0 Assertion and associated + * {@link Authentication} token + * @return the default assertion validator strategy + */ + public static Converter createDefaultAssertionValidator() { + + return createAssertionValidator(Saml2ErrorCodes.INVALID_ASSERTION, + (assertionToken) -> SAML20AssertionValidators.attributeValidator, + (assertionToken) -> createValidationContext(assertionToken, (params) -> params + .put(SAML2AssertionValidationParameters.CLOCK_SKEW, Duration.ofMinutes(5).toMillis()))); + } + + /** + * Construct a default strategy for validating each SAML 2.0 Assertion and associated + * {@link Authentication} token + * @param contextConverter the conversion strategy to use to generate a + * {@link ValidationContext} for each assertion being validated + * @return the default assertion validator strategy + */ + public static Converter createDefaultAssertionValidator( + Converter contextConverter) { + + return createAssertionValidator(Saml2ErrorCodes.INVALID_ASSERTION, + (assertionToken) -> SAML20AssertionValidators.attributeValidator, contextConverter); + } + + /** + * Construct a default strategy for converting a SAML 2.0 Response and + * {@link Authentication} token into a {@link Saml2Authentication} + * @return the default response authentication converter strategy + */ + public static Converter createDefaultResponseAuthenticationConverter() { + return (responseToken) -> { + Response response = responseToken.response; + Saml2AuthenticationToken token = responseToken.token; + Assertion assertion = CollectionUtils.firstElement(response.getAssertions()); + String username = assertion.getSubject().getNameID().getValue(); + Map> attributes = getAssertionAttributes(assertion); + return new Saml2Authentication(new DefaultSaml2AuthenticatedPrincipal(username, attributes), + token.getSaml2Response(), AuthorityUtils.createAuthorityList("ROLE_USER")); + }; + } + + /** + * @param authentication the authentication request object, must be of type + * {@link Saml2AuthenticationToken} + * @return {@link Saml2Authentication} if the assertion is valid + * @throws AuthenticationException if a validation exception occurs + */ + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + try { + Saml2AuthenticationToken token = (Saml2AuthenticationToken) authentication; + String serializedResponse = token.getSaml2Response(); + Response response = parse(serializedResponse); + process(token, response); + return this.responseAuthenticationConverter.convert(new ResponseToken(response, token)); + } + catch (Saml2AuthenticationException ex) { + throw ex; + } + catch (Exception ex) { + throw createAuthenticationException(Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR, ex.getMessage(), ex); + } + } + + @Override + public boolean supports(Class authentication) { + return authentication != null && Saml2AuthenticationToken.class.isAssignableFrom(authentication); + } + + private Response parse(String response) throws Saml2Exception, Saml2AuthenticationException { + try { + Document document = this.parserPool + .parse(new ByteArrayInputStream(response.getBytes(StandardCharsets.UTF_8))); + Element element = document.getDocumentElement(); + return (Response) this.responseUnmarshaller.unmarshall(element); + } + catch (Exception ex) { + throw createAuthenticationException(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA, ex.getMessage(), ex); + } + } + + private void process(Saml2AuthenticationToken token, Response response) { + String issuer = response.getIssuer().getValue(); + this.logger.debug(LogMessage.format("Processing SAML response from %s", issuer)); + boolean responseSigned = response.isSigned(); + + ResponseToken responseToken = new ResponseToken(response, token); + Saml2ResponseValidatorResult result = this.responseSignatureValidator.convert(responseToken); + if (responseSigned) { + this.responseElementsDecrypter.accept(responseToken); + } + result = result.concat(this.responseValidator.convert(responseToken)); + boolean allAssertionsSigned = true; + for (Assertion assertion : response.getAssertions()) { + AssertionToken assertionToken = new AssertionToken(assertion, token); + result = result.concat(this.assertionSignatureValidator.convert(assertionToken)); + allAssertionsSigned = allAssertionsSigned && assertion.isSigned(); + if (responseSigned || assertion.isSigned()) { + this.assertionElementsDecrypter.accept(new AssertionToken(assertion, token)); + } + result = result.concat(this.assertionValidator.convert(assertionToken)); + } + if (!responseSigned && !allAssertionsSigned) { + String description = "Either the response or one of the assertions is unsigned. " + + "Please either sign the response or all of the assertions."; + throw createAuthenticationException(Saml2ErrorCodes.INVALID_SIGNATURE, description, null); + } + Assertion firstAssertion = CollectionUtils.firstElement(response.getAssertions()); + if (!hasName(firstAssertion)) { + Saml2Error error = new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND, + "Assertion [" + firstAssertion.getID() + "] is missing a subject"); + result = result.concat(error); + } + + if (result.hasErrors()) { + Collection errors = result.getErrors(); + if (this.logger.isTraceEnabled()) { + this.logger.debug("Found " + errors.size() + " validation errors in SAML response [" + response.getID() + + "]: " + errors); + } + else if (this.logger.isDebugEnabled()) { + this.logger.debug( + "Found " + errors.size() + " validation errors in SAML response [" + response.getID() + "]"); + } + Saml2Error first = errors.iterator().next(); + throw createAuthenticationException(first.getErrorCode(), first.getDescription(), null); + } + else { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Successfully processed SAML Response [" + response.getID() + "]"); + } + } + } + + private Converter createDefaultResponseSignatureValidator() { + return (responseToken) -> { + Response response = responseToken.getResponse(); + RelyingPartyRegistration registration = responseToken.getToken().getRelyingPartyRegistration(); + if (response.isSigned()) { + return OpenSamlVerificationUtils.verifySignature(response, registration).post(response.getSignature()); + } + return Saml2ResponseValidatorResult.success(); + }; + } + + private Consumer createDefaultResponseElementsDecrypter() { + return (responseToken) -> { + Response response = responseToken.getResponse(); + RelyingPartyRegistration registration = responseToken.getToken().getRelyingPartyRegistration(); + try { + OpenSamlDecryptionUtils.decryptResponseElements(response, registration); + } + catch (Exception ex) { + throw createAuthenticationException(Saml2ErrorCodes.DECRYPTION_ERROR, ex.getMessage(), ex); + } + }; + } + + private Converter createDefaultResponseValidator() { + return (responseToken) -> { + Response response = responseToken.getResponse(); + Saml2AuthenticationToken token = responseToken.getToken(); + Saml2ResponseValidatorResult result = Saml2ResponseValidatorResult.success(); + String issuer = response.getIssuer().getValue(); + String destination = response.getDestination(); + String location = token.getRelyingPartyRegistration().getAssertionConsumerServiceLocation(); + if (StringUtils.hasText(destination) && !destination.equals(location)) { + String message = "Invalid destination [" + destination + "] for SAML response [" + response.getID() + + "]"; + result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION, message)); + } + String assertingPartyEntityId = token.getRelyingPartyRegistration().getAssertingPartyDetails() + .getEntityId(); + if (!StringUtils.hasText(issuer) || !issuer.equals(assertingPartyEntityId)) { + String message = String.format("Invalid issuer [%s] for SAML response [%s]", issuer, response.getID()); + result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, message)); + } + if (response.getAssertions().isEmpty()) { + throw createAuthenticationException(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA, + "No assertions found in response.", null); + } + return result; + }; + } + + private Converter createDefaultAssertionSignatureValidator() { + return createAssertionValidator(Saml2ErrorCodes.INVALID_SIGNATURE, (assertionToken) -> { + RelyingPartyRegistration registration = assertionToken.getToken().getRelyingPartyRegistration(); + SignatureTrustEngine engine = OpenSamlVerificationUtils.trustEngine(registration); + return SAML20AssertionValidators.createSignatureValidator(engine); + }, (assertionToken) -> new ValidationContext( + Collections.singletonMap(SAML2AssertionValidationParameters.SIGNATURE_REQUIRED, false))); + } + + private Consumer createDefaultAssertionElementsDecrypter() { + return (assertionToken) -> { + Assertion assertion = assertionToken.getAssertion(); + RelyingPartyRegistration registration = assertionToken.getToken().getRelyingPartyRegistration(); + try { + OpenSamlDecryptionUtils.decryptAssertionElements(assertion, registration); + } + catch (Exception ex) { + throw createAuthenticationException(Saml2ErrorCodes.DECRYPTION_ERROR, ex.getMessage(), ex); + } + }; + } + + private boolean hasName(Assertion assertion) { + if (assertion == null) { + return false; + } + if (assertion.getSubject() == null) { + return false; + } + if (assertion.getSubject().getNameID() == null) { + return false; + } + return assertion.getSubject().getNameID().getValue() != null; + } + + private static Map> getAssertionAttributes(Assertion assertion) { + Map> attributeMap = new LinkedHashMap<>(); + for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) { + for (Attribute attribute : attributeStatement.getAttributes()) { + List attributeValues = new ArrayList<>(); + for (XMLObject xmlObject : attribute.getAttributeValues()) { + Object attributeValue = getXmlObjectValue(xmlObject); + if (attributeValue != null) { + attributeValues.add(attributeValue); + } + } + attributeMap.put(attribute.getName(), attributeValues); + } + } + return attributeMap; + } + + private static Object getXmlObjectValue(XMLObject xmlObject) { + if (xmlObject instanceof XSAny) { + return ((XSAny) xmlObject).getTextContent(); + } + if (xmlObject instanceof XSString) { + return ((XSString) xmlObject).getValue(); + } + if (xmlObject instanceof XSInteger) { + return ((XSInteger) xmlObject).getValue(); + } + if (xmlObject instanceof XSURI) { + return ((XSURI) xmlObject).getURI(); + } + if (xmlObject instanceof XSBoolean) { + XSBooleanValue xsBooleanValue = ((XSBoolean) xmlObject).getValue(); + return (xsBooleanValue != null) ? xsBooleanValue.getValue() : null; + } + if (xmlObject instanceof XSDateTime) { + return ((XSDateTime) xmlObject).getValue(); + } + return null; + } + + private static Saml2AuthenticationException createAuthenticationException(String code, String message, + Exception cause) { + return new Saml2AuthenticationException(new Saml2Error(code, message), cause); + } + + private static Converter createAssertionValidator(String errorCode, + Converter validatorConverter, + Converter contextConverter) { + + return (assertionToken) -> { + Assertion assertion = assertionToken.assertion; + SAML20AssertionValidator validator = validatorConverter.convert(assertionToken); + ValidationContext context = contextConverter.convert(assertionToken); + try { + ValidationResult result = validator.validate(assertion, context); + if (result == ValidationResult.VALID) { + return Saml2ResponseValidatorResult.success(); + } + } + catch (Exception ex) { + String message = String.format("Invalid assertion [%s] for SAML response [%s]: %s", assertion.getID(), + ((Response) assertion.getParent()).getID(), ex.getMessage()); + return Saml2ResponseValidatorResult.failure(new Saml2Error(errorCode, message)); + } + String message = String.format("Invalid assertion [%s] for SAML response [%s]: %s", assertion.getID(), + ((Response) assertion.getParent()).getID(), context.getValidationFailureMessage()); + return Saml2ResponseValidatorResult.failure(new Saml2Error(errorCode, message)); + }; + } + + private static ValidationContext createValidationContext(AssertionToken assertionToken, + Consumer> paramsConsumer) { + String audience = assertionToken.token.getRelyingPartyRegistration().getEntityId(); + String recipient = assertionToken.token.getRelyingPartyRegistration().getAssertionConsumerServiceLocation(); + Map params = new HashMap<>(); + params.put(SAML2AssertionValidationParameters.COND_VALID_AUDIENCES, Collections.singleton(audience)); + params.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, Collections.singleton(recipient)); + paramsConsumer.accept(params); + return new ValidationContext(params); + } + + private static class SAML20AssertionValidators { + + private static final Collection conditions = new ArrayList<>(); + + private static final Collection subjects = new ArrayList<>(); + + private static final Collection statements = new ArrayList<>(); + + private static final SignaturePrevalidator validator = new SAMLSignatureProfileValidator(); + + static { + conditions.add(new AudienceRestrictionConditionValidator()); + conditions.add(new DelegationRestrictionConditionValidator()); + conditions.add(new ConditionValidator() { + @Nonnull + @Override + public QName getServicedCondition() { + return OneTimeUse.DEFAULT_ELEMENT_NAME; + } + + @Nonnull + @Override + public ValidationResult validate(Condition condition, Assertion assertion, ValidationContext context) { + // applications should validate their own OneTimeUse conditions + return ValidationResult.VALID; + } + }); + subjects.add(new BearerSubjectConfirmationValidator() { + @Override + protected ValidationResult validateAddress(SubjectConfirmation confirmation, Assertion assertion, + ValidationContext context, boolean required) { + // applications should validate their own addresses - gh-7514 + return ValidationResult.VALID; + } + + @Override + protected ValidationResult validateInResponseTo(SubjectConfirmation confirmation, Assertion assertion, + ValidationContext context, boolean required) { + // applications should validate their own in response to + return ValidationResult.VALID; + } + }); + } + + private static final SAML20AssertionValidator attributeValidator = new SAML20AssertionValidator(conditions, + subjects, statements, null, null, null) { + @Nonnull + @Override + protected ValidationResult validateSignature(Assertion token, ValidationContext context) { + return ValidationResult.VALID; + } + }; + + static SAML20AssertionValidator createSignatureValidator(SignatureTrustEngine engine) { + return new SAML20AssertionValidator(new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), null, engine, + validator) { + @Nonnull + @Override + protected ValidationResult validateConditions(Assertion assertion, ValidationContext context) { + return ValidationResult.VALID; + } + + @Nonnull + @Override + protected ValidationResult validateSubjectConfirmation(Assertion assertion, ValidationContext context) { + return ValidationResult.VALID; + } + + @Nonnull + @Override + protected ValidationResult validateStatements(Assertion assertion, ValidationContext context) { + return ValidationResult.VALID; + } + }; + + } + + } + + /** + * A tuple containing an OpenSAML {@link Response} and its associated authentication + * token. + * + * @since 5.4 + */ + public static class ResponseToken { + + private final Saml2AuthenticationToken token; + + private final Response response; + + ResponseToken(Response response, Saml2AuthenticationToken token) { + this.token = token; + this.response = response; + } + + public Response getResponse() { + return this.response; + } + + public Saml2AuthenticationToken getToken() { + return this.token; + } + + } + + /** + * A tuple containing an OpenSAML {@link Assertion} and its associated authentication + * token. + * + * @since 5.4 + */ + public static class AssertionToken { + + private final Saml2AuthenticationToken token; + + private final Assertion assertion; + + AssertionToken(Assertion assertion, Saml2AuthenticationToken token) { + this.token = token; + this.assertion = assertion; + } + + public Assertion getAssertion() { + return this.assertion; + } + + public Saml2AuthenticationToken getToken() { + return this.token; + } + + } + +} diff --git a/saml2/saml2-service-provider/opensaml4/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactory.java b/saml2/saml2-service-provider/opensaml4/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactory.java new file mode 100644 index 0000000000..30a3af1835 --- /dev/null +++ b/saml2/saml2-service-provider/opensaml4/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactory.java @@ -0,0 +1,180 @@ +/* + * Copyright 2002-2020 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.saml2.provider.service.authentication; + +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +import org.opensaml.core.config.ConfigurationService; +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.saml2.core.AuthnRequest; +import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.impl.AuthnRequestBuilder; +import org.opensaml.saml.saml2.core.impl.IssuerBuilder; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.provider.service.authentication.OpenSamlSigningUtils.QueryParametersPartial; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A {@link Saml2AuthenticationRequestFactory} that generates, signs, and serializes a + * SAML 2.0 AuthnRequest using OpenSAML 4 + * + * @author Josh Cummings + * @since 5.5 + */ +public final class OpenSaml4AuthenticationRequestFactory implements Saml2AuthenticationRequestFactory { + + static { + OpenSamlInitializationService.initialize(); + } + + private final AuthnRequestBuilder authnRequestBuilder; + + private final IssuerBuilder issuerBuilder; + + private Clock clock = Clock.systemUTC(); + + private Converter authenticationRequestContextConverter; + + /** + * Creates an {@link OpenSaml4AuthenticationRequestFactory} + */ + public OpenSaml4AuthenticationRequestFactory() { + this.authenticationRequestContextConverter = this::createAuthnRequest; + XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class); + this.authnRequestBuilder = (AuthnRequestBuilder) registry.getBuilderFactory() + .getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME); + this.issuerBuilder = (IssuerBuilder) registry.getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME); + } + + /** + * {@inheritDoc} + */ + @Override + @Deprecated + public String createAuthenticationRequest(Saml2AuthenticationRequest request) { + RelyingPartyRegistration registration = RelyingPartyRegistration.withRegistrationId("noId") + .assertionConsumerServiceBinding(Saml2MessageBinding.POST) + .assertionConsumerServiceLocation(request.getAssertionConsumerServiceUrl()) + .entityId(request.getIssuer()).remoteIdpEntityId("noIssuer").idpWebSsoUrl("noUrl") + .credentials((credentials) -> credentials.addAll(request.getCredentials())).build(); + Saml2AuthenticationRequestContext context = Saml2AuthenticationRequestContext.builder() + .relyingPartyRegistration(registration).issuer(request.getIssuer()) + .assertionConsumerServiceUrl(request.getAssertionConsumerServiceUrl()).build(); + AuthnRequest authnRequest = this.authenticationRequestContextConverter.convert(context); + return OpenSamlSigningUtils.serialize(OpenSamlSigningUtils.sign(authnRequest, registration)); + } + + /** + * {@inheritDoc} + */ + @Override + public Saml2PostAuthenticationRequest createPostAuthenticationRequest(Saml2AuthenticationRequestContext context) { + AuthnRequest authnRequest = this.authenticationRequestContextConverter.convert(context); + RelyingPartyRegistration registration = context.getRelyingPartyRegistration(); + if (registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) { + OpenSamlSigningUtils.sign(authnRequest, registration); + } + String xml = OpenSamlSigningUtils.serialize(authnRequest); + return Saml2PostAuthenticationRequest.withAuthenticationRequestContext(context) + .samlRequest(Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8))).build(); + } + + /** + * {@inheritDoc} + */ + @Override + public Saml2RedirectAuthenticationRequest createRedirectAuthenticationRequest( + Saml2AuthenticationRequestContext context) { + AuthnRequest authnRequest = this.authenticationRequestContextConverter.convert(context); + RelyingPartyRegistration registration = context.getRelyingPartyRegistration(); + String xml = OpenSamlSigningUtils.serialize(authnRequest); + Saml2RedirectAuthenticationRequest.Builder result = Saml2RedirectAuthenticationRequest + .withAuthenticationRequestContext(context); + String deflatedAndEncoded = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(xml)); + result.samlRequest(deflatedAndEncoded).relayState(context.getRelayState()); + if (registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) { + QueryParametersPartial partial = OpenSamlSigningUtils.sign(registration).param("SAMLRequest", + deflatedAndEncoded); + if (StringUtils.hasText(context.getRelayState())) { + partial.param("RelayState", context.getRelayState()); + } + Map parameters = partial.parameters(); + return result.sigAlg(parameters.get("SigAlg")).signature(parameters.get("Signature")).build(); + } + return result.build(); + } + + private AuthnRequest createAuthnRequest(Saml2AuthenticationRequestContext context) { + String issuer = context.getIssuer(); + String destination = context.getDestination(); + String assertionConsumerServiceUrl = context.getAssertionConsumerServiceUrl(); + String protocolBinding = context.getRelyingPartyRegistration().getAssertionConsumerServiceBinding().getUrn(); + AuthnRequest auth = this.authnRequestBuilder.buildObject(); + if (auth.getID() == null) { + auth.setID("ARQ" + UUID.randomUUID().toString().substring(1)); + } + if (auth.getIssueInstant() == null) { + auth.setIssueInstant(Instant.now(this.clock)); + } + if (auth.isForceAuthn() == null) { + auth.setForceAuthn(Boolean.FALSE); + } + if (auth.isPassive() == null) { + auth.setIsPassive(Boolean.FALSE); + } + if (auth.getProtocolBinding() == null) { + auth.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI); + } + auth.setProtocolBinding(protocolBinding); + Issuer iss = this.issuerBuilder.buildObject(); + iss.setValue(issuer); + auth.setIssuer(iss); + auth.setDestination(destination); + auth.setAssertionConsumerServiceURL(assertionConsumerServiceUrl); + return auth; + } + + /** + * Set the strategy for building an {@link AuthnRequest} from a given context + * @param authenticationRequestContextConverter the conversion strategy to use + */ + public void setAuthenticationRequestContextConverter( + Converter authenticationRequestContextConverter) { + Assert.notNull(authenticationRequestContextConverter, "authenticationRequestContextConverter cannot be null"); + this.authenticationRequestContextConverter = authenticationRequestContextConverter; + } + + /** + * Use this {@link Clock} with {@link Instant#now()} for generating timestamps + * @param clock the {@link Clock} to use + */ + public void setClock(Clock clock) { + Assert.notNull(clock, "clock cannot be null"); + this.clock = clock; + } + +} diff --git a/saml2/saml2-service-provider/opensaml4/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java b/saml2/saml2-service-provider/opensaml4/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java new file mode 100644 index 0000000000..bc94bc3b2b --- /dev/null +++ b/saml2/saml2-service-provider/opensaml4/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java @@ -0,0 +1,661 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.authentication; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import javax.xml.namespace.QName; + +import net.shibboleth.utilities.java.support.xml.SerializeSupport; +import org.junit.Test; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.core.xml.io.Marshaller; +import org.opensaml.core.xml.io.MarshallingException; +import org.opensaml.core.xml.schema.XSDateTime; +import org.opensaml.core.xml.schema.impl.XSDateTimeBuilder; +import org.opensaml.saml.common.assertion.ValidationContext; +import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters; +import org.opensaml.saml.saml2.core.Assertion; +import org.opensaml.saml.saml2.core.Attribute; +import org.opensaml.saml.saml2.core.AttributeStatement; +import org.opensaml.saml.saml2.core.AttributeValue; +import org.opensaml.saml.saml2.core.Conditions; +import org.opensaml.saml.saml2.core.EncryptedAssertion; +import org.opensaml.saml.saml2.core.EncryptedAttribute; +import org.opensaml.saml.saml2.core.EncryptedID; +import org.opensaml.saml.saml2.core.NameID; +import org.opensaml.saml.saml2.core.OneTimeUse; +import org.opensaml.saml.saml2.core.Response; +import org.opensaml.saml.saml2.core.SubjectConfirmation; +import org.opensaml.saml.saml2.core.SubjectConfirmationData; +import org.opensaml.saml.saml2.core.impl.AttributeBuilder; +import org.opensaml.saml.saml2.core.impl.EncryptedAssertionBuilder; +import org.opensaml.saml.saml2.core.impl.EncryptedIDBuilder; +import org.opensaml.saml.saml2.core.impl.NameIDBuilder; +import org.opensaml.xmlsec.encryption.impl.EncryptedDataBuilder; +import org.opensaml.xmlsec.signature.support.SignatureConstants; +import org.w3c.dom.Element; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.core.Authentication; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.core.Saml2ErrorCodes; +import org.springframework.security.saml2.core.Saml2ResponseValidatorResult; +import org.springframework.security.saml2.core.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link OpenSaml4AuthenticationProvider} + * + * @author Filip Hanik + * @author Josh Cummings + */ +public class OpenSaml4AuthenticationProviderTests { + + private static String DESTINATION = "https://localhost/login/saml2/sso/idp-alias"; + + private static String RELYING_PARTY_ENTITY_ID = "https://localhost/saml2/service-provider-metadata/idp-alias"; + + private static String ASSERTING_PARTY_ENTITY_ID = "https://some.idp.test/saml2/idp"; + + private OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider(); + + private Saml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("name", + Collections.emptyMap()); + + private Saml2Authentication authentication = new Saml2Authentication(this.principal, "response", + Collections.emptyList()); + + @Test + public void supportsWhenSaml2AuthenticationTokenThenReturnTrue() { + assertThat(this.provider.supports(Saml2AuthenticationToken.class)) + .withFailMessage( + OpenSaml4AuthenticationProvider.class + "should support " + Saml2AuthenticationToken.class) + .isTrue(); + } + + @Test + public void supportsWhenNotSaml2AuthenticationTokenThenReturnFalse() { + assertThat(!this.provider.supports(Authentication.class)) + .withFailMessage(OpenSaml4AuthenticationProvider.class + "should not support " + Authentication.class) + .isTrue(); + } + + @Test + public void authenticateWhenUnknownDataClassThenThrowAuthenticationException() { + Assertion assertion = (Assertion) XMLObjectProviderRegistrySupport.getBuilderFactory() + .getBuilder(Assertion.DEFAULT_ELEMENT_NAME).buildObject(Assertion.DEFAULT_ELEMENT_NAME); + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> this.provider.authenticate( + new Saml2AuthenticationToken(verifying(registration()).build(), serialize(assertion)))) + .satisfies(errorOf(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA)); + } + + @Test + public void authenticateWhenXmlErrorThenThrowAuthenticationException() { + Saml2AuthenticationToken token = new Saml2AuthenticationToken(verifying(registration()).build(), "invalid xml"); + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> this.provider.authenticate(token)) + .satisfies(errorOf(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA)); + } + + @Test + public void authenticateWhenInvalidDestinationThenThrowAuthenticationException() { + Response response = response(DESTINATION + "invalid", ASSERTING_PARTY_ENTITY_ID); + response.getAssertions().add(assertion()); + TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), + RELYING_PARTY_ENTITY_ID); + Saml2AuthenticationToken token = token(response, verifying(registration())); + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> this.provider.authenticate(token)) + .satisfies(errorOf(Saml2ErrorCodes.INVALID_DESTINATION)); + } + + @Test + public void authenticateWhenNoAssertionsPresentThenThrowAuthenticationException() { + Saml2AuthenticationToken token = token(); + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> this.provider.authenticate(token)) + .satisfies(errorOf(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA, "No assertions found in response.")); + } + + @Test + public void authenticateWhenInvalidSignatureOnAssertionThenThrowAuthenticationException() { + Response response = response(); + response.getAssertions().add(assertion()); + Saml2AuthenticationToken token = token(response, verifying(registration())); + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> this.provider.authenticate(token)) + .satisfies(errorOf(Saml2ErrorCodes.INVALID_SIGNATURE)); + } + + @Test + public void authenticateWhenOpenSAMLValidationErrorThenThrowAuthenticationException() { + Response response = response(); + Assertion assertion = assertion(); + assertion.getSubject().getSubjectConfirmations().get(0).getSubjectConfirmationData() + .setNotOnOrAfter(Instant.now().minus(Duration.ofDays(3))); + TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), + RELYING_PARTY_ENTITY_ID); + response.getAssertions().add(assertion); + Saml2AuthenticationToken token = token(response, verifying(registration())); + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> this.provider.authenticate(token)) + .satisfies(errorOf(Saml2ErrorCodes.INVALID_ASSERTION)); + } + + @Test + public void authenticateWhenMissingSubjectThenThrowAuthenticationException() { + Response response = response(); + Assertion assertion = assertion(); + assertion.setSubject(null); + TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), + RELYING_PARTY_ENTITY_ID); + response.getAssertions().add(assertion); + Saml2AuthenticationToken token = token(response, verifying(registration())); + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> this.provider.authenticate(token)) + .satisfies(errorOf(Saml2ErrorCodes.SUBJECT_NOT_FOUND)); + } + + @Test + public void authenticateWhenUsernameMissingThenThrowAuthenticationException() { + Response response = response(); + Assertion assertion = assertion(); + assertion.getSubject().getNameID().setValue(null); + TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), + RELYING_PARTY_ENTITY_ID); + response.getAssertions().add(assertion); + Saml2AuthenticationToken token = token(response, verifying(registration())); + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> this.provider.authenticate(token)) + .satisfies(errorOf(Saml2ErrorCodes.SUBJECT_NOT_FOUND)); + } + + @Test + public void authenticateWhenAssertionContainsValidationAddressThenItSucceeds() { + Response response = response(); + Assertion assertion = assertion(); + assertion.getSubject().getSubjectConfirmations() + .forEach((sc) -> sc.getSubjectConfirmationData().setAddress("10.10.10.10")); + TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), + RELYING_PARTY_ENTITY_ID); + response.getAssertions().add(assertion); + Saml2AuthenticationToken token = token(response, verifying(registration())); + this.provider.authenticate(token); + } + + @Test + public void authenticateWhenAssertionContainsAttributesThenItSucceeds() { + Response response = response(); + Assertion assertion = assertion(); + List attributes = attributeStatements(); + assertion.getAttributeStatements().addAll(attributes); + TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), + RELYING_PARTY_ENTITY_ID); + response.getAssertions().add(assertion); + Saml2AuthenticationToken token = token(response, verifying(registration())); + Authentication authentication = this.provider.authenticate(token); + Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal(); + Map expected = new LinkedHashMap<>(); + expected.put("email", Arrays.asList("john.doe@example.com", "doe.john@example.com")); + expected.put("name", Collections.singletonList("John Doe")); + expected.put("age", Collections.singletonList(21)); + expected.put("website", Collections.singletonList("https://johndoe.com/")); + expected.put("registered", Collections.singletonList(true)); + Instant registeredDate = Instant.parse("1970-01-01T00:00:00Z"); + expected.put("registeredDate", Collections.singletonList(registeredDate)); + assertThat((String) principal.getFirstAttribute("name")).isEqualTo("John Doe"); + assertThat(principal.getAttributes()).isEqualTo(expected); + } + + @Test + public void authenticateWhenEncryptedAssertionWithoutSignatureThenItFails() { + Response response = response(); + EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(), + TestSaml2X509Credentials.assertingPartyEncryptingCredential()); + response.getEncryptedAssertions().add(encryptedAssertion); + TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), + RELYING_PARTY_ENTITY_ID); + Saml2AuthenticationToken token = token(response, decrypting(registration())); + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> this.provider.authenticate(token)) + .satisfies(errorOf(Saml2ErrorCodes.INVALID_SIGNATURE)); + } + + @Test + public void authenticateWhenEncryptedAssertionWithSignatureThenItSucceeds() { + Response response = response(); + Assertion assertion = TestOpenSamlObjects.signed(assertion(), + TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); + EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion, + TestSaml2X509Credentials.assertingPartyEncryptingCredential()); + response.getEncryptedAssertions().add(encryptedAssertion); + TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), + RELYING_PARTY_ENTITY_ID); + Saml2AuthenticationToken token = token(response, decrypting(verifying(registration()))); + this.provider.authenticate(token); + } + + @Test + public void authenticateWhenEncryptedAssertionWithResponseSignatureThenItSucceeds() { + Response response = response(); + EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(), + TestSaml2X509Credentials.assertingPartyEncryptingCredential()); + response.getEncryptedAssertions().add(encryptedAssertion); + TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), + RELYING_PARTY_ENTITY_ID); + Saml2AuthenticationToken token = token(response, decrypting(verifying(registration()))); + this.provider.authenticate(token); + } + + @Test + public void authenticateWhenEncryptedNameIdWithSignatureThenItSucceeds() { + Response response = response(); + Assertion assertion = assertion(); + NameID nameId = assertion.getSubject().getNameID(); + EncryptedID encryptedID = TestOpenSamlObjects.encrypted(nameId, + TestSaml2X509Credentials.assertingPartyEncryptingCredential()); + assertion.getSubject().setNameID(null); + assertion.getSubject().setEncryptedID(encryptedID); + response.getAssertions().add(assertion); + TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), + RELYING_PARTY_ENTITY_ID); + Saml2AuthenticationToken token = token(response, decrypting(verifying(registration()))); + this.provider.authenticate(token); + } + + @Test + public void authenticateWhenEncryptedAttributeThenDecrypts() { + Response response = response(); + Assertion assertion = assertion(); + EncryptedAttribute attribute = TestOpenSamlObjects.encrypted("name", "value", + TestSaml2X509Credentials.assertingPartyEncryptingCredential()); + AttributeStatement statement = build(AttributeStatement.DEFAULT_ELEMENT_NAME); + statement.getEncryptedAttributes().add(attribute); + assertion.getAttributeStatements().add(statement); + response.getAssertions().add(assertion); + TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), + RELYING_PARTY_ENTITY_ID); + Saml2AuthenticationToken token = token(response, decrypting(verifying(registration()))); + Saml2Authentication authentication = (Saml2Authentication) this.provider.authenticate(token); + Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal(); + assertThat(principal.getAttribute("name")).containsExactly("value"); + } + + @Test + public void authenticateWhenDecryptionKeysAreMissingThenThrowAuthenticationException() { + Response response = response(); + EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(), + TestSaml2X509Credentials.assertingPartyEncryptingCredential()); + response.getEncryptedAssertions().add(encryptedAssertion); + TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), + RELYING_PARTY_ENTITY_ID); + Saml2AuthenticationToken token = token(response, verifying(registration())); + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> this.provider.authenticate(token)) + .satisfies(errorOf(Saml2ErrorCodes.DECRYPTION_ERROR, "Failed to decrypt EncryptedData")); + } + + @Test + public void authenticateWhenDecryptionKeysAreWrongThenThrowAuthenticationException() { + Response response = response(); + EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion(), + TestSaml2X509Credentials.assertingPartyEncryptingCredential()); + response.getEncryptedAssertions().add(encryptedAssertion); + TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), + RELYING_PARTY_ENTITY_ID); + Saml2AuthenticationToken token = token(response, registration() + .decryptionX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartyPrivateCredential()))); + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> this.provider.authenticate(token)) + .satisfies(errorOf(Saml2ErrorCodes.DECRYPTION_ERROR, "Failed to decrypt EncryptedData")); + } + + @Test + public void writeObjectWhenTypeIsSaml2AuthenticationThenNoException() throws IOException { + Response response = response(); + Assertion assertion = TestOpenSamlObjects.signed(assertion(), + TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); + EncryptedAssertion encryptedAssertion = TestOpenSamlObjects.encrypted(assertion, + TestSaml2X509Credentials.assertingPartyEncryptingCredential()); + response.getEncryptedAssertions().add(encryptedAssertion); + TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), + RELYING_PARTY_ENTITY_ID); + Saml2AuthenticationToken token = token(response, decrypting(verifying(registration()))); + Saml2Authentication authentication = (Saml2Authentication) this.provider.authenticate(token); + // the following code will throw an exception if authentication isn't serializable + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(1024); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteStream); + objectOutputStream.writeObject(authentication); + objectOutputStream.flush(); + } + + @Test + public void createDefaultAssertionValidatorWhenAssertionThenValidates() { + Response response = TestOpenSamlObjects.signedResponseWithOneAssertion(); + Assertion assertion = response.getAssertions().get(0); + OpenSaml4AuthenticationProvider.AssertionToken assertionToken = new OpenSaml4AuthenticationProvider.AssertionToken( + assertion, token()); + assertThat( + OpenSaml4AuthenticationProvider.createDefaultAssertionValidator().convert(assertionToken).hasErrors()) + .isFalse(); + } + + @Test + public void authenticateWhenDelegatingToDefaultAssertionValidatorThenUses() { + OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider(); + // @formatter:off + provider.setAssertionValidator((assertionToken) -> OpenSaml4AuthenticationProvider + .createDefaultAssertionValidator((token) -> new ValidationContext()) + .convert(assertionToken) + .concat(new Saml2Error("wrong error", "wrong error")) + ); + // @formatter:on + Response response = response(); + Assertion assertion = assertion(); + OneTimeUse oneTimeUse = build(OneTimeUse.DEFAULT_ELEMENT_NAME); + assertion.getConditions().getConditions().add(oneTimeUse); + response.getAssertions().add(assertion); + TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), + ASSERTING_PARTY_ENTITY_ID); + Saml2AuthenticationToken token = token(response, verifying(registration())); + // @formatter:off + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> provider.authenticate(token)).isInstanceOf(Saml2AuthenticationException.class) + .satisfies((error) -> assertThat(error.getSaml2Error().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_ASSERTION)); + // @formatter:on + } + + @Test + public void authenticateWhenCustomAssertionValidatorThenUses() { + Converter validator = mock( + Converter.class); + OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider(); + // @formatter:off + provider.setAssertionValidator((assertionToken) -> OpenSaml4AuthenticationProvider.createDefaultAssertionValidator() + .convert(assertionToken) + .concat(validator.convert(assertionToken)) + ); + // @formatter:on + Response response = response(); + Assertion assertion = assertion(); + response.getAssertions().add(assertion); + TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), + ASSERTING_PARTY_ENTITY_ID); + Saml2AuthenticationToken token = token(response, verifying(registration())); + given(validator.convert(any(OpenSaml4AuthenticationProvider.AssertionToken.class))) + .willReturn(Saml2ResponseValidatorResult.success()); + provider.authenticate(token); + verify(validator).convert(any(OpenSaml4AuthenticationProvider.AssertionToken.class)); + } + + @Test + public void authenticateWhenDefaultConditionValidatorNotUsedThenSignatureStillChecked() { + OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider(); + provider.setAssertionValidator((assertionToken) -> Saml2ResponseValidatorResult.success()); + Response response = response(); + Assertion assertion = assertion(); + TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.relyingPartyDecryptingCredential(), + RELYING_PARTY_ENTITY_ID); // broken + // signature + response.getAssertions().add(assertion); + TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), + ASSERTING_PARTY_ENTITY_ID); + Saml2AuthenticationToken token = token(response, verifying(registration())); + // @formatter:off + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> provider.authenticate(token)) + .satisfies((error) -> assertThat(error.getSaml2Error().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_SIGNATURE)); + // @formatter:on + } + + @Test + public void authenticateWhenValidationContextCustomizedThenUsers() { + Map parameters = new HashMap<>(); + parameters.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, Collections.singleton("blah")); + ValidationContext context = mock(ValidationContext.class); + given(context.getStaticParameters()).willReturn(parameters); + OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider(); + provider.setAssertionValidator( + OpenSaml4AuthenticationProvider.createDefaultAssertionValidator((assertionToken) -> context)); + Response response = response(); + Assertion assertion = assertion(); + response.getAssertions().add(assertion); + TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), + ASSERTING_PARTY_ENTITY_ID); + Saml2AuthenticationToken token = token(response, verifying(registration())); + // @formatter:off + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> provider.authenticate(token)).isInstanceOf(Saml2AuthenticationException.class) + .satisfies((error) -> assertThat(error).hasMessageContaining("Invalid assertion")); + // @formatter:on + verify(context, atLeastOnce()).getStaticParameters(); + } + + @Test + public void authenticateWithSHA1SignatureThenItSucceeds() throws Exception { + Response response = response(); + Assertion assertion = TestOpenSamlObjects.signed(assertion(), + TestSaml2X509Credentials.assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID, + SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1); + response.getAssertions().add(assertion); + Saml2AuthenticationToken token = token(response, verifying(registration())); + this.provider.authenticate(token); + } + + @Test + public void setAssertionValidatorWhenNullThenIllegalArgument() { + // @formatter:off + assertThatIllegalArgumentException() + .isThrownBy(() -> this.provider.setAssertionValidator(null)); + // @formatter:on + } + + @Test + public void createDefaultResponseAuthenticationConverterWhenResponseThenConverts() { + Response response = TestOpenSamlObjects.signedResponseWithOneAssertion(); + Saml2AuthenticationToken token = token(response, verifying(registration())); + ResponseToken responseToken = new ResponseToken(response, token); + Saml2Authentication authentication = OpenSaml4AuthenticationProvider + .createDefaultResponseAuthenticationConverter().convert(responseToken); + assertThat(authentication.getName()).isEqualTo("test@saml.user"); + } + + @Test + public void authenticateWhenResponseAuthenticationConverterConfiguredThenUses() { + Converter authenticationConverter = mock(Converter.class); + OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider(); + provider.setResponseAuthenticationConverter(authenticationConverter); + Response response = TestOpenSamlObjects.signedResponseWithOneAssertion(); + Saml2AuthenticationToken token = token(response, verifying(registration())); + provider.authenticate(token); + verify(authenticationConverter).convert(any()); + } + + @Test + public void setResponseAuthenticationConverterWhenNullThenIllegalArgument() { + // @formatter:off + assertThatIllegalArgumentException() + .isThrownBy(() -> this.provider.setResponseAuthenticationConverter(null)); + // @formatter:on + } + + @Test + public void setResponseElementsDecrypterWhenNullThenIllegalArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> this.provider.setResponseElementsDecrypter(null)); + } + + @Test + public void setAssertionElementsDecrypterWhenNullThenIllegalArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> this.provider.setAssertionElementsDecrypter(null)); + } + + @Test + public void authenticateWhenCustomResponseElementsDecrypterThenDecryptsResponse() { + Response response = response(); + Assertion assertion = assertion(); + TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), + RELYING_PARTY_ENTITY_ID); + response.getEncryptedAssertions().add(new EncryptedAssertionBuilder().buildObject()); + TestOpenSamlObjects.signed(response, TestSaml2X509Credentials.assertingPartySigningCredential(), + RELYING_PARTY_ENTITY_ID); + Saml2AuthenticationToken token = token(response, verifying(registration())); + this.provider.setResponseElementsDecrypter((tuple) -> tuple.getResponse().getAssertions().add(assertion)); + Authentication authentication = this.provider.authenticate(token); + assertThat(authentication.getName()).isEqualTo("test@saml.user"); + } + + @Test + public void authenticateWhenCustomAssertionElementsDecrypterThenDecryptsAssertion() { + Response response = response(); + Assertion assertion = assertion(); + EncryptedID id = new EncryptedIDBuilder().buildObject(); + id.setEncryptedData(new EncryptedDataBuilder().buildObject()); + assertion.getSubject().setEncryptedID(id); + TestOpenSamlObjects.signed(assertion, TestSaml2X509Credentials.assertingPartySigningCredential(), + RELYING_PARTY_ENTITY_ID); + response.getAssertions().add(assertion); + Saml2AuthenticationToken token = token(response, verifying(registration())); + this.provider.setAssertionElementsDecrypter((tuple) -> { + NameID name = new NameIDBuilder().buildObject(); + name.setValue("decrypted name"); + tuple.getAssertion().getSubject().setNameID(name); + }); + Authentication authentication = this.provider.authenticate(token); + assertThat(authentication.getName()).isEqualTo("decrypted name"); + } + + private T build(QName qName) { + return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName); + } + + private String serialize(XMLObject object) { + try { + Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object); + Element element = marshaller.marshall(object); + return SerializeSupport.nodeToString(element); + } + catch (MarshallingException ex) { + throw new Saml2Exception(ex); + } + } + + private Consumer errorOf(String errorCode) { + return errorOf(errorCode, null); + } + + private Consumer errorOf(String errorCode, String description) { + return (ex) -> { + assertThat(ex.getSaml2Error().getErrorCode()).isEqualTo(errorCode); + if (StringUtils.hasText(description)) { + assertThat(ex.getSaml2Error().getDescription()).contains(description); + } + }; + } + + private Response response() { + Response response = TestOpenSamlObjects.response(); + response.setIssueInstant(Instant.now()); + return response; + } + + private Response response(String destination, String issuerEntityId) { + Response response = TestOpenSamlObjects.response(destination, issuerEntityId); + response.setIssueInstant(Instant.now()); + return response; + } + + private Assertion assertion() { + Assertion assertion = TestOpenSamlObjects.assertion(); + assertion.setIssueInstant(Instant.now()); + for (SubjectConfirmation confirmation : assertion.getSubject().getSubjectConfirmations()) { + SubjectConfirmationData data = confirmation.getSubjectConfirmationData(); + data.setNotBefore(Instant.now().minus(Duration.ofMillis(5 * 60 * 1000))); + data.setNotOnOrAfter(Instant.now().plus(Duration.ofMillis(5 * 60 * 1000))); + } + Conditions conditions = assertion.getConditions(); + conditions.setNotBefore(Instant.now().minus(Duration.ofMillis(5 * 60 * 1000))); + conditions.setNotOnOrAfter(Instant.now().plus(Duration.ofMillis(5 * 60 * 1000))); + return assertion; + } + + private List attributeStatements() { + List attributeStatements = TestOpenSamlObjects.attributeStatements(); + AttributeBuilder attributeBuilder = new AttributeBuilder(); + Attribute registeredDateAttr = attributeBuilder.buildObject(); + registeredDateAttr.setName("registeredDate"); + XSDateTime registeredDate = new XSDateTimeBuilder().buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, + XSDateTime.TYPE_NAME); + registeredDate.setValue(Instant.parse("1970-01-01T00:00:00Z")); + registeredDateAttr.getAttributeValues().add(registeredDate); + attributeStatements.iterator().next().getAttributes().add(registeredDateAttr); + return attributeStatements; + } + + private Saml2AuthenticationToken token() { + Response response = response(); + RelyingPartyRegistration registration = verifying(registration()).build(); + return new Saml2AuthenticationToken(registration, serialize(response)); + } + + private Saml2AuthenticationToken token(Response response, RelyingPartyRegistration.Builder registration) { + return new Saml2AuthenticationToken(registration.build(), serialize(response)); + } + + private RelyingPartyRegistration.Builder registration() { + return TestRelyingPartyRegistrations.noCredentials().entityId(RELYING_PARTY_ENTITY_ID) + .assertionConsumerServiceLocation(DESTINATION) + .assertingPartyDetails((party) -> party.entityId(ASSERTING_PARTY_ENTITY_ID)); + } + + private RelyingPartyRegistration.Builder verifying(RelyingPartyRegistration.Builder builder) { + return builder.assertingPartyDetails((party) -> party + .verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))); + } + + private RelyingPartyRegistration.Builder decrypting(RelyingPartyRegistration.Builder builder) { + return builder + .decryptionX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyDecryptingCredential())); + } + +} diff --git a/saml2/saml2-service-provider/opensaml4/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactoryTests.java b/saml2/saml2-service-provider/opensaml4/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactoryTests.java new file mode 100644 index 0000000000..0297d10f7f --- /dev/null +++ b/saml2/saml2-service-provider/opensaml4/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactoryTests.java @@ -0,0 +1,274 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.authentication; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.time.Instant; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.saml2.core.AuthnRequest; +import org.opensaml.saml.saml2.core.impl.AuthnRequestUnmarshaller; +import org.opensaml.xmlsec.signature.support.SignatureConstants; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.credentials.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link OpenSaml4AuthenticationRequestFactory} + */ +public class OpenSaml4AuthenticationRequestFactoryTests { + + private OpenSaml4AuthenticationRequestFactory factory; + + private Saml2AuthenticationRequestContext.Builder contextBuilder; + + private Saml2AuthenticationRequestContext context; + + private RelyingPartyRegistration.Builder relyingPartyRegistrationBuilder; + + private RelyingPartyRegistration relyingPartyRegistration; + + private AuthnRequestUnmarshaller unmarshaller; + + @Before + public void setUp() { + this.relyingPartyRegistrationBuilder = RelyingPartyRegistration.withRegistrationId("id") + .assertionConsumerServiceLocation("template") + .providerDetails((c) -> c.webSsoUrl("https://destination/sso")) + .providerDetails((c) -> c.entityId("remote-entity-id")).localEntityIdTemplate("local-entity-id") + .credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartySigningCredential())); + this.relyingPartyRegistration = this.relyingPartyRegistrationBuilder.build(); + this.contextBuilder = Saml2AuthenticationRequestContext.builder().issuer("https://issuer") + .relyingPartyRegistration(this.relyingPartyRegistration) + .assertionConsumerServiceUrl("https://issuer/sso"); + this.context = this.contextBuilder.build(); + this.factory = new OpenSaml4AuthenticationRequestFactory(); + this.unmarshaller = (AuthnRequestUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory() + .getUnmarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME); + } + + @Test + public void createAuthenticationRequestWhenInvokingDeprecatedMethodThenReturnsXML() { + Saml2AuthenticationRequest request = Saml2AuthenticationRequest.withAuthenticationRequestContext(this.context) + .build(); + String result = this.factory.createAuthenticationRequest(request); + assertThat(result.replace("\n", "")) + .startsWith(" c.signAuthNRequest(false)).build()) + .build(); + Saml2RedirectAuthenticationRequest result = this.factory.createRedirectAuthenticationRequest(this.context); + assertThat(result.getSamlRequest()).isNotEmpty(); + assertThat(result.getRelayState()).isEqualTo("Relay State Value"); + assertThat(result.getSigAlg()).isNull(); + assertThat(result.getSignature()).isNull(); + assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); + } + + @Test + public void createRedirectAuthenticationRequestWhenSignRequestThenSignatureIsPresent() { + this.context = this.contextBuilder.relayState("Relay State Value") + .relyingPartyRegistration(this.relyingPartyRegistration).build(); + Saml2RedirectAuthenticationRequest request = this.factory.createRedirectAuthenticationRequest(this.context); + assertThat(request.getRelayState()).isEqualTo("Relay State Value"); + assertThat(request.getSigAlg()).isEqualTo(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); + assertThat(request.getSignature()).isNotNull(); + } + + @Test + public void createRedirectAuthenticationRequestWhenSignRequestThenCredentialIsRequired() { + Saml2X509Credential credential = org.springframework.security.saml2.core.TestSaml2X509Credentials + .relyingPartyVerifyingCredential(); + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.noCredentials() + .assertingPartyDetails((party) -> party.verificationX509Credentials((c) -> c.add(credential))).build(); + this.context = this.contextBuilder.relayState("Relay State Value").relyingPartyRegistration(registration) + .build(); + assertThatExceptionOfType(Saml2Exception.class) + .isThrownBy(() -> this.factory.createPostAuthenticationRequest(this.context)); + } + + @Test + public void createPostAuthenticationRequestWhenNotSignRequestThenNoSignatureIsPresent() { + this.context = this.contextBuilder.relayState("Relay State Value") + .relyingPartyRegistration( + RelyingPartyRegistration.withRelyingPartyRegistration(this.relyingPartyRegistration) + .providerDetails((c) -> c.signAuthNRequest(false)).build()) + .build(); + Saml2PostAuthenticationRequest result = this.factory.createPostAuthenticationRequest(this.context); + assertThat(result.getSamlRequest()).isNotEmpty(); + assertThat(result.getRelayState()).isEqualTo("Relay State Value"); + assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(new String(Saml2Utils.samlDecode(result.getSamlRequest()), StandardCharsets.UTF_8)) + .doesNotContain("ds:Signature"); + } + + @Test + public void createPostAuthenticationRequestWhenSignRequestThenSignatureIsPresent() { + this.context = this.contextBuilder.relayState("Relay State Value") + .relyingPartyRegistration( + RelyingPartyRegistration.withRelyingPartyRegistration(this.relyingPartyRegistration).build()) + .build(); + Saml2PostAuthenticationRequest result = this.factory.createPostAuthenticationRequest(this.context); + assertThat(result.getSamlRequest()).isNotEmpty(); + assertThat(result.getRelayState()).isEqualTo("Relay State Value"); + assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(new String(Saml2Utils.samlDecode(result.getSamlRequest()), StandardCharsets.UTF_8)) + .contains("ds:Signature"); + } + + @Test + public void createPostAuthenticationRequestWhenSignRequestThenCredentialIsRequired() { + Saml2X509Credential credential = org.springframework.security.saml2.core.TestSaml2X509Credentials + .relyingPartyVerifyingCredential(); + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.noCredentials() + .assertingPartyDetails((party) -> party.verificationX509Credentials((c) -> c.add(credential))).build(); + this.context = this.contextBuilder.relayState("Relay State Value").relyingPartyRegistration(registration) + .build(); + assertThatExceptionOfType(Saml2Exception.class) + .isThrownBy(() -> this.factory.createPostAuthenticationRequest(this.context)); + } + + @Test + public void createAuthenticationRequestWhenDefaultThenReturnsPostBinding() { + AuthnRequest authn = getAuthNRequest(Saml2MessageBinding.POST); + Assert.assertEquals(SAMLConstants.SAML2_POST_BINDING_URI, authn.getProtocolBinding()); + } + + @Test + public void createPostAuthenticationRequestWhenAuthnRequestConsumerThenUses() { + Converter authenticationRequestContextConverter = mock( + Converter.class); + given(authenticationRequestContextConverter.convert(this.context)).willReturn(authnRequest()); + this.factory.setAuthenticationRequestContextConverter(authenticationRequestContextConverter); + + this.factory.createPostAuthenticationRequest(this.context); + verify(authenticationRequestContextConverter).convert(this.context); + } + + @Test + public void createRedirectAuthenticationRequestWhenAuthnRequestConsumerThenUses() { + Converter authenticationRequestContextConverter = mock( + Converter.class); + given(authenticationRequestContextConverter.convert(this.context)).willReturn(authnRequest()); + this.factory.setAuthenticationRequestContextConverter(authenticationRequestContextConverter); + + this.factory.createRedirectAuthenticationRequest(this.context); + verify(authenticationRequestContextConverter).convert(this.context); + } + + @Test + public void setAuthenticationRequestContextConverterWhenNullThenException() { + // @formatter:off + assertThatIllegalArgumentException() + .isThrownBy(() -> this.factory.setAuthenticationRequestContextConverter(null)); + // @formatter:on + } + + @Test + public void createPostAuthenticationRequestWhenAssertionConsumerServiceBindingThenUses() { + RelyingPartyRegistration relyingPartyRegistration = this.relyingPartyRegistrationBuilder + .assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT).build(); + Saml2AuthenticationRequestContext context = this.contextBuilder + .relyingPartyRegistration(relyingPartyRegistration).build(); + Saml2PostAuthenticationRequest request = this.factory.createPostAuthenticationRequest(context); + String samlRequest = request.getSamlRequest(); + String inflated = new String(Saml2Utils.samlDecode(samlRequest)); + assertThat(inflated).contains("ProtocolBinding=\"" + SAMLConstants.SAML2_REDIRECT_BINDING_URI + "\""); + } + + @Test + public void createRedirectAuthenticationRequestWhenSHA1SignRequestThenSignatureIsPresent() { + RelyingPartyRegistration relyingPartyRegistration = this.relyingPartyRegistrationBuilder + .assertingPartyDetails( + (a) -> a.signingAlgorithms((algs) -> algs.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1))) + .build(); + Saml2AuthenticationRequestContext context = this.contextBuilder.relayState("Relay State Value") + .relyingPartyRegistration(relyingPartyRegistration).build(); + Saml2RedirectAuthenticationRequest result = this.factory.createRedirectAuthenticationRequest(context); + assertThat(result.getSamlRequest()).isNotEmpty(); + assertThat(result.getRelayState()).isEqualTo("Relay State Value"); + assertThat(result.getSigAlg()).isEqualTo(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1); + assertThat(result.getSignature()).isNotNull(); + assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); + } + + private AuthnRequest authnRequest() { + AuthnRequest authnRequest = TestOpenSamlObjects.authnRequest(); + authnRequest.setIssueInstant(Instant.now()); + return authnRequest; + } + + private AuthnRequest getAuthNRequest(Saml2MessageBinding binding) { + AbstractSaml2AuthenticationRequest result = (binding == Saml2MessageBinding.REDIRECT) + ? this.factory.createRedirectAuthenticationRequest(this.context) + : this.factory.createPostAuthenticationRequest(this.context); + String samlRequest = result.getSamlRequest(); + assertThat(samlRequest).isNotEmpty(); + if (result.getBinding() == Saml2MessageBinding.REDIRECT) { + samlRequest = Saml2Utils.samlInflate(Saml2Utils.samlDecode(samlRequest)); + } + else { + samlRequest = new String(Saml2Utils.samlDecode(samlRequest), StandardCharsets.UTF_8); + } + try { + Document document = XMLObjectProviderRegistrySupport.getParserPool() + .parse(new ByteArrayInputStream(samlRequest.getBytes(StandardCharsets.UTF_8))); + Element element = document.getDocumentElement(); + return (AuthnRequest) this.unmarshaller.unmarshall(element); + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + +} diff --git a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle index c10aadf708..ee978368fa 100644 --- a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle +++ b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle @@ -1,14 +1,48 @@ apply plugin: 'io.spring.convention.spring-module' -dependencies { - compile project(':spring-security-core') - compile project(':spring-security-web') +build.dependsOn(project(":saml2-service-provider-core").tasks["build"]) +build.dependsOn(project(":saml2-service-provider-opensaml3").tasks["build"]) +build.dependsOn(project(":saml2-service-provider-opensaml4").tasks["build"]) - compile("org.opensaml:opensaml-core") - compile("org.opensaml:opensaml-saml-api") - compile("org.opensaml:opensaml-saml-impl") +check.dependsOn(project(":saml2-service-provider-core").tasks["check"]) +check.dependsOn(project(":saml2-service-provider-opensaml3").tasks["check"]) +check.dependsOn(project(":saml2-service-provider-opensaml4").tasks["check"]) - provided 'javax.servlet:javax.servlet-api' +test.dependsOn(project(":saml2-service-provider-core").tasks["test"]) +test.dependsOn(project(":saml2-service-provider-opensaml3").tasks["test"]) +test.dependsOn(project(":saml2-service-provider-opensaml4").tasks["test"]) - testCompile 'com.squareup.okhttp3:mockwebserver' +clean.dependsOn(project(":saml2-service-provider-core").tasks["clean"]) +clean.dependsOn(project(":saml2-service-provider-opensaml3").tasks["clean"]) +clean.dependsOn(project(":saml2-service-provider-opensaml4").tasks["clean"]) + +format.dependsOn(project(":saml2-service-provider-core").tasks["format"]) +format.dependsOn(project(":saml2-service-provider-opensaml3").tasks["format"]) +format.dependsOn(project(":saml2-service-provider-opensaml4").tasks["format"]) + +configurations { + core { + canBeConsumed = false + canBeResolved = true + } + opensaml3 { + canBeConsumed = false + canBeResolved = true + } + opensaml4 { + canBeConsumed = false + canBeResolved = true + } +} + +dependencies { + core(project(path: ":saml2-service-provider-core", configuration: 'classesOnlyElements')) + opensaml3(project(path: ":saml2-service-provider-opensaml3", configuration: 'classesOnlyElements')) + opensaml4(project(path: ":saml2-service-provider-opensaml4", configuration: 'classesOnlyElements')) +} + +jar { + from configurations.core + from configurations.opensaml3 + from configurations.opensaml4 } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java deleted file mode 100644 index 26bfd1a848..0000000000 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Copyright 2002-2020 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.saml2.provider.service.authentication; - -import java.nio.charset.StandardCharsets; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import java.time.Clock; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import net.shibboleth.utilities.java.support.resolver.CriteriaSet; -import net.shibboleth.utilities.java.support.xml.SerializeSupport; -import org.joda.time.DateTime; -import org.opensaml.core.config.ConfigurationService; -import org.opensaml.core.xml.config.XMLObjectProviderRegistry; -import org.opensaml.core.xml.io.MarshallingException; -import org.opensaml.saml.common.xml.SAMLConstants; -import org.opensaml.saml.saml2.core.AuthnRequest; -import org.opensaml.saml.saml2.core.Issuer; -import org.opensaml.saml.saml2.core.impl.AuthnRequestBuilder; -import org.opensaml.saml.saml2.core.impl.AuthnRequestMarshaller; -import org.opensaml.saml.saml2.core.impl.IssuerBuilder; -import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver; -import org.opensaml.security.SecurityException; -import org.opensaml.security.credential.BasicCredential; -import org.opensaml.security.credential.Credential; -import org.opensaml.security.credential.CredentialSupport; -import org.opensaml.security.credential.UsageType; -import org.opensaml.xmlsec.SignatureSigningParameters; -import org.opensaml.xmlsec.SignatureSigningParametersResolver; -import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion; -import org.opensaml.xmlsec.crypto.XMLSigningUtil; -import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration; -import org.opensaml.xmlsec.signature.support.SignatureConstants; -import org.opensaml.xmlsec.signature.support.SignatureSupport; -import org.w3c.dom.Element; - -import org.springframework.core.convert.converter.Converter; -import org.springframework.security.saml2.Saml2Exception; -import org.springframework.security.saml2.core.OpenSamlInitializationService; -import org.springframework.security.saml2.core.Saml2X509Credential; -import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest.Builder; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; -import org.springframework.web.util.UriComponentsBuilder; -import org.springframework.web.util.UriUtils; - -/** - * @since 5.2 - */ -public class OpenSamlAuthenticationRequestFactory implements Saml2AuthenticationRequestFactory { - - static { - OpenSamlInitializationService.initialize(); - } - - private Clock clock = Clock.systemUTC(); - - private AuthnRequestMarshaller marshaller; - - private AuthnRequestBuilder authnRequestBuilder; - - private IssuerBuilder issuerBuilder; - - private Converter protocolBindingResolver = (context) -> { - if (context == null) { - return SAMLConstants.SAML2_POST_BINDING_URI; - } - return context.getRelyingPartyRegistration().getAssertionConsumerServiceBinding().getUrn(); - }; - - private Converter authenticationRequestContextConverter = this::createAuthnRequest; - - /** - * Creates an {@link OpenSamlAuthenticationRequestFactory} - */ - public OpenSamlAuthenticationRequestFactory() { - XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class); - this.marshaller = (AuthnRequestMarshaller) registry.getMarshallerFactory() - .getMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME); - this.authnRequestBuilder = (AuthnRequestBuilder) registry.getBuilderFactory() - .getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME); - this.issuerBuilder = (IssuerBuilder) registry.getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME); - } - - @Override - @Deprecated - public String createAuthenticationRequest(Saml2AuthenticationRequest request) { - AuthnRequest authnRequest = createAuthnRequest(request.getIssuer(), request.getDestination(), - request.getAssertionConsumerServiceUrl(), this.protocolBindingResolver.convert(null)); - for (org.springframework.security.saml2.credentials.Saml2X509Credential credential : request.getCredentials()) { - if (credential.isSigningCredential()) { - X509Certificate certificate = credential.getCertificate(); - PrivateKey privateKey = credential.getPrivateKey(); - BasicCredential cred = CredentialSupport.getSimpleCredential(certificate, privateKey); - cred.setEntityId(request.getIssuer()); - cred.setUsageType(UsageType.SIGNING); - SignatureSigningParameters parameters = new SignatureSigningParameters(); - parameters.setSigningCredential(cred); - parameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); - parameters.setSignatureReferenceDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA256); - parameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); - return serialize(sign(authnRequest, parameters)); - } - } - throw new IllegalArgumentException("No signing credential provided"); - } - - @Override - public Saml2PostAuthenticationRequest createPostAuthenticationRequest(Saml2AuthenticationRequestContext context) { - AuthnRequest authnRequest = this.authenticationRequestContextConverter.convert(context); - String xml = context.getRelyingPartyRegistration().getAssertingPartyDetails().getWantAuthnRequestsSigned() - ? serialize(sign(authnRequest, context.getRelyingPartyRegistration())) : serialize(authnRequest); - - return Saml2PostAuthenticationRequest.withAuthenticationRequestContext(context) - .samlRequest(Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8))).build(); - } - - @Override - public Saml2RedirectAuthenticationRequest createRedirectAuthenticationRequest( - Saml2AuthenticationRequestContext context) { - AuthnRequest authnRequest = this.authenticationRequestContextConverter.convert(context); - String xml = serialize(authnRequest); - Builder result = Saml2RedirectAuthenticationRequest.withAuthenticationRequestContext(context); - String deflatedAndEncoded = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(xml)); - result.samlRequest(deflatedAndEncoded).relayState(context.getRelayState()); - if (context.getRelyingPartyRegistration().getAssertingPartyDetails().getWantAuthnRequestsSigned()) { - Map parameters = new LinkedHashMap<>(); - parameters.put("SAMLRequest", deflatedAndEncoded); - if (StringUtils.hasText(context.getRelayState())) { - parameters.put("RelayState", context.getRelayState()); - } - sign(parameters, context.getRelyingPartyRegistration()); - return result.sigAlg(parameters.get("SigAlg")).signature(parameters.get("Signature")).build(); - } - return result.build(); - } - - private AuthnRequest createAuthnRequest(Saml2AuthenticationRequestContext context) { - return createAuthnRequest(context.getIssuer(), context.getDestination(), - context.getAssertionConsumerServiceUrl(), this.protocolBindingResolver.convert(context)); - } - - private AuthnRequest createAuthnRequest(String issuer, String destination, String assertionConsumerServiceUrl, - String protocolBinding) { - AuthnRequest auth = this.authnRequestBuilder.buildObject(); - auth.setID("ARQ" + UUID.randomUUID().toString().substring(1)); - auth.setIssueInstant(new DateTime(this.clock.millis())); - auth.setForceAuthn(Boolean.FALSE); - auth.setIsPassive(Boolean.FALSE); - auth.setProtocolBinding(protocolBinding); - Issuer iss = this.issuerBuilder.buildObject(); - iss.setValue(issuer); - auth.setIssuer(iss); - auth.setDestination(destination); - auth.setAssertionConsumerServiceURL(assertionConsumerServiceUrl); - return auth; - } - - /** - * Set the {@link AuthnRequest} post-processor resolver - * @param authenticationRequestContextConverter - * @since 5.4 - */ - public void setAuthenticationRequestContextConverter( - Converter authenticationRequestContextConverter) { - Assert.notNull(authenticationRequestContextConverter, "authenticationRequestContextConverter cannot be null"); - this.authenticationRequestContextConverter = authenticationRequestContextConverter; - } - - /** - * ' Use this {@link Clock} with {@link Instant#now()} for generating timestamps - * @param clock - */ - public void setClock(Clock clock) { - Assert.notNull(clock, "clock cannot be null"); - this.clock = clock; - } - - /** - * Sets the {@code protocolBinding} to use when generating authentication requests. - * Acceptable values are {@link SAMLConstants#SAML2_POST_BINDING_URI} and - * {@link SAMLConstants#SAML2_REDIRECT_BINDING_URI} The IDP will be reading this value - * in the {@code AuthNRequest} to determine how to send the Response/Assertion to the - * ACS URL, assertion consumer service URL. - * @param protocolBinding either {@link SAMLConstants#SAML2_POST_BINDING_URI} or - * {@link SAMLConstants#SAML2_REDIRECT_BINDING_URI} - * @throws IllegalArgumentException if the protocolBinding is not valid - * @deprecated Use - * {@link org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.Builder#assertionConsumerServiceBinding(Saml2MessageBinding)} - * instead - */ - @Deprecated - public void setProtocolBinding(String protocolBinding) { - boolean isAllowedBinding = SAMLConstants.SAML2_POST_BINDING_URI.equals(protocolBinding) - || SAMLConstants.SAML2_REDIRECT_BINDING_URI.equals(protocolBinding); - if (!isAllowedBinding) { - throw new IllegalArgumentException("Invalid protocol binding: " + protocolBinding); - } - this.protocolBindingResolver = (context) -> protocolBinding; - } - - private AuthnRequest sign(AuthnRequest authnRequest, RelyingPartyRegistration relyingPartyRegistration) { - SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration); - return sign(authnRequest, parameters); - } - - private AuthnRequest sign(AuthnRequest authnRequest, SignatureSigningParameters parameters) { - try { - SignatureSupport.signObject(authnRequest, parameters); - return authnRequest; - } - catch (Exception ex) { - throw new Saml2Exception(ex); - } - } - - private void sign(Map components, RelyingPartyRegistration relyingPartyRegistration) { - SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration); - sign(components, parameters); - } - - private void sign(Map components, SignatureSigningParameters parameters) { - Credential credential = parameters.getSigningCredential(); - String algorithmUri = parameters.getSignatureAlgorithm(); - components.put("SigAlg", algorithmUri); - UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); - for (Map.Entry component : components.entrySet()) { - builder.queryParam(component.getKey(), UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1)); - } - String queryString = builder.build(true).toString().substring(1); - try { - byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri, - queryString.getBytes(StandardCharsets.UTF_8)); - String b64Signature = Saml2Utils.samlEncode(rawSignature); - components.put("Signature", b64Signature); - } - catch (SecurityException ex) { - throw new Saml2Exception(ex); - } - } - - private String serialize(AuthnRequest authnRequest) { - try { - Element element = this.marshaller.marshall(authnRequest); - return SerializeSupport.nodeToString(element); - } - catch (MarshallingException ex) { - throw new Saml2Exception(ex); - } - } - - private SignatureSigningParameters resolveSigningParameters(RelyingPartyRegistration relyingPartyRegistration) { - List credentials = resolveSigningCredentials(relyingPartyRegistration); - List algorithms = relyingPartyRegistration.getAssertingPartyDetails().getSigningAlgorithms(); - List digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256); - String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS; - SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver(); - CriteriaSet criteria = new CriteriaSet(); - BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration(); - signingConfiguration.setSigningCredentials(credentials); - signingConfiguration.setSignatureAlgorithms(algorithms); - signingConfiguration.setSignatureReferenceDigestMethods(digests); - signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization); - criteria.add(new SignatureSigningConfigurationCriterion(signingConfiguration)); - try { - SignatureSigningParameters parameters = resolver.resolveSingle(criteria); - Assert.notNull(parameters, "Failed to resolve any signing credential"); - return parameters; - } - catch (Exception ex) { - throw new Saml2Exception(ex); - } - } - - private List resolveSigningCredentials(RelyingPartyRegistration relyingPartyRegistration) { - List credentials = new ArrayList<>(); - for (Saml2X509Credential x509Credential : relyingPartyRegistration.getSigningX509Credentials()) { - X509Certificate certificate = x509Credential.getCertificate(); - PrivateKey privateKey = x509Credential.getPrivateKey(); - BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey); - credential.setEntityId(relyingPartyRegistration.getEntityId()); - credential.setUsageType(UsageType.SIGNING); - credentials.add(credential); - } - return credentials; - } - -} diff --git a/samples/boot/saml2login/spring-security-samples-boot-saml2login.gradle b/samples/boot/saml2login/spring-security-samples-boot-saml2login.gradle index f46ce4a8d9..ee78e3f199 100644 --- a/samples/boot/saml2login/spring-security-samples-boot-saml2login.gradle +++ b/samples/boot/saml2login/spring-security-samples-boot-saml2login.gradle @@ -16,9 +16,15 @@ apply plugin: 'io.spring.convention.spring-sample-boot' +sourceCompatibility = '11' + +repositories { + maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" } +} + dependencies { compile project(':spring-security-config') - compile project(':spring-security-saml2-service-provider') + compile project(':saml2-service-provider-opensaml4') compile 'org.springframework.boot:spring-boot-starter-thymeleaf' compile 'org.springframework.boot:spring-boot-starter-web' compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5' diff --git a/samples/javaconfig/saml2login/spring-security-samples-javaconfig-saml2login.gradle b/samples/javaconfig/saml2login/spring-security-samples-javaconfig-saml2login.gradle index e1d82499dc..beccb62a10 100644 --- a/samples/javaconfig/saml2login/spring-security-samples-javaconfig-saml2login.gradle +++ b/samples/javaconfig/saml2login/spring-security-samples-javaconfig-saml2login.gradle @@ -1,7 +1,11 @@ apply plugin: 'io.spring.convention.spring-sample-war' +repositories { + maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" } +} + dependencies { - compile project(':spring-security-saml2-service-provider') + compile project(':saml2-service-provider-opensaml3') compile project(':spring-security-config') compile slf4jDependencies