Merge pull request #23045 from jzheaux

* pr/23045:
  Polish "Configure SAML 2.0 Service Provider via Metadata"
  Configure SAML 2.0 Service Provider via Metadata

Closes gh-23045
This commit is contained in:
Stephane Nicoll
2020-09-02 10:35:24 +02:00
5 changed files with 119 additions and 10 deletions

View File

@@ -140,6 +140,11 @@ public class Saml2RelyingPartyProperties {
*/
private String entityId;
/**
* URI to the metadata 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() {

View File

@@ -22,12 +22,14 @@ import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;
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;
@@ -36,9 +38,13 @@ import org.springframework.security.converter.RsaKeyConverters;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.AssertingPartyDetails;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.Builder;
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,24 +70,36 @@ class Saml2RelyingPartyRegistrationConfiguration {
}
private RelyingPartyRegistration asRegistration(String id, Registration properties) {
boolean signRequest = properties.getIdentityprovider().getSinglesignon().isSignRequest();
validateSigningCredentials(properties, signRequest);
RelyingPartyRegistration.Builder builder = RelyingPartyRegistration.withRegistrationId(id);
boolean usingMetadata = StringUtils.hasText(properties.getIdentityprovider().getMetadataUri());
Builder builder = (usingMetadata) ? RelyingPartyRegistrations
.fromMetadataLocation(properties.getIdentityprovider().getMetadataUri()).registrationId(id)
: RelyingPartyRegistration.withRegistrationId(id);
builder.assertionConsumerServiceLocation(
"{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI);
builder.assertingPartyDetails((details) -> {
details.singleSignOnServiceLocation(properties.getIdentityprovider().getSinglesignon().getUrl());
details.entityId(properties.getIdentityprovider().getEntityId());
details.singleSignOnServiceBinding(properties.getIdentityprovider().getSinglesignon().getBinding());
details.wantAuthnRequestsSigned(signRequest);
});
builder.assertingPartyDetails(mapIdentityProvider(properties, usingMetadata));
builder.signingX509Credentials((credentials) -> properties.getSigning().getCredentials().stream()
.map(this::asSigningCredential).forEach(credentials::add));
builder.assertingPartyDetails((details) -> details
.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 Consumer<AssertingPartyDetails.Builder> mapIdentityProvider(Registration properties,
boolean usingMetadata) {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
Saml2RelyingPartyProperties.Identityprovider identityprovider = properties.getIdentityprovider();
return (details) -> {
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);
};
}
private void validateSigningCredentials(Registration properties, boolean signRequest) {

View File

@@ -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,8 @@ 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.core.io.Resource;
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 +118,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, new ClassPathResource("saml/idp-metadata"));
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 +196,14 @@ class Saml2RelyingPartyAutoConfigurationTests {
return filters.stream().anyMatch(filter::isInstance);
}
private void setupMockResponse(MockWebServer server, Resource resourceBody) throws Exception {
try (InputStream metadataSource = resourceBody.getInputStream()) {
Buffer metadataBuffer = new Buffer().readFrom(metadataSource);
MockResponse metadataResponse = new MockResponse().setBody(metadataBuffer);
server.enqueue(metadataResponse);
}
}
@Configuration(proxyBeanMethods = false)
static class RegistrationRepositoryConfiguration {

View File

@@ -102,6 +102,14 @@ class Saml2RelyingPartyPropertiesTests {
.isEqualTo(new Saml2RelyingPartyProperties.Registration().getRelyingPartyEntityId());
}
@Test
void customizeIdentityProviderMetadataUri() {
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));
}

View File

@@ -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>