Configure SAML 2.0 Service Provider via Metadata

See gh-23045
pull/23173/head
Josh Cummings 4 years ago committed by Stephane Nicoll
parent bd9928cc31
commit 5187c01e39

@ -140,6 +140,11 @@ public class Saml2RelyingPartyProperties {
*/
private String entityId;
/**
* Endpoint for discovery-based configuration.
*/
private String metadataUri;
private final Singlesignon singlesignon = new Singlesignon();
private final Verification verification = new Verification();
@ -152,6 +157,14 @@ public class Saml2RelyingPartyProperties {
this.entityId = entityId;
}
public String getMetadataUri() {
return this.metadataUri;
}
public void setMetadataUri(String metadataUri) {
this.metadataUri = metadataUri;
}
@Deprecated
@DeprecatedConfigurationProperty(reason = "moved to 'singlesignon.url'")
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.Registration;
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.Conditional;
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.RelyingPartyRegistration;
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.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link Configuration @Configuration} used to map {@link Saml2RelyingPartyProperties} to
@ -64,16 +67,25 @@ class Saml2RelyingPartyRegistrationConfiguration {
}
private RelyingPartyRegistration asRegistration(String id, Registration properties) {
boolean signRequest = properties.getIdentityprovider().getSinglesignon().isSignRequest();
validateSigningCredentials(properties, signRequest);
RelyingPartyRegistration.Builder builder = RelyingPartyRegistration.withRegistrationId(id);
RelyingPartyRegistration.Builder builder;
boolean usingMetadata = StringUtils.hasText(properties.getIdentityprovider().getMetadataUri());
if (usingMetadata) {
builder = RelyingPartyRegistrations.fromMetadataLocation(properties.getIdentityprovider().getMetadataUri())
.registrationId(id);
}
else {
builder = RelyingPartyRegistration.withRegistrationId(id);
}
builder.assertionConsumerServiceLocation(
"{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI);
Saml2RelyingPartyProperties.Identityprovider identityprovider = properties.getIdentityprovider();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
builder.assertingPartyDetails((details) -> {
details.singleSignOnServiceLocation(properties.getIdentityprovider().getSinglesignon().getUrl());
details.entityId(properties.getIdentityprovider().getEntityId());
details.singleSignOnServiceBinding(properties.getIdentityprovider().getSinglesignon().getBinding());
details.wantAuthnRequestsSigned(signRequest);
map.from(identityprovider::getEntityId).to(details::entityId);
map.from(identityprovider.getSinglesignon()::getBinding).to(details::singleSignOnServiceBinding);
map.from(identityprovider.getSinglesignon()::getUrl).to(details::singleSignOnServiceLocation);
map.from(identityprovider.getSinglesignon()::isSignRequest).when((signRequest) -> !usingMetadata)
.to(details::wantAuthnRequestsSigned);
});
builder.signingX509Credentials((credentials) -> properties.getSigning().getCredentials().stream()
.map(this::asSigningCredential).forEach(credentials::add));
@ -81,7 +93,10 @@ class Saml2RelyingPartyRegistrationConfiguration {
.verificationX509Credentials((credentials) -> properties.getIdentityprovider().getVerification()
.getCredentials().stream().map(this::asVerificationCredential).forEach(credentials::add)));
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) {

@ -16,10 +16,14 @@
package org.springframework.boot.autoconfigure.security.saml2;
import java.io.InputStream;
import java.util.List;
import javax.servlet.Filter;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okio.Buffer;
import org.junit.jupiter.api.Test;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@ -112,6 +117,20 @@ class Saml2RelyingPartyAutoConfigurationTests {
.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
void relyingPartyRegistrationRepositoryShouldBeConditionalOnMissingBean() {
this.contextRunner.withPropertyValues(getPropertyValues())
@ -176,6 +195,14 @@ class Saml2RelyingPartyAutoConfigurationTests {
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)
static class RegistrationRepositoryConfiguration {

@ -102,6 +102,14 @@ class Saml2RelyingPartyPropertiesTests {
.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) {
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…
Cancel
Save