Skip to content

Commit 5187c01

Browse files
jzheauxsnicoll
authored andcommitted
Configure SAML 2.0 Service Provider via Metadata
See gh-23045
1 parent bd9928c commit 5187c01

File tree

5 files changed

+113
-8
lines changed

5 files changed

+113
-8
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyProperties.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ public static class Identityprovider {
140140
*/
141141
private String entityId;
142142

143+
/**
144+
* Endpoint for discovery-based configuration.
145+
*/
146+
private String metadataUri;
147+
143148
private final Singlesignon singlesignon = new Singlesignon();
144149

145150
private final Verification verification = new Verification();
@@ -152,6 +157,14 @@ public void setEntityId(String entityId) {
152157
this.entityId = entityId;
153158
}
154159

160+
public String getMetadataUri() {
161+
return this.metadataUri;
162+
}
163+
164+
public void setMetadataUri(String metadataUri) {
165+
this.metadataUri = metadataUri;
166+
}
167+
155168
@Deprecated
156169
@DeprecatedConfigurationProperty(reason = "moved to 'singlesignon.url'")
157170
public String getSsoUrl() {

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyRegistrationConfiguration.java

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyProperties.Identityprovider.Verification;
2929
import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyProperties.Registration;
3030
import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyProperties.Registration.Signing;
31+
import org.springframework.boot.context.properties.PropertyMapper;
3132
import org.springframework.context.annotation.Bean;
3233
import org.springframework.context.annotation.Conditional;
3334
import org.springframework.context.annotation.Configuration;
@@ -37,8 +38,10 @@
3738
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
3839
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
3940
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
41+
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations;
4042
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
4143
import org.springframework.util.Assert;
44+
import org.springframework.util.StringUtils;
4245

4346
/**
4447
* {@link Configuration @Configuration} used to map {@link Saml2RelyingPartyProperties} to
@@ -64,24 +67,36 @@ private RelyingPartyRegistration asRegistration(Map.Entry<String, Registration>
6467
}
6568

6669
private RelyingPartyRegistration asRegistration(String id, Registration properties) {
67-
boolean signRequest = properties.getIdentityprovider().getSinglesignon().isSignRequest();
68-
validateSigningCredentials(properties, signRequest);
69-
RelyingPartyRegistration.Builder builder = RelyingPartyRegistration.withRegistrationId(id);
70+
RelyingPartyRegistration.Builder builder;
71+
boolean usingMetadata = StringUtils.hasText(properties.getIdentityprovider().getMetadataUri());
72+
if (usingMetadata) {
73+
builder = RelyingPartyRegistrations.fromMetadataLocation(properties.getIdentityprovider().getMetadataUri())
74+
.registrationId(id);
75+
}
76+
else {
77+
builder = RelyingPartyRegistration.withRegistrationId(id);
78+
}
7079
builder.assertionConsumerServiceLocation(
7180
"{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI);
81+
Saml2RelyingPartyProperties.Identityprovider identityprovider = properties.getIdentityprovider();
82+
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
7283
builder.assertingPartyDetails((details) -> {
73-
details.singleSignOnServiceLocation(properties.getIdentityprovider().getSinglesignon().getUrl());
74-
details.entityId(properties.getIdentityprovider().getEntityId());
75-
details.singleSignOnServiceBinding(properties.getIdentityprovider().getSinglesignon().getBinding());
76-
details.wantAuthnRequestsSigned(signRequest);
84+
map.from(identityprovider::getEntityId).to(details::entityId);
85+
map.from(identityprovider.getSinglesignon()::getBinding).to(details::singleSignOnServiceBinding);
86+
map.from(identityprovider.getSinglesignon()::getUrl).to(details::singleSignOnServiceLocation);
87+
map.from(identityprovider.getSinglesignon()::isSignRequest).when((signRequest) -> !usingMetadata)
88+
.to(details::wantAuthnRequestsSigned);
7789
});
7890
builder.signingX509Credentials((credentials) -> properties.getSigning().getCredentials().stream()
7991
.map(this::asSigningCredential).forEach(credentials::add));
8092
builder.assertingPartyDetails((details) -> details
8193
.verificationX509Credentials((credentials) -> properties.getIdentityprovider().getVerification()
8294
.getCredentials().stream().map(this::asVerificationCredential).forEach(credentials::add)));
8395
builder.entityId(properties.getRelyingPartyEntityId());
84-
return builder.build();
96+
RelyingPartyRegistration registration = builder.build();
97+
boolean signRequest = registration.getAssertingPartyDetails().getWantAuthnRequestsSigned();
98+
validateSigningCredentials(properties, signRequest);
99+
return registration;
85100
}
86101

87102
private void validateSigningCredentials(Registration properties, boolean signRequest) {

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyAutoConfigurationTests.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@
1616

1717
package org.springframework.boot.autoconfigure.security.saml2;
1818

19+
import java.io.InputStream;
1920
import java.util.List;
2021

2122
import javax.servlet.Filter;
2223

24+
import okhttp3.mockwebserver.MockResponse;
25+
import okhttp3.mockwebserver.MockWebServer;
26+
import okio.Buffer;
2327
import org.junit.jupiter.api.Test;
2428

2529
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -30,6 +34,7 @@
3034
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
3135
import org.springframework.context.annotation.Bean;
3236
import org.springframework.context.annotation.Configuration;
37+
import org.springframework.core.io.ClassPathResource;
3338
import org.springframework.security.config.BeanIds;
3439
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
3540
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@@ -112,6 +117,20 @@ void autoConfigurationWhenSignRequestsFalseAndNoSigningCredentialsShouldNotThrow
112117
.run((context) -> assertThat(context).hasSingleBean(RelyingPartyRegistrationRepository.class));
113118
}
114119

120+
@Test
121+
void autoconfigurationShouldQueryIdentityProviderMetadataWhenMetadataUrlIsPresent() throws Exception {
122+
try (MockWebServer server = new MockWebServer()) {
123+
server.start();
124+
String metadataUrl = server.url("").toString();
125+
setupMockResponse(server);
126+
this.contextRunner.withPropertyValues(PREFIX + ".foo.identityprovider.metadata-uri=" + metadataUrl)
127+
.run((context) -> {
128+
assertThat(context).hasSingleBean(RelyingPartyRegistrationRepository.class);
129+
assertThat(server.getRequestCount()).isEqualTo(1);
130+
});
131+
}
132+
}
133+
115134
@Test
116135
void relyingPartyRegistrationRepositoryShouldBeConditionalOnMissingBean() {
117136
this.contextRunner.withPropertyValues(getPropertyValues())
@@ -176,6 +195,14 @@ private boolean hasFilter(AssertableWebApplicationContext context, Class<? exten
176195
return filters.stream().anyMatch(filter::isInstance);
177196
}
178197

198+
private void setupMockResponse(MockWebServer server) throws Exception {
199+
try (InputStream metadataSource = new ClassPathResource("saml/idp-metadata").getInputStream()) {
200+
Buffer metadataBuffer = new Buffer().readFrom(metadataSource);
201+
MockResponse metadataResponse = new MockResponse().setBody(metadataBuffer);
202+
server.enqueue(metadataResponse);
203+
}
204+
}
205+
179206
@Configuration(proxyBeanMethods = false)
180207
static class RegistrationRepositoryConfiguration {
181208

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyPropertiesTests.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ void customizeRelyingPartyEntityIdDefaultsToServiceProviderMetadata() {
102102
.isEqualTo(new Saml2RelyingPartyProperties.Registration().getRelyingPartyEntityId());
103103
}
104104

105+
@Test
106+
void customizeIdentityProviderMetadataUrl() {
107+
bind("spring.security.saml2.relyingparty.registration.simplesamlphp.identityprovider.metadata-uri",
108+
"https://idp.example.org/metadata");
109+
assertThat(this.properties.getRegistration().get("simplesamlphp").getIdentityprovider().getMetadataUri())
110+
.isEqualTo("https://idp.example.org/metadata");
111+
}
112+
105113
private void bind(String name, String value) {
106114
bind(Collections.singletonMap(name, value));
107115
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<md:EntityDescriptor entityID="https://idp.example.com/idp/shibboleth"
2+
xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xmlns:shibmd="urn:mace:shibboleth:metadata:1.0"
5+
xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
6+
xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui">
7+
<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
8+
<md:KeyDescriptor>
9+
<ds:KeyInfo>
10+
<ds:X509Data>
11+
<ds:X509Certificate>
12+
MIIDZjCCAk6gAwIBAgIVAL9O+PA7SXtlwZZY8MVSE9On1cVWMA0GCSqGSIb3DQEB
13+
BQUAMCkxJzAlBgNVBAMTHmlkZW0tcHVwYWdlbnQuZG16LWludC51bmltby5pdDAe
14+
Fw0xMzA3MjQwMDQ0MTRaFw0zMzA3MjQwMDQ0MTRaMCkxJzAlBgNVBAMTHmlkZW0t
15+
cHVwYWdlbnQuZG16LWludC51bmltby5pdDCCASIwDQYJKoZIhvcNAMIIDQADggEP
16+
ADCCAQoCggEBAIAcp/VyzZGXUF99kwj4NvL/Rwv4YvBgLWzpCuoxqHZ/hmBwJtqS
17+
v0y9METBPFbgsF3hCISnxbcmNVxf/D0MoeKtw1YPbsUmow/bFe+r72hZ+IVAcejN
18+
iDJ7t5oTjsRN1t1SqvVVk6Ryk5AZhpFW+W9pE9N6c7kJ16Rp2/mbtax9OCzxpece
19+
byi1eiLfIBmkcRawL/vCc2v6VLI18i6HsNVO3l2yGosKCbuSoGDx2fCdAOk/rgdz
20+
cWOvFsIZSKuD+FVbSS/J9GVs7yotsS4PRl4iX9UMnfDnOMfO7bcBgbXtDl4SCU1v
21+
dJrRw7IL/pLz34Rv9a8nYitrzrxtLOp3nYUCAwEAAaOBhDCBgTBgBgMIIDEEWTBX
22+
gh5pZGVtLXB1cGFnZW50LmRtei1pbnQudW5pbW8uaXSGNWh0dHBzOi8vaWRlbS1w
23+
dXBhZ2VudC5kbXotaW50LnVuaW1vLml0L2lkcC9zaGliYm9sZXRoMB0GA1UdDgQW
24+
BBT8PANzz+adGnTRe8ldcyxAwe4VnzANBgkqhkiG9w0BAQUFAAOCAQEAOEnO8Clu
25+
9z/Lf/8XOOsTdxJbV29DIF3G8KoQsB3dBsLwPZVEAQIP6ceS32Xaxrl6FMTDDNkL
26+
qUvvInUisw0+I5zZwYHybJQCletUWTnz58SC4C9G7FpuXHFZnOGtRcgGD1NOX4UU
27+
duus/4nVcGSLhDjszZ70Xtj0gw2Sn46oQPHTJ81QZ3Y9ih+Aj1c9OtUSBwtWZFkU
28+
yooAKoR8li68Yb21zN2N65AqV+ndL98M8xUYMKLONuAXStDeoVCipH6PJ09Z5U2p
29+
V5p4IQRV6QBsNw9CISJFuHzkVYTH5ZxzN80Ru46vh4y2M0Nu8GQ9I085KoZkrf5e
30+
Cq53OZt9ISjHEw==
31+
</ds:X509Certificate>
32+
</ds:X509Data>
33+
</ds:KeyInfo>
34+
</md:KeyDescriptor>
35+
<md:SingleSignOnService
36+
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
37+
Location="https://idp.example.com/sso"/>
38+
</md:IDPSSODescriptor>
39+
<md:ContactPerson contactType="technical">
40+
<md:EmailAddress>mailto:[email protected]</md:EmailAddress>
41+
</md:ContactPerson>
42+
</md:EntityDescriptor>

0 commit comments

Comments
 (0)