diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyProperties.java index eae0fc30c5..4fdb677d34 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-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. @@ -23,6 +23,7 @@ import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.core.io.Resource; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; /** * SAML2 relying party properties. @@ -124,6 +125,8 @@ public class Saml2RelyingPartyProperties { */ private String ssoUrl; + private SingleSignOn singleSignOn = new SingleSignOn(); + private Verification verification = new Verification(); public String getEntityId() { @@ -134,18 +137,67 @@ public class Saml2RelyingPartyProperties { this.entityId = entityId; } + @Deprecated public String getSsoUrl() { - return this.ssoUrl; + return this.getSingleSignOn().getUrl(); } + @Deprecated public void setSsoUrl(String ssoUrl) { - this.ssoUrl = ssoUrl; + this.singleSignOn.setUrl(ssoUrl); + } + + public SingleSignOn getSingleSignOn() { + return this.singleSignOn; } public Verification getVerification() { return this.verification; } + public static class SingleSignOn { + + /** + * Remote endpoint to send authentication requests to. + */ + private String url; + + /** + * Whether to redirect or post authentication requests. + */ + private Saml2MessageBinding binding = Saml2MessageBinding.REDIRECT; + + /** + * Whether to sign authentication requests. + */ + private boolean signRequest = true; + + public String getUrl() { + return this.url; + } + + public void setUrl(String url) { + this.url = url; + } + + public Saml2MessageBinding getBinding() { + return this.binding; + } + + public void setBinding(Saml2MessageBinding binding) { + this.binding = binding; + } + + public boolean isSignRequest() { + return this.signRequest; + } + + public void setSignRequest(boolean signRequest) { + this.signRequest = signRequest; + } + + } + public static class Verification { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyRegistrationConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyRegistrationConfiguration.java index ae99b09580..a9d019b2c3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyRegistrationConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyRegistrationConfiguration.java @@ -66,15 +66,28 @@ class Saml2RelyingPartyRegistrationConfiguration { } private RelyingPartyRegistration asRegistration(String id, Registration properties) { + boolean signRequest = properties.getIdentityprovider().getSingleSignOn().isSignRequest(); + validateSigningCredentials(properties, signRequest); RelyingPartyRegistration.Builder builder = RelyingPartyRegistration.withRegistrationId(id); builder.assertionConsumerServiceUrlTemplate( "{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI); - builder.providerDetails((details) -> details.webSsoUrl(properties.getIdentityprovider().getSsoUrl())); + builder.providerDetails( + (details) -> details.webSsoUrl(properties.getIdentityprovider().getSingleSignOn().getUrl())); builder.providerDetails((details) -> details.entityId(properties.getIdentityprovider().getEntityId())); + builder.providerDetails( + (details) -> details.binding(properties.getIdentityprovider().getSingleSignOn().getBinding())); + builder.providerDetails((details) -> details.signAuthNRequest(signRequest)); builder.credentials((credentials) -> credentials.addAll(asCredentials(properties))); return builder.build(); } + private void validateSigningCredentials(Registration properties, boolean signRequest) { + if (signRequest) { + Assert.state(!properties.getSigning().getCredentials().isEmpty(), + "Signing credentials must not be empty when authentication requests require signing."); + } + } + private List asCredentials(Registration properties) { List credentials = new ArrayList<>(); properties.getSigning().getCredentials().stream().map(this::asSigningCredential).forEach(credentials::add); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyAutoConfigurationTests.java index 5ed7c6a365..ab952120db 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyAutoConfigurationTests.java @@ -34,6 +34,7 @@ import org.springframework.security.config.BeanIds; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.SecurityFilterChain; @@ -85,11 +86,28 @@ public class Saml2RelyingPartyAutoConfigurationTests { .isEqualTo("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php"); assertThat(registration.getAssertionConsumerServiceUrlTemplate()) .isEqualTo("{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI); + assertThat(registration.getProviderDetails().getBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(registration.getProviderDetails().isSignAuthNRequest()).isEqualTo(false); assertThat(registration.getSigningCredentials()).isNotNull(); assertThat(registration.getVerificationCredentials()).isNotNull(); }); } + @Test + void autoConfigurationWhenSignRequestsTrueAndNoSigningCredentialsShouldThrowException() { + this.contextRunner.withPropertyValues(getPropertyValuesWithoutSigningCredentials(true)).run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).hasMessageContaining( + "Signing credentials must not be empty when authentication requests require signing."); + }); + } + + @Test + void autoConfigurationWhenSignRequestsFalseAndNoSigningCredentialsShouldNotThrowException() { + this.contextRunner.withPropertyValues(getPropertyValuesWithoutSigningCredentials(false)) + .run((context) -> assertThat(context).hasSingleBean(RelyingPartyRegistrationRepository.class)); + } + @Test void relyingPartyRegistrationRepositoryShouldBeConditionalOnMissingBean() { this.contextRunner.withPropertyValues(getPropertyValues()) @@ -112,11 +130,22 @@ public class Saml2RelyingPartyAutoConfigurationTests { .run((context) -> assertThat(hasFilter(context, Saml2WebSsoAuthenticationFilter.class)).isFalse()); } + private String[] getPropertyValuesWithoutSigningCredentials(boolean signRequests) { + return new String[] { PREFIX + + ".foo.identityprovider.single-sign-on.url=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php", + PREFIX + ".foo.identityprovider.single-sign-on.binding=post", + PREFIX + ".foo.identityprovider.single-sign-on.sign-request=" + signRequests, + PREFIX + ".foo.identityprovider.entity-id=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php", + PREFIX + ".foo.identityprovider.verification.credentials[0].certificate-location=classpath:saml/certificate-location" }; + } + private String[] getPropertyValues() { return new String[] { PREFIX + ".foo.signing.credentials[0].private-key-location=classpath:saml/private-key-location", PREFIX + ".foo.signing.credentials[0].certificate-location=classpath:saml/certificate-location", - PREFIX + ".foo.identityprovider.sso-url=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php", + PREFIX + ".foo.identityprovider.single-sign-on.url=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php", + PREFIX + ".foo.identityprovider.single-sign-on.binding=post", + PREFIX + ".foo.identityprovider.single-sign-on.sign-request=false", PREFIX + ".foo.identityprovider.entity-id=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php", PREFIX + ".foo.identityprovider.verification.credentials[0].certificate-location=classpath:saml/certificate-location" }; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyPropertiesTests.java new file mode 100644 index 0000000000..41429840b0 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyPropertiesTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-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.boot.autoconfigure.security.saml2; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Saml2RelyingPartyProperties}. + * + * @author Madhura Bhave + */ +class Saml2RelyingPartyPropertiesTests { + + private final Saml2RelyingPartyProperties properties = new Saml2RelyingPartyProperties(); + + @Deprecated + @Test + void customizeSsoUrlDeprecated() { + bind("spring.security.saml2.relyingparty.registration.simplesamlphp.identity-provider.single-sign-on.url", + "https://simplesaml-for-spring-saml/SSOService.php"); + assertThat( + this.properties.getRegistration().get("simplesamlphp").getIdentityprovider().getSingleSignOn().getUrl()) + .isEqualTo("https://simplesaml-for-spring-saml/SSOService.php"); + } + + @Test + void customizeSsoUrl() { + bind("spring.security.saml2.relyingparty.registration.simplesamlphp.identity-provider.single-sign-on.url", + "https://simplesaml-for-spring-saml/SSOService.php"); + assertThat( + this.properties.getRegistration().get("simplesamlphp").getIdentityprovider().getSingleSignOn().getUrl()) + .isEqualTo("https://simplesaml-for-spring-saml/SSOService.php"); + } + + @Test + void customizeSsoBindingDefaultsToRedirect() { + this.properties.getRegistration().put("simplesamlphp", new Saml2RelyingPartyProperties.Registration()); + assertThat(this.properties.getRegistration().get("simplesamlphp").getIdentityprovider().getSingleSignOn() + .getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); + } + + @Test + void customizeSsoBinding() { + bind("spring.security.saml2.relyingparty.registration.simplesamlphp.identity-provider.single-sign-on.binding", + "post"); + assertThat(this.properties.getRegistration().get("simplesamlphp").getIdentityprovider().getSingleSignOn() + .getBinding()).isEqualTo(Saml2MessageBinding.POST); + } + + @Test + void customizeSsoSignRequests() { + bind("spring.security.saml2.relyingparty.registration.simplesamlphp.identity-provider.single-sign-on.sign-request", + "false"); + assertThat(this.properties.getRegistration().get("simplesamlphp").getIdentityprovider().getSingleSignOn() + .isSignRequest()).isEqualTo(false); + } + + @Test + void customizeSsoSignRequestsIsTrueByDefault() { + this.properties.getRegistration().put("simplesamlphp", new Saml2RelyingPartyProperties.Registration()); + assertThat(this.properties.getRegistration().get("simplesamlphp").getIdentityprovider().getSingleSignOn() + .isSignRequest()).isEqualTo(true); + } + + private void bind(String name, String value) { + bind(Collections.singletonMap(name, value)); + } + + private void bind(Map map) { + ConfigurationPropertySource source = new MapConfigurationPropertySource(map); + new Binder(source).bind("spring.security.saml2.relyingparty", Bindable.ofInstance(this.properties)); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-saml2-service-provider/src/main/resources/application.yml b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-saml2-service-provider/src/main/resources/application.yml index 98303152f1..438cb6f372 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-saml2-service-provider/src/main/resources/application.yml +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-saml2-service-provider/src/main/resources/application.yml @@ -13,7 +13,8 @@ spring: credentials: - certificate-location: "classpath:saml/certificate.txt" entity-id: simplesaml - sso-url: https://simplesaml-for-spring-saml/SSOService.php + single-sign-on: + url: https://simplesaml-for-spring-saml/SSOService.php okta: signing: credentials: @@ -24,4 +25,6 @@ spring: credentials: - certificate-location: "classpath:saml/certificate.txt" entity-id: okta-id-1234 - sso-url: https://okta-for-spring/saml2/idp/SSOService.php + single-sign-on: + url: + https://okta-for-spring/saml2/idp/SSOService.php