diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverter.java new file mode 100644 index 0000000000..71127120e4 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverter.java @@ -0,0 +1,161 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.registration; + +import java.io.InputStream; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +import net.shibboleth.utilities.java.support.xml.ParserPool; +import org.opensaml.core.config.ConfigurationService; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.opensaml.core.xml.io.Unmarshaller; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.saml2.metadata.EntitiesDescriptor; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; +import org.opensaml.saml.saml2.metadata.IDPSSODescriptor; +import org.opensaml.saml.saml2.metadata.KeyDescriptor; +import org.opensaml.saml.saml2.metadata.SingleSignOnService; +import org.opensaml.security.credential.UsageType; +import org.opensaml.xmlsec.keyinfo.KeyInfoSupport; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.core.Saml2X509Credential; + +class OpenSamlAssertingPartyMetadataConverter { + + static { + OpenSamlInitializationService.initialize(); + } + + private final XMLObjectProviderRegistry registry; + + private final ParserPool parserPool; + + /** + * Creates a {@link OpenSamlAssertingPartyMetadataConverter} + */ + OpenSamlAssertingPartyMetadataConverter() { + this.registry = ConfigurationService.get(XMLObjectProviderRegistry.class); + this.parserPool = this.registry.getParserPool(); + } + + RelyingPartyRegistration.Builder convert(InputStream inputStream) { + EntityDescriptor descriptor = entityDescriptor(inputStream); + IDPSSODescriptor idpssoDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS); + if (idpssoDescriptor == null) { + throw new Saml2Exception("Metadata response is missing the necessary IDPSSODescriptor element"); + } + List verification = new ArrayList<>(); + List encryption = new ArrayList<>(); + for (KeyDescriptor keyDescriptor : idpssoDescriptor.getKeyDescriptors()) { + if (keyDescriptor.getUse().equals(UsageType.SIGNING)) { + List certificates = certificates(keyDescriptor); + for (X509Certificate certificate : certificates) { + verification.add(Saml2X509Credential.verification(certificate)); + } + } + if (keyDescriptor.getUse().equals(UsageType.ENCRYPTION)) { + List certificates = certificates(keyDescriptor); + for (X509Certificate certificate : certificates) { + encryption.add(Saml2X509Credential.encryption(certificate)); + } + } + if (keyDescriptor.getUse().equals(UsageType.UNSPECIFIED)) { + List certificates = certificates(keyDescriptor); + for (X509Certificate certificate : certificates) { + verification.add(Saml2X509Credential.verification(certificate)); + encryption.add(Saml2X509Credential.encryption(certificate)); + } + } + } + if (verification.isEmpty()) { + throw new Saml2Exception( + "Metadata response is missing verification certificates, necessary for verifying SAML assertions"); + } + RelyingPartyRegistration.Builder builder = RelyingPartyRegistration.withRegistrationId(descriptor.getEntityID()) + .assertingPartyDetails((party) -> party.entityId(descriptor.getEntityID()) + .wantAuthnRequestsSigned(Boolean.TRUE.equals(idpssoDescriptor.getWantAuthnRequestsSigned())) + .verificationX509Credentials((c) -> c.addAll(verification)) + .encryptionX509Credentials((c) -> c.addAll(encryption))); + for (SingleSignOnService singleSignOnService : idpssoDescriptor.getSingleSignOnServices()) { + Saml2MessageBinding binding; + if (singleSignOnService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) { + binding = Saml2MessageBinding.POST; + } + else if (singleSignOnService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) { + binding = Saml2MessageBinding.REDIRECT; + } + else { + continue; + } + builder.assertingPartyDetails( + (party) -> party.singleSignOnServiceLocation(singleSignOnService.getLocation()) + .singleSignOnServiceBinding(binding)); + return builder; + } + throw new Saml2Exception( + "Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests"); + } + + private List certificates(KeyDescriptor keyDescriptor) { + try { + return KeyInfoSupport.getCertificates(keyDescriptor.getKeyInfo()); + } + catch (CertificateException ex) { + throw new Saml2Exception(ex); + } + } + + private EntityDescriptor entityDescriptor(InputStream inputStream) { + Document document = document(inputStream); + Element element = document.getDocumentElement(); + Unmarshaller unmarshaller = this.registry.getUnmarshallerFactory().getUnmarshaller(element); + if (unmarshaller == null) { + throw new Saml2Exception("Unsupported element of type " + element.getTagName()); + } + try { + XMLObject object = unmarshaller.unmarshall(element); + if (object instanceof EntitiesDescriptor) { + return ((EntitiesDescriptor) object).getEntityDescriptors().get(0); + } + if (object instanceof EntityDescriptor) { + return (EntityDescriptor) object; + } + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + throw new Saml2Exception("Unsupported element of type " + element.getTagName()); + } + + private Document document(InputStream inputStream) { + try { + return this.parserPool.parse(inputStream); + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java index 1e43aec582..6e5284a941 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java @@ -17,38 +17,16 @@ package org.springframework.security.saml2.provider.service.registration; import java.io.IOException; -import java.io.InputStream; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import net.shibboleth.utilities.java.support.xml.ParserPool; -import org.opensaml.core.config.ConfigurationService; -import org.opensaml.core.xml.XMLObject; -import org.opensaml.core.xml.config.XMLObjectProviderRegistry; -import org.opensaml.core.xml.io.Unmarshaller; -import org.opensaml.saml.common.xml.SAMLConstants; -import org.opensaml.saml.saml2.metadata.EntitiesDescriptor; -import org.opensaml.saml.saml2.metadata.EntityDescriptor; -import org.opensaml.saml.saml2.metadata.IDPSSODescriptor; -import org.opensaml.saml.saml2.metadata.KeyDescriptor; -import org.opensaml.saml.saml2.metadata.SingleSignOnService; -import org.opensaml.security.credential.UsageType; -import org.opensaml.xmlsec.keyinfo.KeyInfoSupport; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; -import org.springframework.security.saml2.Saml2Exception; import org.springframework.security.saml2.core.OpenSamlInitializationService; -import org.springframework.security.saml2.core.Saml2X509Credential; /** * An {@link HttpMessageConverter} that takes an {@code IDPSSODescriptor} in an HTTP @@ -84,16 +62,13 @@ public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter OpenSamlInitializationService.initialize(); } - private final XMLObjectProviderRegistry registry; - - private final ParserPool parserPool; + private final OpenSamlAssertingPartyMetadataConverter converter; /** * Creates a {@link OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter} */ public OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter() { - this.registry = ConfigurationService.get(XMLObjectProviderRegistry.class); - this.parserPool = this.registry.getParserPool(); + this.converter = new OpenSamlAssertingPartyMetadataConverter(); } @Override @@ -114,101 +89,7 @@ public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter @Override public RelyingPartyRegistration.Builder read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { - EntityDescriptor descriptor = entityDescriptor(inputMessage.getBody()); - IDPSSODescriptor idpssoDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS); - if (idpssoDescriptor == null) { - throw new Saml2Exception("Metadata response is missing the necessary IDPSSODescriptor element"); - } - List verification = new ArrayList<>(); - List encryption = new ArrayList<>(); - for (KeyDescriptor keyDescriptor : idpssoDescriptor.getKeyDescriptors()) { - if (keyDescriptor.getUse().equals(UsageType.SIGNING)) { - List certificates = certificates(keyDescriptor); - for (X509Certificate certificate : certificates) { - verification.add(Saml2X509Credential.verification(certificate)); - } - } - if (keyDescriptor.getUse().equals(UsageType.ENCRYPTION)) { - List certificates = certificates(keyDescriptor); - for (X509Certificate certificate : certificates) { - encryption.add(Saml2X509Credential.encryption(certificate)); - } - } - if (keyDescriptor.getUse().equals(UsageType.UNSPECIFIED)) { - List certificates = certificates(keyDescriptor); - for (X509Certificate certificate : certificates) { - verification.add(Saml2X509Credential.verification(certificate)); - encryption.add(Saml2X509Credential.encryption(certificate)); - } - } - } - if (verification.isEmpty()) { - throw new Saml2Exception( - "Metadata response is missing verification certificates, necessary for verifying SAML assertions"); - } - RelyingPartyRegistration.Builder builder = RelyingPartyRegistration.withRegistrationId(descriptor.getEntityID()) - .assertingPartyDetails((party) -> party.entityId(descriptor.getEntityID()) - .wantAuthnRequestsSigned(Boolean.TRUE.equals(idpssoDescriptor.getWantAuthnRequestsSigned())) - .verificationX509Credentials((c) -> c.addAll(verification)) - .encryptionX509Credentials((c) -> c.addAll(encryption))); - for (SingleSignOnService singleSignOnService : idpssoDescriptor.getSingleSignOnServices()) { - Saml2MessageBinding binding; - if (singleSignOnService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) { - binding = Saml2MessageBinding.POST; - } - else if (singleSignOnService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) { - binding = Saml2MessageBinding.REDIRECT; - } - else { - continue; - } - builder.assertingPartyDetails( - (party) -> party.singleSignOnServiceLocation(singleSignOnService.getLocation()) - .singleSignOnServiceBinding(binding)); - return builder; - } - throw new Saml2Exception( - "Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests"); - } - - private List certificates(KeyDescriptor keyDescriptor) { - try { - return KeyInfoSupport.getCertificates(keyDescriptor.getKeyInfo()); - } - catch (CertificateException ex) { - throw new Saml2Exception(ex); - } - } - - private EntityDescriptor entityDescriptor(InputStream inputStream) { - Document document = document(inputStream); - Element element = document.getDocumentElement(); - Unmarshaller unmarshaller = this.registry.getUnmarshallerFactory().getUnmarshaller(element); - if (unmarshaller == null) { - throw new Saml2Exception("Unsupported element of type " + element.getTagName()); - } - try { - XMLObject object = unmarshaller.unmarshall(element); - if (object instanceof EntitiesDescriptor) { - return ((EntitiesDescriptor) object).getEntityDescriptors().get(0); - } - if (object instanceof EntityDescriptor) { - return (EntityDescriptor) object; - } - } - catch (Exception ex) { - throw new Saml2Exception(ex); - } - throw new Saml2Exception("Unsupported element of type " + element.getTagName()); - } - - private Document document(InputStream inputStream) { - try { - return this.parserPool.parse(inputStream); - } - catch (Exception ex) { - throw new Saml2Exception(ex); - } + return this.converter.convert(inputMessage.getBody()); } @Override diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrations.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrations.java index 05e8695fb4..e765a73259 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrations.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrations.java @@ -16,36 +16,48 @@ package org.springframework.security.saml2.provider.service.registration; -import java.util.Arrays; +import java.io.IOException; +import java.io.InputStream; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.ResourceLoader; import org.springframework.security.saml2.Saml2Exception; -import org.springframework.web.client.RestClientException; -import org.springframework.web.client.RestOperations; -import org.springframework.web.client.RestTemplate; /** * A utility class for constructing instances of {@link RelyingPartyRegistration} * * @author Josh Cummings + * @author Ryan Cassar * @since 5.4 */ public final class RelyingPartyRegistrations { - private static final RestOperations rest = new RestTemplate( - Arrays.asList(new OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter())); + private static final OpenSamlAssertingPartyMetadataConverter assertingPartyMetadataConverter = new OpenSamlAssertingPartyMetadataConverter(); + + private static final ResourceLoader resourceLoader = new DefaultResourceLoader(); private RelyingPartyRegistrations() { } /** * Return a {@link RelyingPartyRegistration.Builder} based off of the given SAML 2.0 - * Asserting Party (IDP) metadata. + * Asserting Party (IDP) metadata location. + * + * Valid locations can be classpath- or file-based or they can be HTTP endpoints. Some + * valid endpoints might include: + * + *
+	 *   metadataLocation = "classpath:asserting-party-metadata.xml";
+	 *   metadataLocation = "file:asserting-party-metadata.xml";
+	 *   metadataLocation = "https://ap.example.org/metadata";
+	 * 
* * Note that by default the registrationId is set to be the given metadata location, * but this will most often not be sufficient. To complete the configuration, most * applications will also need to provide a registrationId, like so: * *
+	 *  String metadataLocation = "file:C:\\saml\\metadata.xml"
 	 *	RelyingPartyRegistration registration = RelyingPartyRegistrations
 	 * 		.fromMetadataLocation(metadataLocation)
 	 * 		.registrationId("registration-id")
@@ -56,14 +68,15 @@ public final class RelyingPartyRegistrations {
 	 * about the asserting party. Thus, you will need to remember to still populate
 	 * anything about the relying party, like any private keys the relying party will use
 	 * for signing AuthnRequests.
-	 * @param metadataLocation
+	 * @param metadataLocation The classpath- or file-based locations or HTTP endpoints of
+	 * the asserting party metadata file
 	 * @return the {@link RelyingPartyRegistration.Builder} for further configuration
 	 */
 	public static RelyingPartyRegistration.Builder fromMetadataLocation(String metadataLocation) {
-		try {
-			return rest.getForObject(metadataLocation, RelyingPartyRegistration.Builder.class);
+		try (InputStream source = resourceLoader.getResource(metadataLocation).getInputStream()) {
+			return assertingPartyMetadataConverter.convert(source);
 		}
-		catch (RestClientException ex) {
+		catch (IOException ex) {
 			if (ex.getCause() instanceof Saml2Exception) {
 				throw (Saml2Exception) ex.getCause();
 			}
diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverterTests.java
new file mode 100644
index 0000000000..0667a0a569
--- /dev/null
+++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataConverterTests.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2002-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.saml2.provider.service.registration;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.springframework.security.saml2.Saml2Exception;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+public class OpenSamlAssertingPartyMetadataConverterTests {
+
+	private static final String CERTIFICATE = "MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYDVQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwXc2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0BwaXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAaBgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQDDBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlrQHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduOnRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+vZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLuxbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6zV9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk";
+
+	private static final String ENTITIES_DESCRIPTOR_TEMPLATE = "\n%s";
+
+	private static final String ENTITY_DESCRIPTOR_TEMPLATE = "\n%s"
+			+ "";
+
+	private static final String IDP_SSO_DESCRIPTOR_TEMPLATE = "\n"
+			+ "%s\n" + "";
+
+	private static final String KEY_DESCRIPTOR_TEMPLATE = "\n"
+			+ "\n" + "\n"
+			+ "" + CERTIFICATE + "\n" + "\n" + "\n"
+			+ "";
+
+	private static final String SINGLE_SIGN_ON_SERVICE_TEMPLATE = "";
+
+	private OpenSamlAssertingPartyMetadataConverter converter;
+
+	@Before
+	public void setup() {
+		this.converter = new OpenSamlAssertingPartyMetadataConverter();
+	}
+
+	@Test
+	public void readWhenMissingIDPSSODescriptorThenException() {
+		String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, "");
+		InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
+		assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream))
+				.withMessageContaining("Metadata response is missing the necessary IDPSSODescriptor element");
+	}
+
+	@Test
+	public void readWhenMissingVerificationKeyThenException() {
+		String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, ""));
+		InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
+		assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream))
+				.withMessageContaining(
+						"Metadata response is missing verification certificates, necessary for verifying SAML assertions");
+	}
+
+	@Test
+	public void readWhenMissingSingleSignOnServiceThenException() {
+		String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE,
+				String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\"")));
+		InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
+		assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream))
+				.withMessageContaining(
+						"Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests");
+	}
+
+	@Test
+	public void readWhenDescriptorFullySpecifiedThenConfigures() throws Exception {
+		String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE,
+				String.format(IDP_SSO_DESCRIPTOR_TEMPLATE,
+						String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\"")
+								+ String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"encryption\"")
+								+ String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE)));
+		InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
+		RelyingPartyRegistration registration = this.converter.convert(inputStream).registrationId("one").build();
+		RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails();
+		assertThat(details.getWantAuthnRequestsSigned()).isFalse();
+		assertThat(details.getSingleSignOnServiceLocation()).isEqualTo("sso-location");
+		assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
+		assertThat(details.getEntityId()).isEqualTo("entity-id");
+		assertThat(details.getVerificationX509Credentials()).hasSize(1);
+		assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate())
+				.isEqualTo(x509Certificate(CERTIFICATE));
+		assertThat(details.getEncryptionX509Credentials()).hasSize(1);
+		assertThat(details.getEncryptionX509Credentials().iterator().next().getCertificate())
+				.isEqualTo(x509Certificate(CERTIFICATE));
+	}
+
+	// gh-9051
+	@Test
+	public void readWhenEntitiesDescriptorThenConfigures() throws Exception {
+		String payload = String.format(ENTITIES_DESCRIPTOR_TEMPLATE,
+				String.format(ENTITY_DESCRIPTOR_TEMPLATE,
+						String.format(IDP_SSO_DESCRIPTOR_TEMPLATE,
+								String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\"")
+										+ String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"encryption\"")
+										+ String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE))));
+		InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
+		RelyingPartyRegistration registration = this.converter.convert(inputStream).registrationId("one").build();
+		RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails();
+		assertThat(details.getWantAuthnRequestsSigned()).isFalse();
+		assertThat(details.getSingleSignOnServiceLocation()).isEqualTo("sso-location");
+		assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
+		assertThat(details.getEntityId()).isEqualTo("entity-id");
+		assertThat(details.getVerificationX509Credentials()).hasSize(1);
+		assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate())
+				.isEqualTo(x509Certificate(CERTIFICATE));
+		assertThat(details.getEncryptionX509Credentials()).hasSize(1);
+		assertThat(details.getEncryptionX509Credentials().iterator().next().getCertificate())
+				.isEqualTo(x509Certificate(CERTIFICATE));
+	}
+
+	@Test
+	public void readWhenKeyDescriptorHasNoUseThenConfiguresBothKeyTypes() throws Exception {
+		String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, String.format(IDP_SSO_DESCRIPTOR_TEMPLATE,
+				String.format(KEY_DESCRIPTOR_TEMPLATE, "") + String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE)));
+		InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
+		RelyingPartyRegistration registration = this.converter.convert(inputStream).registrationId("one").build();
+		RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails();
+		assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate())
+				.isEqualTo(x509Certificate(CERTIFICATE));
+		assertThat(details.getEncryptionX509Credentials()).hasSize(1);
+		assertThat(details.getEncryptionX509Credentials().iterator().next().getCertificate())
+				.isEqualTo(x509Certificate(CERTIFICATE));
+	}
+
+	X509Certificate x509Certificate(String data) {
+		try {
+			InputStream certificate = new ByteArrayInputStream(Base64.getDecoder().decode(data.getBytes()));
+			return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(certificate);
+		}
+		catch (Exception ex) {
+			throw new IllegalArgumentException(ex);
+		}
+	}
+
+	// gh-9051
+	@Test
+	public void readWhenUnsupportedElementThenSaml2Exception() {
+		String payload = "";
+		InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
+		assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream))
+				.withMessage("Unsupported element of type saml2:Assertion");
+	}
+
+}
diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationsTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationsTests.java
index 3c64c8e653..5adb99a224 100644
--- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationsTests.java
+++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationsTests.java
@@ -16,6 +16,8 @@
 
 package org.springframework.security.saml2.provider.service.registration;
 
+import java.io.File;
+
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import org.junit.Test;
@@ -85,7 +87,7 @@ public class RelyingPartyRegistrationsTests {
 			+ "   \n" + "    \n" + "";
 
 	@Test
-	public void fromMetadataLocationWhenResolvableThenPopulatesBuilder() throws Exception {
+	public void fromMetadataUrlLocationWhenResolvableThenPopulatesBuilder() throws Exception {
 		try (MockWebServer server = new MockWebServer()) {
 			server.enqueue(new MockResponse().setBody(IDP_SSO_DESCRIPTOR_PAYLOAD).setResponseCode(200));
 			RelyingPartyRegistration registration = RelyingPartyRegistrations
@@ -101,7 +103,7 @@ public class RelyingPartyRegistrationsTests {
 	}
 
 	@Test
-	public void fromMetadataLocationWhenUnresolvableThenSaml2Exception() throws Exception {
+	public void fromMetadataUrlLocationWhenUnresolvableThenSaml2Exception() throws Exception {
 		try (MockWebServer server = new MockWebServer()) {
 			server.enqueue(new MockResponse().setBody(IDP_SSO_DESCRIPTOR_PAYLOAD).setResponseCode(200));
 			String url = server.url("/").toString();
@@ -112,7 +114,7 @@ public class RelyingPartyRegistrationsTests {
 	}
 
 	@Test
-	public void fromMetadataLocationWhenMalformedResponseThenSaml2Exception() throws Exception {
+	public void fromMetadataUrlLocationWhenMalformedResponseThenSaml2Exception() throws Exception {
 		try (MockWebServer server = new MockWebServer()) {
 			server.enqueue(new MockResponse().setBody("malformed").setResponseCode(200));
 			String url = server.url("/").toString();
@@ -121,4 +123,24 @@ public class RelyingPartyRegistrationsTests {
 		}
 	}
 
+	@Test
+	public void fromMetadataFileLocationWhenResolvableThenPopulatesBuilder() {
+		File file = new File("src/test/resources/test-metadata.xml");
+		RelyingPartyRegistration registration = RelyingPartyRegistrations
+				.fromMetadataLocation("file:" + file.getAbsolutePath()).entityId("rp").build();
+		RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails();
+		assertThat(details.getEntityId()).isEqualTo("https://idp.example.com/idp/shibboleth");
+		assertThat(details.getSingleSignOnServiceLocation())
+				.isEqualTo("https://idp.example.com/idp/profile/SAML2/POST/SSO");
+		assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST);
+		assertThat(details.getVerificationX509Credentials()).hasSize(1);
+		assertThat(details.getEncryptionX509Credentials()).hasSize(1);
+	}
+
+	@Test
+	public void fromMetadataFileLocationWhenNotFoundThenSaml2Exception() {
+		assertThatExceptionOfType(Saml2Exception.class)
+				.isThrownBy(() -> RelyingPartyRegistrations.fromMetadataLocation("filePath"));
+	}
+
 }
diff --git a/saml2/saml2-service-provider/src/test/resources/test-metadata.xml b/saml2/saml2-service-provider/src/test/resources/test-metadata.xml
new file mode 100644
index 0000000000..1e7afc7eb9
--- /dev/null
+++ b/saml2/saml2-service-provider/src/test/resources/test-metadata.xml
@@ -0,0 +1,86 @@
+
+
+	
+		
+			example.com
+
+			
+				
+					Consortium GARR IdP
+				
+				
+					Consortium GARR IdP
+				
+
+				
+					This Identity Provider gives support for the Consortium GARR's user community
+				
+				
+					Questo Identity Provider di test fornisce supporto alla comunita' utenti GARR
+				
+			
+		
+
+		
+			
+				
+					
+						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==
+					
+				
+			
+		
+
+		
+	
+
+	
+		
+			Consortium GARR
+		
+		
+			Consortium GARR
+		
+
+		
+			Consortium GARR
+		
+		
+			Consortium GARR
+		
+
+		
+			https://example.org
+		
+	
+
+	
+		mailto:technical.contact@example.com
+	
+
+