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;
|
||||
|
||||
/**
|
||||
* 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…
Reference in New Issue