Skip to content

Commit 9a11cc8

Browse files
ryan.cassarjzheaux
ryan.cassar
authored andcommitted
Add File-based Metadata Resolution
Closes gh-9028
1 parent 78d5ffe commit 9a11cc8

File tree

6 files changed

+467
-136
lines changed

6 files changed

+467
-136
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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.io.InputStream;
20+
import java.security.cert.CertificateException;
21+
import java.security.cert.X509Certificate;
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
25+
import net.shibboleth.utilities.java.support.xml.ParserPool;
26+
import org.opensaml.core.config.ConfigurationService;
27+
import org.opensaml.core.xml.XMLObject;
28+
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
29+
import org.opensaml.core.xml.io.Unmarshaller;
30+
import org.opensaml.saml.common.xml.SAMLConstants;
31+
import org.opensaml.saml.saml2.metadata.EntitiesDescriptor;
32+
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
33+
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
34+
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
35+
import org.opensaml.saml.saml2.metadata.SingleSignOnService;
36+
import org.opensaml.security.credential.UsageType;
37+
import org.opensaml.xmlsec.keyinfo.KeyInfoSupport;
38+
import org.w3c.dom.Document;
39+
import org.w3c.dom.Element;
40+
41+
import org.springframework.security.saml2.Saml2Exception;
42+
import org.springframework.security.saml2.core.OpenSamlInitializationService;
43+
import org.springframework.security.saml2.core.Saml2X509Credential;
44+
45+
class OpenSamlAssertingPartyMetadataConverter {
46+
47+
static {
48+
OpenSamlInitializationService.initialize();
49+
}
50+
51+
private final XMLObjectProviderRegistry registry;
52+
53+
private final ParserPool parserPool;
54+
55+
/**
56+
* Creates a {@link OpenSamlAssertingPartyMetadataConverter}
57+
*/
58+
OpenSamlAssertingPartyMetadataConverter() {
59+
this.registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
60+
this.parserPool = this.registry.getParserPool();
61+
}
62+
63+
RelyingPartyRegistration.Builder convert(InputStream inputStream) {
64+
EntityDescriptor descriptor = entityDescriptor(inputStream);
65+
IDPSSODescriptor idpssoDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);
66+
if (idpssoDescriptor == null) {
67+
throw new Saml2Exception("Metadata response is missing the necessary IDPSSODescriptor element");
68+
}
69+
List<Saml2X509Credential> verification = new ArrayList<>();
70+
List<Saml2X509Credential> encryption = new ArrayList<>();
71+
for (KeyDescriptor keyDescriptor : idpssoDescriptor.getKeyDescriptors()) {
72+
if (keyDescriptor.getUse().equals(UsageType.SIGNING)) {
73+
List<X509Certificate> certificates = certificates(keyDescriptor);
74+
for (X509Certificate certificate : certificates) {
75+
verification.add(Saml2X509Credential.verification(certificate));
76+
}
77+
}
78+
if (keyDescriptor.getUse().equals(UsageType.ENCRYPTION)) {
79+
List<X509Certificate> certificates = certificates(keyDescriptor);
80+
for (X509Certificate certificate : certificates) {
81+
encryption.add(Saml2X509Credential.encryption(certificate));
82+
}
83+
}
84+
if (keyDescriptor.getUse().equals(UsageType.UNSPECIFIED)) {
85+
List<X509Certificate> certificates = certificates(keyDescriptor);
86+
for (X509Certificate certificate : certificates) {
87+
verification.add(Saml2X509Credential.verification(certificate));
88+
encryption.add(Saml2X509Credential.encryption(certificate));
89+
}
90+
}
91+
}
92+
if (verification.isEmpty()) {
93+
throw new Saml2Exception(
94+
"Metadata response is missing verification certificates, necessary for verifying SAML assertions");
95+
}
96+
RelyingPartyRegistration.Builder builder = RelyingPartyRegistration.withRegistrationId(descriptor.getEntityID())
97+
.assertingPartyDetails((party) -> party.entityId(descriptor.getEntityID())
98+
.wantAuthnRequestsSigned(Boolean.TRUE.equals(idpssoDescriptor.getWantAuthnRequestsSigned()))
99+
.verificationX509Credentials((c) -> c.addAll(verification))
100+
.encryptionX509Credentials((c) -> c.addAll(encryption)));
101+
for (SingleSignOnService singleSignOnService : idpssoDescriptor.getSingleSignOnServices()) {
102+
Saml2MessageBinding binding;
103+
if (singleSignOnService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) {
104+
binding = Saml2MessageBinding.POST;
105+
}
106+
else if (singleSignOnService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) {
107+
binding = Saml2MessageBinding.REDIRECT;
108+
}
109+
else {
110+
continue;
111+
}
112+
builder.assertingPartyDetails(
113+
(party) -> party.singleSignOnServiceLocation(singleSignOnService.getLocation())
114+
.singleSignOnServiceBinding(binding));
115+
return builder;
116+
}
117+
throw new Saml2Exception(
118+
"Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests");
119+
}
120+
121+
private List<X509Certificate> certificates(KeyDescriptor keyDescriptor) {
122+
try {
123+
return KeyInfoSupport.getCertificates(keyDescriptor.getKeyInfo());
124+
}
125+
catch (CertificateException ex) {
126+
throw new Saml2Exception(ex);
127+
}
128+
}
129+
130+
private EntityDescriptor entityDescriptor(InputStream inputStream) {
131+
Document document = document(inputStream);
132+
Element element = document.getDocumentElement();
133+
Unmarshaller unmarshaller = this.registry.getUnmarshallerFactory().getUnmarshaller(element);
134+
if (unmarshaller == null) {
135+
throw new Saml2Exception("Unsupported element of type " + element.getTagName());
136+
}
137+
try {
138+
XMLObject object = unmarshaller.unmarshall(element);
139+
if (object instanceof EntitiesDescriptor) {
140+
return ((EntitiesDescriptor) object).getEntityDescriptors().get(0);
141+
}
142+
if (object instanceof EntityDescriptor) {
143+
return (EntityDescriptor) object;
144+
}
145+
}
146+
catch (Exception ex) {
147+
throw new Saml2Exception(ex);
148+
}
149+
throw new Saml2Exception("Unsupported element of type " + element.getTagName());
150+
}
151+
152+
private Document document(InputStream inputStream) {
153+
try {
154+
return this.parserPool.parse(inputStream);
155+
}
156+
catch (Exception ex) {
157+
throw new Saml2Exception(ex);
158+
}
159+
}
160+
161+
}

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

+3-122
Original file line numberDiff line numberDiff line change
@@ -17,38 +17,16 @@
1717
package org.springframework.security.saml2.provider.service.registration;
1818

1919
import java.io.IOException;
20-
import java.io.InputStream;
21-
import java.security.cert.CertificateException;
22-
import java.security.cert.X509Certificate;
23-
import java.util.ArrayList;
2420
import java.util.Arrays;
2521
import java.util.List;
2622

27-
import net.shibboleth.utilities.java.support.xml.ParserPool;
28-
import org.opensaml.core.config.ConfigurationService;
29-
import org.opensaml.core.xml.XMLObject;
30-
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
31-
import org.opensaml.core.xml.io.Unmarshaller;
32-
import org.opensaml.saml.common.xml.SAMLConstants;
33-
import org.opensaml.saml.saml2.metadata.EntitiesDescriptor;
34-
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
35-
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
36-
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
37-
import org.opensaml.saml.saml2.metadata.SingleSignOnService;
38-
import org.opensaml.security.credential.UsageType;
39-
import org.opensaml.xmlsec.keyinfo.KeyInfoSupport;
40-
import org.w3c.dom.Document;
41-
import org.w3c.dom.Element;
42-
4323
import org.springframework.http.HttpInputMessage;
4424
import org.springframework.http.HttpOutputMessage;
4525
import org.springframework.http.MediaType;
4626
import org.springframework.http.converter.HttpMessageConverter;
4727
import org.springframework.http.converter.HttpMessageNotReadableException;
4828
import org.springframework.http.converter.HttpMessageNotWritableException;
49-
import org.springframework.security.saml2.Saml2Exception;
5029
import org.springframework.security.saml2.core.OpenSamlInitializationService;
51-
import org.springframework.security.saml2.core.Saml2X509Credential;
5230

5331
/**
5432
* An {@link HttpMessageConverter} that takes an {@code IDPSSODescriptor} in an HTTP
@@ -84,16 +62,13 @@ public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter
8462
OpenSamlInitializationService.initialize();
8563
}
8664

87-
private final XMLObjectProviderRegistry registry;
88-
89-
private final ParserPool parserPool;
65+
private final OpenSamlAssertingPartyMetadataConverter converter;
9066

9167
/**
9268
* Creates a {@link OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter}
9369
*/
9470
public OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter() {
95-
this.registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
96-
this.parserPool = this.registry.getParserPool();
71+
this.converter = new OpenSamlAssertingPartyMetadataConverter();
9772
}
9873

9974
@Override
@@ -114,101 +89,7 @@ public List<MediaType> getSupportedMediaTypes() {
11489
@Override
11590
public RelyingPartyRegistration.Builder read(Class<? extends RelyingPartyRegistration.Builder> clazz,
11691
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
117-
EntityDescriptor descriptor = entityDescriptor(inputMessage.getBody());
118-
IDPSSODescriptor idpssoDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);
119-
if (idpssoDescriptor == null) {
120-
throw new Saml2Exception("Metadata response is missing the necessary IDPSSODescriptor element");
121-
}
122-
List<Saml2X509Credential> verification = new ArrayList<>();
123-
List<Saml2X509Credential> encryption = new ArrayList<>();
124-
for (KeyDescriptor keyDescriptor : idpssoDescriptor.getKeyDescriptors()) {
125-
if (keyDescriptor.getUse().equals(UsageType.SIGNING)) {
126-
List<X509Certificate> certificates = certificates(keyDescriptor);
127-
for (X509Certificate certificate : certificates) {
128-
verification.add(Saml2X509Credential.verification(certificate));
129-
}
130-
}
131-
if (keyDescriptor.getUse().equals(UsageType.ENCRYPTION)) {
132-
List<X509Certificate> certificates = certificates(keyDescriptor);
133-
for (X509Certificate certificate : certificates) {
134-
encryption.add(Saml2X509Credential.encryption(certificate));
135-
}
136-
}
137-
if (keyDescriptor.getUse().equals(UsageType.UNSPECIFIED)) {
138-
List<X509Certificate> certificates = certificates(keyDescriptor);
139-
for (X509Certificate certificate : certificates) {
140-
verification.add(Saml2X509Credential.verification(certificate));
141-
encryption.add(Saml2X509Credential.encryption(certificate));
142-
}
143-
}
144-
}
145-
if (verification.isEmpty()) {
146-
throw new Saml2Exception(
147-
"Metadata response is missing verification certificates, necessary for verifying SAML assertions");
148-
}
149-
RelyingPartyRegistration.Builder builder = RelyingPartyRegistration.withRegistrationId(descriptor.getEntityID())
150-
.assertingPartyDetails((party) -> party.entityId(descriptor.getEntityID())
151-
.wantAuthnRequestsSigned(Boolean.TRUE.equals(idpssoDescriptor.getWantAuthnRequestsSigned()))
152-
.verificationX509Credentials((c) -> c.addAll(verification))
153-
.encryptionX509Credentials((c) -> c.addAll(encryption)));
154-
for (SingleSignOnService singleSignOnService : idpssoDescriptor.getSingleSignOnServices()) {
155-
Saml2MessageBinding binding;
156-
if (singleSignOnService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) {
157-
binding = Saml2MessageBinding.POST;
158-
}
159-
else if (singleSignOnService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) {
160-
binding = Saml2MessageBinding.REDIRECT;
161-
}
162-
else {
163-
continue;
164-
}
165-
builder.assertingPartyDetails(
166-
(party) -> party.singleSignOnServiceLocation(singleSignOnService.getLocation())
167-
.singleSignOnServiceBinding(binding));
168-
return builder;
169-
}
170-
throw new Saml2Exception(
171-
"Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests");
172-
}
173-
174-
private List<X509Certificate> certificates(KeyDescriptor keyDescriptor) {
175-
try {
176-
return KeyInfoSupport.getCertificates(keyDescriptor.getKeyInfo());
177-
}
178-
catch (CertificateException ex) {
179-
throw new Saml2Exception(ex);
180-
}
181-
}
182-
183-
private EntityDescriptor entityDescriptor(InputStream inputStream) {
184-
Document document = document(inputStream);
185-
Element element = document.getDocumentElement();
186-
Unmarshaller unmarshaller = this.registry.getUnmarshallerFactory().getUnmarshaller(element);
187-
if (unmarshaller == null) {
188-
throw new Saml2Exception("Unsupported element of type " + element.getTagName());
189-
}
190-
try {
191-
XMLObject object = unmarshaller.unmarshall(element);
192-
if (object instanceof EntitiesDescriptor) {
193-
return ((EntitiesDescriptor) object).getEntityDescriptors().get(0);
194-
}
195-
if (object instanceof EntityDescriptor) {
196-
return (EntityDescriptor) object;
197-
}
198-
}
199-
catch (Exception ex) {
200-
throw new Saml2Exception(ex);
201-
}
202-
throw new Saml2Exception("Unsupported element of type " + element.getTagName());
203-
}
204-
205-
private Document document(InputStream inputStream) {
206-
try {
207-
return this.parserPool.parse(inputStream);
208-
}
209-
catch (Exception ex) {
210-
throw new Saml2Exception(ex);
211-
}
92+
return this.converter.convert(inputMessage.getBody());
21293
}
21394

21495
@Override

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

+24-11
Original file line numberDiff line numberDiff line change
@@ -16,36 +16,48 @@
1616

1717
package org.springframework.security.saml2.provider.service.registration;
1818

19-
import java.util.Arrays;
19+
import java.io.IOException;
20+
import java.io.InputStream;
2021

22+
import org.springframework.core.io.DefaultResourceLoader;
23+
import org.springframework.core.io.ResourceLoader;
2124
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;
2525

2626
/**
2727
* A utility class for constructing instances of {@link RelyingPartyRegistration}
2828
*
2929
* @author Josh Cummings
30+
* @author Ryan Cassar
3031
* @since 5.4
3132
*/
3233
public final class RelyingPartyRegistrations {
3334

34-
private static final RestOperations rest = new RestTemplate(
35-
Arrays.asList(new OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter()));
35+
private static final OpenSamlAssertingPartyMetadataConverter assertingPartyMetadataConverter = new OpenSamlAssertingPartyMetadataConverter();
36+
37+
private static final ResourceLoader resourceLoader = new DefaultResourceLoader();
3638

3739
private RelyingPartyRegistrations() {
3840
}
3941

4042
/**
4143
* Return a {@link RelyingPartyRegistration.Builder} based off of the given SAML 2.0
42-
* Asserting Party (IDP) metadata.
44+
* Asserting Party (IDP) metadata location.
45+
*
46+
* Valid locations can be classpath- or file-based or they can be HTTP endpoints. Some
47+
* valid endpoints might include:
48+
*
49+
* <pre>
50+
* metadataLocation = "classpath:asserting-party-metadata.xml";
51+
* metadataLocation = "file:asserting-party-metadata.xml";
52+
* metadataLocation = "https://ap.example.org/metadata";
53+
* </pre>
4354
*
4455
* Note that by default the registrationId is set to be the given metadata location,
4556
* but this will most often not be sufficient. To complete the configuration, most
4657
* applications will also need to provide a registrationId, like so:
4758
*
4859
* <pre>
60+
* String metadataLocation = "file:C:\\saml\\metadata.xml"
4961
* RelyingPartyRegistration registration = RelyingPartyRegistrations
5062
* .fromMetadataLocation(metadataLocation)
5163
* .registrationId("registration-id")
@@ -56,14 +68,15 @@ private RelyingPartyRegistrations() {
5668
* about the asserting party. Thus, you will need to remember to still populate
5769
* anything about the relying party, like any private keys the relying party will use
5870
* for signing AuthnRequests.
59-
* @param metadataLocation
71+
* @param metadataLocation The classpath- or file-based locations or HTTP endpoints of
72+
* the asserting party metadata file
6073
* @return the {@link RelyingPartyRegistration.Builder} for further configuration
6174
*/
6275
public static RelyingPartyRegistration.Builder fromMetadataLocation(String metadataLocation) {
63-
try {
64-
return rest.getForObject(metadataLocation, RelyingPartyRegistration.Builder.class);
76+
try (InputStream source = resourceLoader.getResource(metadataLocation).getInputStream()) {
77+
return assertingPartyMetadataConverter.convert(source);
6578
}
66-
catch (RestClientException ex) {
79+
catch (IOException ex) {
6780
if (ex.getCause() instanceof Saml2Exception) {
6881
throw (Saml2Exception) ex.getCause();
6982
}

0 commit comments

Comments
 (0)