Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ public static class Identityprovider {
*/
private String entityId;

/**
* Endpoint for discovery-based configuration.
*/
private String metadataUri;

private final Singlesignon singlesignon = new Singlesignon();

private final Verification verification = new Verification();
Expand All @@ -152,6 +157,14 @@ public void setEntityId(String entityId) {
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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of PropertyMapper adds a lot of noise to what this PR actually changes. I'd prefer if we did that separately (if at all).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we leave the associated code as-is, then nulls will override what was specified through the metadata endpoint.

Would surrounding each of those lines with if statements be preferrable?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I missed that. PropertyMapper it is then!

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
Expand All @@ -37,8 +38,10 @@
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
Expand All @@ -64,24 +67,36 @@ private RelyingPartyRegistration asRegistration(Map.Entry<String, Registration>
}

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));
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 void validateSigningCredentials(Registration properties, boolean signRequest) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,6 +34,7 @@
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;
Expand Down Expand Up @@ -112,6 +117,20 @@ void autoConfigurationWhenSignRequestsFalseAndNoSigningCredentialsShouldNotThrow
.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())
Expand Down Expand Up @@ -176,6 +195,14 @@ private boolean hasFilter(AssertableWebApplicationContext context, Class<? exten
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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ void customizeRelyingPartyEntityIdDefaultsToServiceProviderMetadata() {
.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));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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:[email protected]</md:EmailAddress>
</md:ContactPerson>
</md:EntityDescriptor>