Skip to content

Commit f82190b

Browse files
committed
Add RelyingPartyRegistrations
Closes gh-8484
1 parent 506786f commit f82190b

File tree

5 files changed

+261
-4
lines changed

5 files changed

+261
-4
lines changed

saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ dependencies {
99
compile("org.opensaml:opensaml-saml-impl")
1010

1111
provided 'javax.servlet:javax.servlet-api'
12+
13+
testCompile 'com.squareup.okhttp3:mockwebserver'
1214
}

saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,13 @@ public RelyingPartyRegistration.Builder read(Class<? extends RelyingPartyRegistr
149149
encryption.add(encryption(certificate));
150150
}
151151
}
152+
if (keyDescriptor.getUse().equals(UsageType.UNSPECIFIED)) {
153+
List<X509Certificate> certificates = certificates(keyDescriptor);
154+
for (X509Certificate certificate : certificates) {
155+
verification.add(verification(certificate));
156+
encryption.add(encryption(certificate));
157+
}
158+
}
152159
}
153160
if (verification.isEmpty()) {
154161
throw new Saml2Exception("Metadata response is missing verification certificates, necessary for verifying SAML assertions");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.saml2.provider.service.registration;
18+
19+
import java.util.Arrays;
20+
21+
import org.springframework.security.saml2.Saml2Exception;
22+
import org.springframework.web.client.RestClientException;
23+
import org.springframework.web.client.RestOperations;
24+
import org.springframework.web.client.RestTemplate;
25+
26+
/**
27+
* A utility class for constructing instances of {@link RelyingPartyRegistration}
28+
*
29+
* @author Josh Cummings
30+
* @since 5.4
31+
*/
32+
public final class RelyingPartyRegistrations {
33+
private static final RestOperations rest = new RestTemplate
34+
(Arrays.asList(new OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter()));
35+
36+
/**
37+
* Return a {@link RelyingPartyRegistration.Builder} based off of the given
38+
* SAML 2.0 Asserting Party (IDP) metadata.
39+
*
40+
* Note that by default the registrationId is set to be the given metadata location,
41+
* but this will most often not be sufficient. To complete the configuration, most
42+
* applications will also need to provide a registrationId, like so:
43+
*
44+
* <pre>
45+
* RelyingPartyRegistration registration = RelyingPartyRegistrations
46+
* .fromMetadataLocation(metadataLocation)
47+
* .registrationId("registration-id")
48+
* .build();
49+
* </pre>
50+
*
51+
* Also note that an {@code IDPSSODescriptor} typically only contains information about
52+
* the asserting party. Thus, you will need to remember to still populate anything about the
53+
* relying party, like any private keys the relying party will use for signing AuthnRequests.
54+
*
55+
* @param metadataLocation
56+
* @return the {@link RelyingPartyRegistration.Builder} for further configuration
57+
*/
58+
public static RelyingPartyRegistration.Builder fromMetadataLocation(String metadataLocation) {
59+
try {
60+
return rest.getForObject(metadataLocation, RelyingPartyRegistration.Builder.class);
61+
} catch (RestClientException e) {
62+
if (e.getCause() instanceof Saml2Exception) {
63+
throw (Saml2Exception) e.getCause();
64+
}
65+
throw new Saml2Exception(e);
66+
}
67+
}
68+
}

saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverterTests.java

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverterTests {
4747
"%s\n" +
4848
"</md:IDPSSODescriptor>";
4949
private static final String KEY_DESCRIPTOR_TEMPLATE =
50-
"<md:KeyDescriptor use=\"%s\">\n" +
50+
"<md:KeyDescriptor %s>\n" +
5151
"<ds:KeyInfo xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\">\n" +
5252
"<ds:X509Data>\n" +
5353
"<ds:X509Certificate>" + CERTIFICATE + "</ds:X509Certificate>\n" +
@@ -88,7 +88,7 @@ public void readWhenMissingVerificationKeyThenException() {
8888
public void readWhenMissingSingleSignOnServiceThenException() {
8989
String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE,
9090
String.format(IDP_SSO_DESCRIPTOR_TEMPLATE,
91-
String.format(KEY_DESCRIPTOR_TEMPLATE, "signing")
91+
String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\"")
9292
));
9393
MockClientHttpResponse response = new MockClientHttpResponse(payload.getBytes(), OK);
9494
assertThatCode(() -> this.converter.read(RelyingPartyRegistration.Builder.class, response))
@@ -100,8 +100,8 @@ public void readWhenMissingSingleSignOnServiceThenException() {
100100
public void readWhenDescriptorFullySpecifiedThenConfigures() throws Exception {
101101
String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE,
102102
String.format(IDP_SSO_DESCRIPTOR_TEMPLATE,
103-
String.format(KEY_DESCRIPTOR_TEMPLATE, "signing") +
104-
String.format(KEY_DESCRIPTOR_TEMPLATE, "encryption") +
103+
String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\"") +
104+
String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"encryption\"") +
105105
String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE)
106106
));
107107
MockClientHttpResponse response = new MockClientHttpResponse(payload.getBytes(), OK);
@@ -123,6 +123,27 @@ public void readWhenDescriptorFullySpecifiedThenConfigures() throws Exception {
123123
.isEqualTo(x509Certificate(CERTIFICATE));
124124
}
125125

126+
@Test
127+
public void readWhenKeyDescriptorHasNoUseThenConfiguresBothKeyTypes() throws Exception {
128+
String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE,
129+
String.format(IDP_SSO_DESCRIPTOR_TEMPLATE,
130+
String.format(KEY_DESCRIPTOR_TEMPLATE, "") +
131+
String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE)
132+
));
133+
MockClientHttpResponse response = new MockClientHttpResponse(payload.getBytes(), OK);
134+
RelyingPartyRegistration registration =
135+
this.converter.read(RelyingPartyRegistration.Builder.class, response)
136+
.registrationId("one")
137+
.build();
138+
RelyingPartyRegistration.AssertingPartyDetails details =
139+
registration.getAssertingPartyDetails();
140+
assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate())
141+
.isEqualTo(x509Certificate(CERTIFICATE));
142+
assertThat(details.getEncryptionX509Credentials()).hasSize(1);
143+
assertThat(details.getEncryptionX509Credentials().iterator().next().getCertificate())
144+
.isEqualTo(x509Certificate(CERTIFICATE));
145+
}
146+
126147
X509Certificate x509Certificate(String data) {
127148
try {
128149
InputStream certificate = new ByteArrayInputStream(Base64.getDecoder().decode(data.getBytes()));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.saml2.provider.service.registration;
18+
19+
import okhttp3.mockwebserver.MockResponse;
20+
import okhttp3.mockwebserver.MockWebServer;
21+
import org.junit.Test;
22+
23+
import org.springframework.security.saml2.Saml2Exception;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode;
27+
28+
/**
29+
* Tests for {@link RelyingPartyRegistration}
30+
*/
31+
public class RelyingPartyRegistrationsTests {
32+
private static final String IDP_SSO_DESCRIPTOR_PAYLOAD =
33+
"<md:EntityDescriptor entityID=\"https://idp.example.com/idp/shibboleth\"\n" +
34+
" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"\n" +
35+
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
36+
" xmlns:shibmd=\"urn:mace:shibboleth:metadata:1.0\"\n" +
37+
" xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n" +
38+
" xmlns:mdui=\"urn:oasis:names:tc:SAML:metadata:ui\">\n" +
39+
" \n" +
40+
" <md:IDPSSODescriptor protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n" +
41+
" <md:Extensions>\n" +
42+
" <shibmd:Scope regexp=\"false\">example.com</shibmd:Scope>\n" +
43+
" \n" +
44+
" <mdui:UIInfo>\n" +
45+
" <mdui:DisplayName xml:lang=\"en\">\n" +
46+
" Consortium GARR IdP\n" +
47+
" </mdui:DisplayName>\n" +
48+
" <mdui:DisplayName xml:lang=\"it\">\n" +
49+
" Consortium GARR IdP\n" +
50+
" </mdui:DisplayName>\n" +
51+
" \n" +
52+
" <mdui:Description xml:lang=\"en\">\n" +
53+
" This Identity Provider gives support for the Consortium GARR's user community\n" +
54+
" </mdui:Description>\n" +
55+
" <mdui:Description xml:lang=\"it\">\n" +
56+
" Questo Identity Provider di test fornisce supporto alla comunita' utenti GARR\n" +
57+
" </mdui:Description>\n" +
58+
" </mdui:UIInfo>\n" +
59+
" </md:Extensions>\n" +
60+
" \n" +
61+
" <md:KeyDescriptor>\n" +
62+
" <ds:KeyInfo>\n" +
63+
" <ds:X509Data>\n" +
64+
" <ds:X509Certificate>\n" +
65+
" MIIDZjCCAk6gAwIBAgIVAL9O+PA7SXtlwZZY8MVSE9On1cVWMA0GCSqGSIb3DQEB\n" +
66+
" BQUAMCkxJzAlBgNVBAMTHmlkZW0tcHVwYWdlbnQuZG16LWludC51bmltby5pdDAe\n" +
67+
" Fw0xMzA3MjQwMDQ0MTRaFw0zMzA3MjQwMDQ0MTRaMCkxJzAlBgNVBAMTHmlkZW0t\n" +
68+
" cHVwYWdlbnQuZG16LWludC51bmltby5pdDCCASIwDQYJKoZIhvcNAMIIDQADggEP\n" +
69+
" ADCCAQoCggEBAIAcp/VyzZGXUF99kwj4NvL/Rwv4YvBgLWzpCuoxqHZ/hmBwJtqS\n" +
70+
" v0y9METBPFbgsF3hCISnxbcmNVxf/D0MoeKtw1YPbsUmow/bFe+r72hZ+IVAcejN\n" +
71+
" iDJ7t5oTjsRN1t1SqvVVk6Ryk5AZhpFW+W9pE9N6c7kJ16Rp2/mbtax9OCzxpece\n" +
72+
" byi1eiLfIBmkcRawL/vCc2v6VLI18i6HsNVO3l2yGosKCbuSoGDx2fCdAOk/rgdz\n" +
73+
" cWOvFsIZSKuD+FVbSS/J9GVs7yotsS4PRl4iX9UMnfDnOMfO7bcBgbXtDl4SCU1v\n" +
74+
" dJrRw7IL/pLz34Rv9a8nYitrzrxtLOp3nYUCAwEAAaOBhDCBgTBgBgMIIDEEWTBX\n" +
75+
" gh5pZGVtLXB1cGFnZW50LmRtei1pbnQudW5pbW8uaXSGNWh0dHBzOi8vaWRlbS1w\n" +
76+
" dXBhZ2VudC5kbXotaW50LnVuaW1vLml0L2lkcC9zaGliYm9sZXRoMB0GA1UdDgQW\n" +
77+
" BBT8PANzz+adGnTRe8ldcyxAwe4VnzANBgkqhkiG9w0BAQUFAAOCAQEAOEnO8Clu\n" +
78+
" 9z/Lf/8XOOsTdxJbV29DIF3G8KoQsB3dBsLwPZVEAQIP6ceS32Xaxrl6FMTDDNkL\n" +
79+
" qUvvInUisw0+I5zZwYHybJQCletUWTnz58SC4C9G7FpuXHFZnOGtRcgGD1NOX4UU\n" +
80+
" duus/4nVcGSLhDjszZ70Xtj0gw2Sn46oQPHTJ81QZ3Y9ih+Aj1c9OtUSBwtWZFkU\n" +
81+
" yooAKoR8li68Yb21zN2N65AqV+ndL98M8xUYMKLONuAXStDeoVCipH6PJ09Z5U2p\n" +
82+
" V5p4IQRV6QBsNw9CISJFuHzkVYTH5ZxzN80Ru46vh4y2M0Nu8GQ9I085KoZkrf5e\n" +
83+
" Cq53OZt9ISjHEw==\n" +
84+
" </ds:X509Certificate>\n" +
85+
" </ds:X509Data>\n" +
86+
" </ds:KeyInfo>\n" +
87+
" </md:KeyDescriptor>\n" +
88+
" \n" +
89+
" <md:SingleSignOnService\n" +
90+
" Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n" +
91+
" Location=\"https://idp.example.com/idp/profile/SAML2/POST/SSO\"/>\n" +
92+
" </md:IDPSSODescriptor>\n" +
93+
" \n" +
94+
" <md:Organization>\n" +
95+
" <md:OrganizationName xml:lang=\"en\">\n" +
96+
" Consortium GARR\n" +
97+
" </md:OrganizationName>\n" +
98+
" <md:OrganizationName xml:lang=\"it\">\n" +
99+
" Consortium GARR\n" +
100+
" </md:OrganizationName>\n" +
101+
" \n" +
102+
" <md:OrganizationDisplayName xml:lang=\"en\">\n" +
103+
" Consortium GARR\n" +
104+
" </md:OrganizationDisplayName>\n" +
105+
" <md:OrganizationDisplayName xml:lang=\"it\">\n" +
106+
" Consortium GARR\n" +
107+
" </md:OrganizationDisplayName>\n" +
108+
" \n" +
109+
" <md:OrganizationURL xml:lang=\"it\">\n" +
110+
" https://example.org\n" +
111+
" </md:OrganizationURL>\n" +
112+
" </md:Organization>\n" +
113+
" \n" +
114+
" <md:ContactPerson contactType=\"technical\">\n" +
115+
" <md:EmailAddress>mailto:technical.contact@example.com</md:EmailAddress>\n" +
116+
" </md:ContactPerson>\n" +
117+
" \n" +
118+
"</md:EntityDescriptor>";
119+
120+
@Test
121+
public void fromMetadataLocationWhenResolvableThenPopulatesBuilder() throws Exception {
122+
try (MockWebServer server = new MockWebServer()) {
123+
server.enqueue(new MockResponse().setBody(IDP_SSO_DESCRIPTOR_PAYLOAD).setResponseCode(200));
124+
RelyingPartyRegistration registration = RelyingPartyRegistrations
125+
.fromMetadataLocation(server.url("/").toString())
126+
.entityId("rp")
127+
.build();
128+
RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails();
129+
assertThat(details.getEntityId()).isEqualTo("https://idp.example.com/idp/shibboleth");
130+
assertThat(details.getSingleSignOnServiceLocation())
131+
.isEqualTo("https://idp.example.com/idp/profile/SAML2/POST/SSO");
132+
assertThat(details.getSingleSignOnServiceBinding())
133+
.isEqualTo(Saml2MessageBinding.POST);
134+
assertThat(details.getVerificationX509Credentials()).hasSize(1);
135+
assertThat(details.getEncryptionX509Credentials()).hasSize(1);
136+
}
137+
}
138+
139+
@Test
140+
public void fromMetadataLocationWhenUnresolvableThenSaml2Exception() throws Exception {
141+
try (MockWebServer server = new MockWebServer()) {
142+
server.enqueue(new MockResponse().setBody(IDP_SSO_DESCRIPTOR_PAYLOAD).setResponseCode(200));
143+
String url = server.url("/").toString();
144+
server.shutdown();
145+
assertThatCode(() -> RelyingPartyRegistrations.fromMetadataLocation(url))
146+
.isInstanceOf(Saml2Exception.class);
147+
}
148+
}
149+
150+
@Test
151+
public void fromMetadataLocationWhenMalformedResponseThenSaml2Exception() throws Exception {
152+
try (MockWebServer server = new MockWebServer()) {
153+
server.enqueue(new MockResponse().setBody("malformed").setResponseCode(200));
154+
String url = server.url("/").toString();
155+
assertThatCode(() -> RelyingPartyRegistrations.fromMetadataLocation(url))
156+
.isInstanceOf(Saml2Exception.class);
157+
}
158+
}
159+
}

0 commit comments

Comments
 (0)