diff --git a/etc/nohttp/allowlist.lines b/etc/nohttp/allowlist.lines
index 1b12424b16..a9898c86da 100644
--- a/etc/nohttp/allowlist.lines
+++ b/etc/nohttp/allowlist.lines
@@ -4,3 +4,4 @@
^http://jaspan.com.*
^http://lists.webappsec.org/.*
^http://webblaze.cs.berkeley.edu/.*
+^http://www.w3.org/2000/09/xmldsig.*
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
new file mode 100644
index 0000000000..97ee941fe4
--- /dev/null
+++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java
@@ -0,0 +1,201 @@
+/*
+ * 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.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.config.XMLObjectProviderRegistry;
+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.saml.saml2.metadata.impl.EntityDescriptorUnmarshaller;
+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;
+
+import static java.lang.Boolean.TRUE;
+import static org.opensaml.saml.common.xml.SAMLConstants.SAML20P_NS;
+import static org.springframework.security.saml2.core.Saml2X509Credential.encryption;
+import static org.springframework.security.saml2.core.Saml2X509Credential.verification;
+import static org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.withRegistrationId;
+
+/**
+ * An {@link HttpMessageConverter} that takes an {@code IDPSSODescriptor} in an HTTP response
+ * and converts it into a {@link RelyingPartyRegistration.Builder}.
+ *
+ * The primary use case for this is constructing a {@link RelyingPartyRegistration} for inclusion in a
+ * {@link RelyingPartyRegistrationRepository}. To do so, you can include an instance of this converter in a
+ * {@link org.springframework.web.client.RestOperations} like so:
+ *
+ *
+ * RestOperations rest = new RestTemplate(Collections.singletonList(
+ * new RelyingPartyRegistrationsBuilderHttpMessageConverter()));
+ * RelyingPartyRegistration.Builder builder = rest.getForObject
+ * ("https://idp.example.org/metadata", RelyingPartyRegistration.Builder.class);
+ * RelyingPartyRegistration registration = builder.registrationId("registration-id").build();
+ *
+ *
+ * Note that this will only configure the asserting party (IDP) half of the {@link RelyingPartyRegistration},
+ * meaning where and how to send AuthnRequests, how to verify Assertions, etc.
+ *
+ * To further configure the {@link RelyingPartyRegistration} with relying party (SP) information, you may
+ * invoke the appropriate methods on the builder.
+ *
+ * @author Josh Cummings
+ * @since 5.4
+ */
+public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter
+ implements HttpMessageConverter {
+
+ static {
+ OpenSamlInitializationService.initialize();
+ }
+
+ private final EntityDescriptorUnmarshaller unmarshaller;
+ private final ParserPool parserPool;
+
+ /**
+ * Creates a {@link OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter}
+ */
+ public OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter() {
+ XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
+ this.unmarshaller = (EntityDescriptorUnmarshaller) registry.getUnmarshallerFactory()
+ .getUnmarshaller(EntityDescriptor.DEFAULT_ELEMENT_NAME);
+ this.parserPool = registry.getParserPool();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean canRead(Class> clazz, MediaType mediaType) {
+ return RelyingPartyRegistration.Builder.class.isAssignableFrom(clazz);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean canWrite(Class> clazz, MediaType mediaType) {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List getSupportedMediaTypes() {
+ return Arrays.asList(MediaType.APPLICATION_XML, MediaType.TEXT_XML);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public RelyingPartyRegistration.Builder read(Class extends RelyingPartyRegistration.Builder> clazz, HttpInputMessage inputMessage)
+ throws IOException, HttpMessageNotReadableException {
+
+ EntityDescriptor descriptor = entityDescriptor(inputMessage.getBody());
+ IDPSSODescriptor idpssoDescriptor = descriptor.getIDPSSODescriptor(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(verification(certificate));
+ }
+ }
+ if (keyDescriptor.getUse().equals(UsageType.ENCRYPTION)) {
+ List certificates = certificates(keyDescriptor);
+ for (X509Certificate certificate : certificates) {
+ encryption.add(encryption(certificate));
+ }
+ }
+ }
+ if (verification.isEmpty()) {
+ throw new Saml2Exception("Metadata response is missing verification certificates, necessary for verifying SAML assertions");
+ }
+ RelyingPartyRegistration.Builder builder = withRegistrationId(descriptor.getEntityID())
+ .assertingPartyDetails(party -> party
+ .entityId(descriptor.getEntityID())
+ .wantAuthnRequestsSigned(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 e) {
+ throw new Saml2Exception(e);
+ }
+ }
+
+ private EntityDescriptor entityDescriptor(InputStream inputStream) {
+ try {
+ Document document = this.parserPool.parse(inputStream);
+ Element element = document.getDocumentElement();
+ return (EntityDescriptor) this.unmarshaller.unmarshall(element);
+ } catch (Exception e) {
+ throw new Saml2Exception(e);
+ }
+ }
+
+ @Override
+ public void write(RelyingPartyRegistration.Builder builder, MediaType contentType, HttpOutputMessage outputMessage) throws HttpMessageNotWritableException {
+ throw new HttpMessageNotWritableException("This converter cannot write a RelyingPartyRegistration.Builder");
+ }
+}
diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverterTests.java
new file mode 100644
index 0000000000..270183c17b
--- /dev/null
+++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverterTests.java
@@ -0,0 +1,135 @@
+/*
+ * 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.mock.http.client.MockClientHttpResponse;
+import org.springframework.security.saml2.Saml2Exception;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.springframework.http.HttpStatus.OK;
+
+public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverterTests {
+ 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 ENTITY_DESCRIPTOR_TEMPLATE =
+ "\n" +
+ "\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 OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter converter;
+
+ @Before
+ public void setup() {
+ this.converter = new OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter();
+ }
+
+ @Test
+ public void readWhenMissingIDPSSODescriptorThenException() {
+ MockClientHttpResponse response = new MockClientHttpResponse
+ ((String.format(ENTITY_DESCRIPTOR_TEMPLATE, "")).getBytes(), OK);
+ assertThatCode(() -> this.converter.read(RelyingPartyRegistration.Builder.class, response))
+ .isInstanceOf(Saml2Exception.class)
+ .hasMessageContaining("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, ""));
+ MockClientHttpResponse response = new MockClientHttpResponse(payload.getBytes(), OK);
+ assertThatCode(() -> this.converter.read(RelyingPartyRegistration.Builder.class, response))
+ .isInstanceOf(Saml2Exception.class)
+ .hasMessageContaining("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, "signing")
+ ));
+ MockClientHttpResponse response = new MockClientHttpResponse(payload.getBytes(), OK);
+ assertThatCode(() -> this.converter.read(RelyingPartyRegistration.Builder.class, response))
+ .isInstanceOf(Saml2Exception.class)
+ .hasMessageContaining("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, "signing") +
+ String.format(KEY_DESCRIPTOR_TEMPLATE, "encryption") +
+ String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE)
+ ));
+ MockClientHttpResponse response = new MockClientHttpResponse(payload.getBytes(), OK);
+ RelyingPartyRegistration registration =
+ this.converter.read(RelyingPartyRegistration.Builder.class, response)
+ .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));
+ }
+
+ X509Certificate x509Certificate(String data) {
+ try {
+ InputStream certificate = new ByteArrayInputStream(Base64.getDecoder().decode(data.getBytes()));
+ return (X509Certificate) CertificateFactory.getInstance("X.509")
+ .generateCertificate(certificate);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+}