Configure SAML 2.0 Service Provider via Metadata
See gh-23045
This commit is contained in:
		
							parent
							
								
									bd9928cc31
								
							
						
					
					
						commit
						5187c01e39
					
				| 
						 | 
					@ -140,6 +140,11 @@ public class Saml2RelyingPartyProperties {
 | 
				
			||||||
		 */
 | 
							 */
 | 
				
			||||||
		private String entityId;
 | 
							private String entityId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							/**
 | 
				
			||||||
 | 
							 * Endpoint for discovery-based configuration.
 | 
				
			||||||
 | 
							 */
 | 
				
			||||||
 | 
							private String metadataUri;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		private final Singlesignon singlesignon = new Singlesignon();
 | 
							private final Singlesignon singlesignon = new Singlesignon();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		private final Verification verification = new Verification();
 | 
							private final Verification verification = new Verification();
 | 
				
			||||||
| 
						 | 
					@ -152,6 +157,14 @@ public class Saml2RelyingPartyProperties {
 | 
				
			||||||
			this.entityId = entityId;
 | 
								this.entityId = entityId;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							public String getMetadataUri() {
 | 
				
			||||||
 | 
								return this.metadataUri;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							public void setMetadataUri(String metadataUri) {
 | 
				
			||||||
 | 
								this.metadataUri = metadataUri;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		@Deprecated
 | 
							@Deprecated
 | 
				
			||||||
		@DeprecatedConfigurationProperty(reason = "moved to 'singlesignon.url'")
 | 
							@DeprecatedConfigurationProperty(reason = "moved to 'singlesignon.url'")
 | 
				
			||||||
		public String getSsoUrl() {
 | 
							public String getSsoUrl() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,6 +28,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
 | 
				
			||||||
import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyProperties.Identityprovider.Verification;
 | 
					import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyProperties.Identityprovider.Verification;
 | 
				
			||||||
import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyProperties.Registration;
 | 
					import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyProperties.Registration;
 | 
				
			||||||
import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyProperties.Registration.Signing;
 | 
					import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyProperties.Registration.Signing;
 | 
				
			||||||
 | 
					import org.springframework.boot.context.properties.PropertyMapper;
 | 
				
			||||||
import org.springframework.context.annotation.Bean;
 | 
					import org.springframework.context.annotation.Bean;
 | 
				
			||||||
import org.springframework.context.annotation.Conditional;
 | 
					import org.springframework.context.annotation.Conditional;
 | 
				
			||||||
import org.springframework.context.annotation.Configuration;
 | 
					import org.springframework.context.annotation.Configuration;
 | 
				
			||||||
| 
						 | 
					@ -37,8 +38,10 @@ import org.springframework.security.saml2.core.Saml2X509Credential;
 | 
				
			||||||
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
 | 
					import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
 | 
				
			||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
 | 
					import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
 | 
				
			||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
 | 
					import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
 | 
				
			||||||
 | 
					import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations;
 | 
				
			||||||
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
 | 
					import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
 | 
				
			||||||
import org.springframework.util.Assert;
 | 
					import org.springframework.util.Assert;
 | 
				
			||||||
 | 
					import org.springframework.util.StringUtils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * {@link Configuration @Configuration} used to map {@link Saml2RelyingPartyProperties} to
 | 
					 * {@link Configuration @Configuration} used to map {@link Saml2RelyingPartyProperties} to
 | 
				
			||||||
| 
						 | 
					@ -64,16 +67,25 @@ class Saml2RelyingPartyRegistrationConfiguration {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private RelyingPartyRegistration asRegistration(String id, Registration properties) {
 | 
						private RelyingPartyRegistration asRegistration(String id, Registration properties) {
 | 
				
			||||||
		boolean signRequest = properties.getIdentityprovider().getSinglesignon().isSignRequest();
 | 
							RelyingPartyRegistration.Builder builder;
 | 
				
			||||||
		validateSigningCredentials(properties, signRequest);
 | 
							boolean usingMetadata = StringUtils.hasText(properties.getIdentityprovider().getMetadataUri());
 | 
				
			||||||
		RelyingPartyRegistration.Builder builder = RelyingPartyRegistration.withRegistrationId(id);
 | 
							if (usingMetadata) {
 | 
				
			||||||
 | 
								builder = RelyingPartyRegistrations.fromMetadataLocation(properties.getIdentityprovider().getMetadataUri())
 | 
				
			||||||
 | 
										.registrationId(id);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							else {
 | 
				
			||||||
 | 
								builder = RelyingPartyRegistration.withRegistrationId(id);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		builder.assertionConsumerServiceLocation(
 | 
							builder.assertionConsumerServiceLocation(
 | 
				
			||||||
				"{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI);
 | 
									"{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI);
 | 
				
			||||||
 | 
							Saml2RelyingPartyProperties.Identityprovider identityprovider = properties.getIdentityprovider();
 | 
				
			||||||
 | 
							PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
 | 
				
			||||||
		builder.assertingPartyDetails((details) -> {
 | 
							builder.assertingPartyDetails((details) -> {
 | 
				
			||||||
			details.singleSignOnServiceLocation(properties.getIdentityprovider().getSinglesignon().getUrl());
 | 
								map.from(identityprovider::getEntityId).to(details::entityId);
 | 
				
			||||||
			details.entityId(properties.getIdentityprovider().getEntityId());
 | 
								map.from(identityprovider.getSinglesignon()::getBinding).to(details::singleSignOnServiceBinding);
 | 
				
			||||||
			details.singleSignOnServiceBinding(properties.getIdentityprovider().getSinglesignon().getBinding());
 | 
								map.from(identityprovider.getSinglesignon()::getUrl).to(details::singleSignOnServiceLocation);
 | 
				
			||||||
			details.wantAuthnRequestsSigned(signRequest);
 | 
								map.from(identityprovider.getSinglesignon()::isSignRequest).when((signRequest) -> !usingMetadata)
 | 
				
			||||||
 | 
										.to(details::wantAuthnRequestsSigned);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
		builder.signingX509Credentials((credentials) -> properties.getSigning().getCredentials().stream()
 | 
							builder.signingX509Credentials((credentials) -> properties.getSigning().getCredentials().stream()
 | 
				
			||||||
				.map(this::asSigningCredential).forEach(credentials::add));
 | 
									.map(this::asSigningCredential).forEach(credentials::add));
 | 
				
			||||||
| 
						 | 
					@ -81,7 +93,10 @@ class Saml2RelyingPartyRegistrationConfiguration {
 | 
				
			||||||
				.verificationX509Credentials((credentials) -> properties.getIdentityprovider().getVerification()
 | 
									.verificationX509Credentials((credentials) -> properties.getIdentityprovider().getVerification()
 | 
				
			||||||
						.getCredentials().stream().map(this::asVerificationCredential).forEach(credentials::add)));
 | 
											.getCredentials().stream().map(this::asVerificationCredential).forEach(credentials::add)));
 | 
				
			||||||
		builder.entityId(properties.getRelyingPartyEntityId());
 | 
							builder.entityId(properties.getRelyingPartyEntityId());
 | 
				
			||||||
		return builder.build();
 | 
							RelyingPartyRegistration registration = builder.build();
 | 
				
			||||||
 | 
							boolean signRequest = registration.getAssertingPartyDetails().getWantAuthnRequestsSigned();
 | 
				
			||||||
 | 
							validateSigningCredentials(properties, signRequest);
 | 
				
			||||||
 | 
							return registration;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private void validateSigningCredentials(Registration properties, boolean signRequest) {
 | 
						private void validateSigningCredentials(Registration properties, boolean signRequest) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,10 +16,14 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
package org.springframework.boot.autoconfigure.security.saml2;
 | 
					package org.springframework.boot.autoconfigure.security.saml2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.InputStream;
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import javax.servlet.Filter;
 | 
					import javax.servlet.Filter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import okhttp3.mockwebserver.MockResponse;
 | 
				
			||||||
 | 
					import okhttp3.mockwebserver.MockWebServer;
 | 
				
			||||||
 | 
					import okio.Buffer;
 | 
				
			||||||
import org.junit.jupiter.api.Test;
 | 
					import org.junit.jupiter.api.Test;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
 | 
					import org.springframework.boot.autoconfigure.AutoConfigurations;
 | 
				
			||||||
| 
						 | 
					@ -30,6 +34,7 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner;
 | 
				
			||||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
 | 
					import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
 | 
				
			||||||
import org.springframework.context.annotation.Bean;
 | 
					import org.springframework.context.annotation.Bean;
 | 
				
			||||||
import org.springframework.context.annotation.Configuration;
 | 
					import org.springframework.context.annotation.Configuration;
 | 
				
			||||||
 | 
					import org.springframework.core.io.ClassPathResource;
 | 
				
			||||||
import org.springframework.security.config.BeanIds;
 | 
					import org.springframework.security.config.BeanIds;
 | 
				
			||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 | 
					import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 | 
				
			||||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 | 
					import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 | 
				
			||||||
| 
						 | 
					@ -112,6 +117,20 @@ class Saml2RelyingPartyAutoConfigurationTests {
 | 
				
			||||||
				.run((context) -> assertThat(context).hasSingleBean(RelyingPartyRegistrationRepository.class));
 | 
									.run((context) -> assertThat(context).hasSingleBean(RelyingPartyRegistrationRepository.class));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Test
 | 
				
			||||||
 | 
						void autoconfigurationShouldQueryIdentityProviderMetadataWhenMetadataUrlIsPresent() throws Exception {
 | 
				
			||||||
 | 
							try (MockWebServer server = new MockWebServer()) {
 | 
				
			||||||
 | 
								server.start();
 | 
				
			||||||
 | 
								String metadataUrl = server.url("").toString();
 | 
				
			||||||
 | 
								setupMockResponse(server);
 | 
				
			||||||
 | 
								this.contextRunner.withPropertyValues(PREFIX + ".foo.identityprovider.metadata-uri=" + metadataUrl)
 | 
				
			||||||
 | 
										.run((context) -> {
 | 
				
			||||||
 | 
											assertThat(context).hasSingleBean(RelyingPartyRegistrationRepository.class);
 | 
				
			||||||
 | 
											assertThat(server.getRequestCount()).isEqualTo(1);
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Test
 | 
						@Test
 | 
				
			||||||
	void relyingPartyRegistrationRepositoryShouldBeConditionalOnMissingBean() {
 | 
						void relyingPartyRegistrationRepositoryShouldBeConditionalOnMissingBean() {
 | 
				
			||||||
		this.contextRunner.withPropertyValues(getPropertyValues())
 | 
							this.contextRunner.withPropertyValues(getPropertyValues())
 | 
				
			||||||
| 
						 | 
					@ -176,6 +195,14 @@ class Saml2RelyingPartyAutoConfigurationTests {
 | 
				
			||||||
		return filters.stream().anyMatch(filter::isInstance);
 | 
							return filters.stream().anyMatch(filter::isInstance);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private void setupMockResponse(MockWebServer server) throws Exception {
 | 
				
			||||||
 | 
							try (InputStream metadataSource = new ClassPathResource("saml/idp-metadata").getInputStream()) {
 | 
				
			||||||
 | 
								Buffer metadataBuffer = new Buffer().readFrom(metadataSource);
 | 
				
			||||||
 | 
								MockResponse metadataResponse = new MockResponse().setBody(metadataBuffer);
 | 
				
			||||||
 | 
								server.enqueue(metadataResponse);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Configuration(proxyBeanMethods = false)
 | 
						@Configuration(proxyBeanMethods = false)
 | 
				
			||||||
	static class RegistrationRepositoryConfiguration {
 | 
						static class RegistrationRepositoryConfiguration {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -102,6 +102,14 @@ class Saml2RelyingPartyPropertiesTests {
 | 
				
			||||||
				.isEqualTo(new Saml2RelyingPartyProperties.Registration().getRelyingPartyEntityId());
 | 
									.isEqualTo(new Saml2RelyingPartyProperties.Registration().getRelyingPartyEntityId());
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Test
 | 
				
			||||||
 | 
						void customizeIdentityProviderMetadataUrl() {
 | 
				
			||||||
 | 
							bind("spring.security.saml2.relyingparty.registration.simplesamlphp.identityprovider.metadata-uri",
 | 
				
			||||||
 | 
									"https://idp.example.org/metadata");
 | 
				
			||||||
 | 
							assertThat(this.properties.getRegistration().get("simplesamlphp").getIdentityprovider().getMetadataUri())
 | 
				
			||||||
 | 
									.isEqualTo("https://idp.example.org/metadata");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private void bind(String name, String value) {
 | 
						private void bind(String name, String value) {
 | 
				
			||||||
		bind(Collections.singletonMap(name, value));
 | 
							bind(Collections.singletonMap(name, value));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,42 @@
 | 
				
			||||||
 | 
					<md:EntityDescriptor entityID="https://idp.example.com/idp/shibboleth"
 | 
				
			||||||
 | 
					        xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
 | 
				
			||||||
 | 
					        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
				
			||||||
 | 
					        xmlns:shibmd="urn:mace:shibboleth:metadata:1.0"
 | 
				
			||||||
 | 
					        xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
 | 
				
			||||||
 | 
					        xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui">
 | 
				
			||||||
 | 
					    <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
 | 
				
			||||||
 | 
							<md:KeyDescriptor>
 | 
				
			||||||
 | 
					            <ds:KeyInfo>
 | 
				
			||||||
 | 
					                <ds:X509Data>
 | 
				
			||||||
 | 
					                    <ds:X509Certificate>
 | 
				
			||||||
 | 
					                        MIIDZjCCAk6gAwIBAgIVAL9O+PA7SXtlwZZY8MVSE9On1cVWMA0GCSqGSIb3DQEB
 | 
				
			||||||
 | 
					                        BQUAMCkxJzAlBgNVBAMTHmlkZW0tcHVwYWdlbnQuZG16LWludC51bmltby5pdDAe
 | 
				
			||||||
 | 
					                        Fw0xMzA3MjQwMDQ0MTRaFw0zMzA3MjQwMDQ0MTRaMCkxJzAlBgNVBAMTHmlkZW0t
 | 
				
			||||||
 | 
					                        cHVwYWdlbnQuZG16LWludC51bmltby5pdDCCASIwDQYJKoZIhvcNAMIIDQADggEP
 | 
				
			||||||
 | 
					                        ADCCAQoCggEBAIAcp/VyzZGXUF99kwj4NvL/Rwv4YvBgLWzpCuoxqHZ/hmBwJtqS
 | 
				
			||||||
 | 
					                        v0y9METBPFbgsF3hCISnxbcmNVxf/D0MoeKtw1YPbsUmow/bFe+r72hZ+IVAcejN
 | 
				
			||||||
 | 
					                        iDJ7t5oTjsRN1t1SqvVVk6Ryk5AZhpFW+W9pE9N6c7kJ16Rp2/mbtax9OCzxpece
 | 
				
			||||||
 | 
					                        byi1eiLfIBmkcRawL/vCc2v6VLI18i6HsNVO3l2yGosKCbuSoGDx2fCdAOk/rgdz
 | 
				
			||||||
 | 
					                        cWOvFsIZSKuD+FVbSS/J9GVs7yotsS4PRl4iX9UMnfDnOMfO7bcBgbXtDl4SCU1v
 | 
				
			||||||
 | 
					                        dJrRw7IL/pLz34Rv9a8nYitrzrxtLOp3nYUCAwEAAaOBhDCBgTBgBgMIIDEEWTBX
 | 
				
			||||||
 | 
					                        gh5pZGVtLXB1cGFnZW50LmRtei1pbnQudW5pbW8uaXSGNWh0dHBzOi8vaWRlbS1w
 | 
				
			||||||
 | 
					                        dXBhZ2VudC5kbXotaW50LnVuaW1vLml0L2lkcC9zaGliYm9sZXRoMB0GA1UdDgQW
 | 
				
			||||||
 | 
					                        BBT8PANzz+adGnTRe8ldcyxAwe4VnzANBgkqhkiG9w0BAQUFAAOCAQEAOEnO8Clu
 | 
				
			||||||
 | 
					                        9z/Lf/8XOOsTdxJbV29DIF3G8KoQsB3dBsLwPZVEAQIP6ceS32Xaxrl6FMTDDNkL
 | 
				
			||||||
 | 
					                        qUvvInUisw0+I5zZwYHybJQCletUWTnz58SC4C9G7FpuXHFZnOGtRcgGD1NOX4UU
 | 
				
			||||||
 | 
					                        duus/4nVcGSLhDjszZ70Xtj0gw2Sn46oQPHTJ81QZ3Y9ih+Aj1c9OtUSBwtWZFkU
 | 
				
			||||||
 | 
					                        yooAKoR8li68Yb21zN2N65AqV+ndL98M8xUYMKLONuAXStDeoVCipH6PJ09Z5U2p
 | 
				
			||||||
 | 
					                        V5p4IQRV6QBsNw9CISJFuHzkVYTH5ZxzN80Ru46vh4y2M0Nu8GQ9I085KoZkrf5e
 | 
				
			||||||
 | 
					                        Cq53OZt9ISjHEw==
 | 
				
			||||||
 | 
					                    </ds:X509Certificate>
 | 
				
			||||||
 | 
					                </ds:X509Data>
 | 
				
			||||||
 | 
					            </ds:KeyInfo>
 | 
				
			||||||
 | 
					        </md:KeyDescriptor>
 | 
				
			||||||
 | 
					        <md:SingleSignOnService
 | 
				
			||||||
 | 
					                Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
 | 
				
			||||||
 | 
					                Location="https://idp.example.com/sso"/>
 | 
				
			||||||
 | 
					    </md:IDPSSODescriptor>
 | 
				
			||||||
 | 
					    <md:ContactPerson contactType="technical">
 | 
				
			||||||
 | 
					        <md:EmailAddress>mailto:technical.contact@example.com</md:EmailAddress>
 | 
				
			||||||
 | 
					    </md:ContactPerson>
 | 
				
			||||||
 | 
					</md:EntityDescriptor>
 | 
				
			||||||
		Loading…
	
		Reference in New Issue