From 965e689461b4d4b96914f715df7fcffc08b56f22 Mon Sep 17 00:00:00 2001
From: Josh Cummings <josh.cummings@gmail.com>
Date: Thu, 27 Jan 2022 14:25:00 -0700
Subject: [PATCH] Add EntitiesDescriptor Support

Closes gh-10782
---
 ...enSamlAssertingPartyMetadataConverter.java |  35 ++--
 ...gistrationBuilderHttpMessageConverter.java |   4 +-
 .../RelyingPartyRegistrations.java            |  93 +++++++++-
 ...lAssertingPartyMetadataConverterTests.java |  11 +-
 .../RelyingPartyRegistrationsTests.java       | 116 +++++++++++-
 .../resources/test-entitiesdescriptor.xml     | 168 ++++++++++++++++++
 6 files changed, 407 insertions(+), 20 deletions(-)
 create mode 100644 saml2/saml2-service-provider/src/test/resources/test-entitiesdescriptor.xml

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
index 1b0eb0e35a..c642140e7e 100644
--- 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
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -20,6 +20,8 @@ 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.Collection;
 import java.util.List;
 
 import net.shibboleth.utilities.java.support.xml.ParserPool;
@@ -63,8 +65,24 @@ class OpenSamlAssertingPartyMetadataConverter {
 		this.parserPool = this.registry.getParserPool();
 	}
 
-	RelyingPartyRegistration.Builder convert(InputStream inputStream) {
-		EntityDescriptor descriptor = entityDescriptor(inputStream);
+	Collection<RelyingPartyRegistration.Builder> convert(InputStream inputStream) {
+		List<RelyingPartyRegistration.Builder> builders = new ArrayList<>();
+		XMLObject xmlObject = xmlObject(inputStream);
+		if (xmlObject instanceof EntitiesDescriptor) {
+			EntitiesDescriptor descriptors = (EntitiesDescriptor) xmlObject;
+			for (EntityDescriptor descriptor : descriptors.getEntityDescriptors()) {
+				builders.add(convert(descriptor));
+			}
+			return builders;
+		}
+		if (xmlObject instanceof EntityDescriptor) {
+			EntityDescriptor descriptor = (EntityDescriptor) xmlObject;
+			return Arrays.asList(convert(descriptor));
+		}
+		throw new Saml2Exception("Unsupported element of type " + xmlObject.getClass());
+	}
+
+	RelyingPartyRegistration.Builder convert(EntityDescriptor descriptor) {
 		IDPSSODescriptor idpssoDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);
 		if (idpssoDescriptor == null) {
 			throw new Saml2Exception("Metadata response is missing the necessary IDPSSODescriptor element");
@@ -167,7 +185,7 @@ class OpenSamlAssertingPartyMetadataConverter {
 		return signingMethods(extensions);
 	}
 
-	private EntityDescriptor entityDescriptor(InputStream inputStream) {
+	private XMLObject xmlObject(InputStream inputStream) {
 		Document document = document(inputStream);
 		Element element = document.getDocumentElement();
 		Unmarshaller unmarshaller = this.registry.getUnmarshallerFactory().getUnmarshaller(element);
@@ -175,18 +193,11 @@ class OpenSamlAssertingPartyMetadataConverter {
 			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;
-			}
+			return unmarshaller.unmarshall(element);
 		}
 		catch (Exception ex) {
 			throw new Saml2Exception(ex);
 		}
-		throw new Saml2Exception("Unsupported element of type " + element.getTagName());
 	}
 
 	private Document document(InputStream inputStream) {
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 6e5284a941..e7c908f742 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
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -89,7 +89,7 @@ public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter
 	@Override
 	public RelyingPartyRegistration.Builder read(Class<? extends RelyingPartyRegistration.Builder> clazz,
 			HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
-		return this.converter.convert(inputMessage.getBody());
+		return this.converter.convert(inputMessage.getBody()).iterator().next();
 	}
 
 	@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 2ed5246f64..2f3913f5b4 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
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -18,6 +18,7 @@ package org.springframework.security.saml2.provider.service.registration;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Collection;
 
 import org.springframework.core.io.DefaultResourceLoader;
 import org.springframework.core.io.ResourceLoader;
@@ -122,6 +123,96 @@ public final class RelyingPartyRegistrations {
 	 * @since 5.6
 	 */
 	public static RelyingPartyRegistration.Builder fromMetadata(InputStream source) {
+		return assertingPartyMetadataConverter.convert(source).iterator().next();
+	}
+
+	/**
+	 * Return a {@link Collection} of {@link RelyingPartyRegistration.Builder}s based off
+	 * of the given SAML 2.0 Asserting Party (IDP) metadata location.
+	 *
+	 * Valid locations can be classpath- or file-based or they can be HTTP endpoints. Some
+	 * valid endpoints might include:
+	 *
+	 * <pre>
+	 *   metadataLocation = "classpath:asserting-party-metadata.xml";
+	 *   metadataLocation = "file:asserting-party-metadata.xml";
+	 *   metadataLocation = "https://ap.example.org/metadata";
+	 * </pre>
+	 *
+	 * 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:
+	 *
+	 * <pre>
+	 *	Iterable&lt;RelyingPartyRegistration&gt; registrations = RelyingPartyRegistrations
+	 * 			.collectionFromMetadataLocation(location).iterator();
+	 * 	RelyingPartyRegistration one = registrations.next().registrationId("one").build();
+	 * 	RelyingPartyRegistration two = registrations.next().registrationId("two").build();
+	 * 	return new InMemoryRelyingPartyRegistrationRepository(one, two);
+	 * </pre>
+	 *
+	 * Also note that an {@code IDPSSODescriptor} typically only contains information
+	 * 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 location The classpath- or file-based locations or HTTP endpoints of the
+	 * asserting party metadata file
+	 * @return the {@link Collection} of {@link RelyingPartyRegistration.Builder}s for
+	 * further configuration
+	 * @since 5.7
+	 */
+	public static Collection<RelyingPartyRegistration.Builder> collectionFromMetadataLocation(String location) {
+		try (InputStream source = resourceLoader.getResource(location).getInputStream()) {
+			return collectionFromMetadata(source);
+		}
+		catch (IOException ex) {
+			if (ex.getCause() instanceof Saml2Exception) {
+				throw (Saml2Exception) ex.getCause();
+			}
+			throw new Saml2Exception(ex);
+		}
+	}
+
+	/**
+	 * Return a {@link Collection} of {@link RelyingPartyRegistration.Builder}s based off
+	 * of the given SAML 2.0 Asserting Party (IDP) metadata.
+	 *
+	 * <p>
+	 * This method is intended for scenarios when the metadata is looked up by a separate
+	 * mechanism. One such example is when the metadata is stored in a database.
+	 * </p>
+	 *
+	 * <p>
+	 * <strong>The callers of this method are accountable for closing the
+	 * {@code InputStream} source.</strong>
+	 * </p>
+	 *
+	 * 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:
+	 *
+	 * <pre>
+	 *	String xml = fromDatabase();
+	 *	try (InputStream source = new ByteArrayInputStream(xml.getBytes())) {
+	 *		Iterator&lt;RelyingPartyRegistration&gt; registrations = RelyingPartyRegistrations
+	 * 				.collectionFromMetadata(source).iterator();
+	 * 		RelyingPartyRegistration one = registrations.next().registrationId("one").build();
+	 * 		RelyingPartyRegistration two = registrations.next().registrationId("two").build();
+	 * 		return new InMemoryRelyingPartyRegistrationRepository(one, two);
+	 * 	}
+	 * </pre>
+	 *
+	 * Also note that an {@code IDPSSODescriptor} typically only contains information
+	 * 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 source the {@link InputStream} source containing the asserting party
+	 * metadata
+	 * @return the {@link Collection} of {@link RelyingPartyRegistration.Builder}s for
+	 * further configuration
+	 * @since 5.7
+	 */
+	public static Collection<RelyingPartyRegistration.Builder> collectionFromMetadata(InputStream source) {
 		return assertingPartyMetadataConverter.convert(source);
 	}
 
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
index 1083fb1629..ac81eba426 100644
--- 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
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -98,7 +98,8 @@ public class OpenSamlAssertingPartyMetadataConverterTests {
 								+ String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"encryption\"") + EXTENSIONS_TEMPLATE
 								+ String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE)));
 		InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
-		RelyingPartyRegistration registration = this.converter.convert(inputStream).registrationId("one").build();
+		RelyingPartyRegistration registration = this.converter.convert(inputStream).iterator().next()
+				.registrationId("one").build();
 		RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails();
 		assertThat(details.getWantAuthnRequestsSigned()).isFalse();
 		assertThat(details.getSigningAlgorithms()).containsExactly(SignatureConstants.ALGO_ID_DIGEST_SHA512);
@@ -123,7 +124,8 @@ public class OpenSamlAssertingPartyMetadataConverterTests {
 										+ 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 registration = this.converter.convert(inputStream).iterator().next()
+				.registrationId("one").build();
 		RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails();
 		assertThat(details.getWantAuthnRequestsSigned()).isFalse();
 		assertThat(details.getSingleSignOnServiceLocation()).isEqualTo("sso-location");
@@ -142,7 +144,8 @@ public class OpenSamlAssertingPartyMetadataConverterTests {
 		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 registration = this.converter.convert(inputStream).iterator().next()
+				.registrationId("one").build();
 		RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails();
 		assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate())
 				.isEqualTo(x509Certificate(CERTIFICATE));
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 0aadfbb532..9360475d68 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
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -21,6 +21,7 @@ import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.util.List;
 import java.util.stream.Collectors;
 
 import okhttp3.mockwebserver.MockResponse;
@@ -41,12 +42,18 @@ public class RelyingPartyRegistrationsTests {
 
 	private String metadata;
 
+	private String entitiesDescriptor;
+
 	@BeforeEach
 	public void setup() throws Exception {
 		ClassPathResource resource = new ClassPathResource("test-metadata.xml");
 		try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
 			this.metadata = reader.lines().collect(Collectors.joining());
 		}
+		resource = new ClassPathResource("test-entitiesdescriptor.xml");
+		try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
+			this.entitiesDescriptor = reader.lines().collect(Collectors.joining());
+		}
 	}
 
 	@Test
@@ -129,4 +136,111 @@ public class RelyingPartyRegistrationsTests {
 		}
 	}
 
+	@Test
+	public void collectionFromMetadataLocationWhenResolvableThenPopulatesBuilder() throws Exception {
+		try (MockWebServer server = new MockWebServer()) {
+			server.enqueue(new MockResponse().setBody(this.entitiesDescriptor).setResponseCode(200));
+			List<RelyingPartyRegistration> registrations = RelyingPartyRegistrations
+					.collectionFromMetadataLocation(server.url("/").toString()).stream()
+					.map((r) -> r.entityId("rp").build()).collect(Collectors.toList());
+			assertThat(registrations).hasSize(2);
+			RelyingPartyRegistration first = registrations.get(0);
+			RelyingPartyRegistration.AssertingPartyDetails details = first.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);
+			RelyingPartyRegistration second = registrations.get(1);
+			details = second.getAssertingPartyDetails();
+			assertThat(details.getEntityId()).isEqualTo("https://ap.example.org/idp/shibboleth");
+			assertThat(details.getSingleSignOnServiceLocation())
+					.isEqualTo("https://ap.example.org/idp/profile/SAML2/POST/SSO");
+			assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST);
+			assertThat(details.getVerificationX509Credentials()).hasSize(1);
+			assertThat(details.getEncryptionX509Credentials()).hasSize(1);
+		}
+	}
+
+	@Test
+	public void collectionFromMetadataLocationWhenUnresolvableThenSaml2Exception() throws Exception {
+		try (MockWebServer server = new MockWebServer()) {
+			server.enqueue(new MockResponse().setBody(this.metadata).setResponseCode(200));
+			String url = server.url("/").toString();
+			server.shutdown();
+			assertThatExceptionOfType(Saml2Exception.class)
+					.isThrownBy(() -> RelyingPartyRegistrations.collectionFromMetadataLocation(url));
+		}
+	}
+
+	@Test
+	public void collectionFromMetadataLocationWhenMalformedResponseThenSaml2Exception() throws Exception {
+		try (MockWebServer server = new MockWebServer()) {
+			server.enqueue(new MockResponse().setBody("malformed").setResponseCode(200));
+			String url = server.url("/").toString();
+			assertThatExceptionOfType(Saml2Exception.class)
+					.isThrownBy(() -> RelyingPartyRegistrations.collectionFromMetadataLocation(url));
+		}
+	}
+
+	@Test
+	public void collectionFromMetadataFileWhenResolvableThenPopulatesBuilder() {
+		File file = new File("src/test/resources/test-entitiesdescriptor.xml");
+		RelyingPartyRegistration registration = RelyingPartyRegistrations
+				.collectionFromMetadataLocation("file:" + file.getAbsolutePath()).stream()
+				.map((r) -> r.entityId("rp").build()).findFirst().get();
+		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 collectionFromMetadataFileWhenContainsOnlyEntityDescriptorThenPopulatesBuilder() {
+		File file = new File("src/test/resources/test-metadata.xml");
+		RelyingPartyRegistration registration = RelyingPartyRegistrations
+				.collectionFromMetadataLocation("file:" + file.getAbsolutePath()).stream()
+				.map((r) -> r.entityId("rp").build()).findFirst().get();
+		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 collectionFromMetadataFileWhenNotFoundThenSaml2Exception() {
+		assertThatExceptionOfType(Saml2Exception.class)
+				.isThrownBy(() -> RelyingPartyRegistrations.collectionFromMetadataLocation("filePath"));
+	}
+
+	@Test
+	public void collectionFromMetadataInputStreamWhenResolvableThenPopulatesBuilder() throws Exception {
+		try (InputStream source = new ByteArrayInputStream(this.entitiesDescriptor.getBytes())) {
+			RelyingPartyRegistration registration = RelyingPartyRegistrations.collectionFromMetadata(source).stream()
+					.map((r) -> r.entityId("rp").build()).findFirst().get();
+			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 collectionFromMetadataInputStreamWhenEmptyThenSaml2Exception() throws Exception {
+		try (InputStream source = new ByteArrayInputStream("".getBytes())) {
+			assertThatExceptionOfType(Saml2Exception.class)
+					.isThrownBy(() -> RelyingPartyRegistrations.collectionFromMetadata(source));
+		}
+	}
+
 }
diff --git a/saml2/saml2-service-provider/src/test/resources/test-entitiesdescriptor.xml b/saml2/saml2-service-provider/src/test/resources/test-entitiesdescriptor.xml
new file mode 100644
index 0000000000..605c197307
--- /dev/null
+++ b/saml2/saml2-service-provider/src/test/resources/test-entitiesdescriptor.xml
@@ -0,0 +1,168 @@
+<md:EntitiesDescriptor	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:EntityDescriptor entityID="https://idp.example.com/idp/shibboleth">
+
+		<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+			<md:Extensions>
+				<shibmd:Scope regexp="false">example.com</shibmd:Scope>
+
+				<mdui:UIInfo>
+					<mdui:DisplayName xml:lang="en">
+						Consortium GARR IdP
+					</mdui:DisplayName>
+					<mdui:DisplayName xml:lang="it">
+						Consortium GARR IdP
+					</mdui:DisplayName>
+
+					<mdui:Description xml:lang="en">
+						This Identity Provider gives support for the Consortium GARR's user community
+					</mdui:Description>
+					<mdui:Description xml:lang="it">
+						Questo Identity Provider di test fornisce supporto alla comunita' utenti GARR
+					</mdui:Description>
+				</mdui:UIInfo>
+			</md:Extensions>
+
+			<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/idp/profile/SAML2/POST/SSO"/>
+		</md:IDPSSODescriptor>
+
+		<md:Organization>
+			<md:OrganizationName xml:lang="en">
+				Consortium GARR
+			</md:OrganizationName>
+			<md:OrganizationName xml:lang="it">
+				Consortium GARR
+			</md:OrganizationName>
+
+			<md:OrganizationDisplayName xml:lang="en">
+				Consortium GARR
+			</md:OrganizationDisplayName>
+			<md:OrganizationDisplayName xml:lang="it">
+				Consortium GARR
+			</md:OrganizationDisplayName>
+
+			<md:OrganizationURL xml:lang="it">
+				https://example.org
+			</md:OrganizationURL>
+		</md:Organization>
+
+		<md:ContactPerson contactType="technical">
+			<md:EmailAddress>mailto:technical.contact@example.com</md:EmailAddress>
+		</md:ContactPerson>
+
+	</md:EntityDescriptor>
+	<md:EntityDescriptor entityID="https://ap.example.org/idp/shibboleth">
+
+		<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+			<md:Extensions>
+				<shibmd:Scope regexp="false">example.org</shibmd:Scope>
+
+				<mdui:UIInfo>
+					<mdui:DisplayName xml:lang="en">
+						Consortium GARR IdP
+					</mdui:DisplayName>
+					<mdui:DisplayName xml:lang="it">
+						Consortium GARR IdP
+					</mdui:DisplayName>
+
+					<mdui:Description xml:lang="en">
+						This Identity Provider gives support for the Consortium GARR's user community
+					</mdui:Description>
+					<mdui:Description xml:lang="it">
+						Questo Identity Provider di test fornisce supporto alla comunita' utenti GARR
+					</mdui:Description>
+				</mdui:UIInfo>
+			</md:Extensions>
+
+			<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://ap.example.org/idp/profile/SAML2/POST/SSO"/>
+		</md:IDPSSODescriptor>
+
+		<md:Organization>
+			<md:OrganizationName xml:lang="en">
+				Consortium GARR
+			</md:OrganizationName>
+			<md:OrganizationName xml:lang="it">
+				Consortium GARR
+			</md:OrganizationName>
+
+			<md:OrganizationDisplayName xml:lang="en">
+				Consortium GARR
+			</md:OrganizationDisplayName>
+			<md:OrganizationDisplayName xml:lang="it">
+				Consortium GARR
+			</md:OrganizationDisplayName>
+
+			<md:OrganizationURL xml:lang="it">
+				https://example.org
+			</md:OrganizationURL>
+		</md:Organization>
+
+		<md:ContactPerson contactType="technical">
+			<md:EmailAddress>mailto:technical.contact@example.org</md:EmailAddress>
+		</md:ContactPerson>
+
+	</md:EntityDescriptor>
+</md:EntitiesDescriptor>