소스 검색

Add OpenSamlRelyingPartyRegistration

Issue gh-12841
Josh Cummings 2 년 전
부모
커밋
42cece21b4

+ 87 - 1
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyDetails.java

@@ -16,8 +16,14 @@
 
 package org.springframework.security.saml2.provider.service.registration;
 
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Consumer;
+
 import org.opensaml.saml.saml2.metadata.EntityDescriptor;
 
+import org.springframework.security.saml2.core.Saml2X509Credential;
+
 /**
  * A {@link RelyingPartyRegistration.AssertingPartyDetails} that contains
  * OpenSAML-specific members
@@ -66,12 +72,92 @@ public final class OpenSamlAssertingPartyDetails extends RelyingPartyRegistratio
 	 */
 	public static final class Builder extends RelyingPartyRegistration.AssertingPartyDetails.Builder {
 
-		private final EntityDescriptor descriptor;
+		private EntityDescriptor descriptor;
 
 		private Builder(EntityDescriptor descriptor) {
 			this.descriptor = descriptor;
 		}
 
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Builder entityId(String entityId) {
+			return (Builder) super.entityId(entityId);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Builder wantAuthnRequestsSigned(boolean wantAuthnRequestsSigned) {
+			return (Builder) super.wantAuthnRequestsSigned(wantAuthnRequestsSigned);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Builder signingAlgorithms(Consumer<List<String>> signingMethodAlgorithmsConsumer) {
+			return (Builder) super.signingAlgorithms(signingMethodAlgorithmsConsumer);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Builder verificationX509Credentials(Consumer<Collection<Saml2X509Credential>> credentialsConsumer) {
+			return (Builder) super.verificationX509Credentials(credentialsConsumer);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Builder encryptionX509Credentials(Consumer<Collection<Saml2X509Credential>> credentialsConsumer) {
+			return (Builder) super.encryptionX509Credentials(credentialsConsumer);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Builder singleSignOnServiceLocation(String singleSignOnServiceLocation) {
+			return (Builder) super.singleSignOnServiceLocation(singleSignOnServiceLocation);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Builder singleSignOnServiceBinding(Saml2MessageBinding singleSignOnServiceBinding) {
+			return (Builder) super.singleSignOnServiceBinding(singleSignOnServiceBinding);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Builder singleLogoutServiceLocation(String singleLogoutServiceLocation) {
+			return (Builder) super.singleLogoutServiceLocation(singleLogoutServiceLocation);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Builder singleLogoutServiceResponseLocation(String singleLogoutServiceResponseLocation) {
+			return (Builder) super.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Builder singleLogoutServiceBinding(Saml2MessageBinding singleLogoutServiceBinding) {
+			return (Builder) super.singleLogoutServiceBinding(singleLogoutServiceBinding);
+		}
+
 		/**
 		 * Build an
 		 * {@link org.springframework.security.saml2.provider.service.registration.OpenSamlAssertingPartyDetails}

+ 0 - 220
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverter.java

@@ -1,220 +0,0 @@
-/*
- * Copyright 2002-2023 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.Arrays;
-import java.util.Collection;
-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.ext.saml2alg.SigningMethod;
-import org.opensaml.saml.saml2.metadata.EntitiesDescriptor;
-import org.opensaml.saml.saml2.metadata.EntityDescriptor;
-import org.opensaml.saml.saml2.metadata.Extensions;
-import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
-import org.opensaml.saml.saml2.metadata.KeyDescriptor;
-import org.opensaml.saml.saml2.metadata.SingleLogoutService;
-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 OpenSamlMetadataAssertingPartyDetailsConverter {
-
-	static {
-		OpenSamlInitializationService.initialize();
-	}
-
-	private final XMLObjectProviderRegistry registry;
-
-	private final ParserPool parserPool;
-
-	/**
-	 * Creates a {@link OpenSamlMetadataAssertingPartyDetailsConverter}
-	 */
-	OpenSamlMetadataAssertingPartyDetailsConverter() {
-		this.registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
-		this.parserPool = this.registry.getParserPool();
-	}
-
-	Collection<RelyingPartyRegistration.AssertingPartyDetails.Builder> convert(InputStream inputStream) {
-		List<RelyingPartyRegistration.AssertingPartyDetails.Builder> builders = new ArrayList<>();
-		XMLObject xmlObject = xmlObject(inputStream);
-		if (xmlObject instanceof EntitiesDescriptor) {
-			EntitiesDescriptor descriptors = (EntitiesDescriptor) xmlObject;
-			for (EntityDescriptor descriptor : descriptors.getEntityDescriptors()) {
-				if (descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) != null) {
-					builders.add(convert(descriptor));
-				}
-			}
-			if (builders.isEmpty()) {
-				throw new Saml2Exception("Metadata contains no IDPSSODescriptor elements");
-			}
-			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.AssertingPartyDetails.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");
-		}
-		List<Saml2X509Credential> verification = new ArrayList<>();
-		List<Saml2X509Credential> encryption = new ArrayList<>();
-		for (KeyDescriptor keyDescriptor : idpssoDescriptor.getKeyDescriptors()) {
-			if (keyDescriptor.getUse().equals(UsageType.SIGNING)) {
-				List<X509Certificate> certificates = certificates(keyDescriptor);
-				for (X509Certificate certificate : certificates) {
-					verification.add(Saml2X509Credential.verification(certificate));
-				}
-			}
-			if (keyDescriptor.getUse().equals(UsageType.ENCRYPTION)) {
-				List<X509Certificate> certificates = certificates(keyDescriptor);
-				for (X509Certificate certificate : certificates) {
-					encryption.add(Saml2X509Credential.encryption(certificate));
-				}
-			}
-			if (keyDescriptor.getUse().equals(UsageType.UNSPECIFIED)) {
-				List<X509Certificate> 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.AssertingPartyDetails.Builder party = OpenSamlAssertingPartyDetails
-				.withEntityDescriptor(descriptor).entityId(descriptor.getEntityID())
-				.wantAuthnRequestsSigned(Boolean.TRUE.equals(idpssoDescriptor.getWantAuthnRequestsSigned()))
-				.verificationX509Credentials((c) -> c.addAll(verification))
-				.encryptionX509Credentials((c) -> c.addAll(encryption));
-		List<SigningMethod> signingMethods = signingMethods(idpssoDescriptor);
-		for (SigningMethod method : signingMethods) {
-			party.signingAlgorithms((algorithms) -> algorithms.add(method.getAlgorithm()));
-		}
-		if (idpssoDescriptor.getSingleSignOnServices().isEmpty()) {
-			throw new Saml2Exception(
-					"Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests");
-		}
-		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;
-			}
-			party.singleSignOnServiceLocation(singleSignOnService.getLocation()).singleSignOnServiceBinding(binding);
-			break;
-		}
-		for (SingleLogoutService singleLogoutService : idpssoDescriptor.getSingleLogoutServices()) {
-			Saml2MessageBinding binding;
-			if (singleLogoutService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) {
-				binding = Saml2MessageBinding.POST;
-			}
-			else if (singleLogoutService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) {
-				binding = Saml2MessageBinding.REDIRECT;
-			}
-			else {
-				continue;
-			}
-			String responseLocation = (singleLogoutService.getResponseLocation() == null)
-					? singleLogoutService.getLocation() : singleLogoutService.getResponseLocation();
-			party.singleLogoutServiceLocation(singleLogoutService.getLocation())
-					.singleLogoutServiceResponseLocation(responseLocation).singleLogoutServiceBinding(binding);
-			break;
-		}
-		return party;
-	}
-
-	private List<X509Certificate> certificates(KeyDescriptor keyDescriptor) {
-		try {
-			return KeyInfoSupport.getCertificates(keyDescriptor.getKeyInfo());
-		}
-		catch (CertificateException ex) {
-			throw new Saml2Exception(ex);
-		}
-	}
-
-	private List<SigningMethod> signingMethods(IDPSSODescriptor idpssoDescriptor) {
-		Extensions extensions = idpssoDescriptor.getExtensions();
-		List<SigningMethod> result = signingMethods(extensions);
-		if (!result.isEmpty()) {
-			return result;
-		}
-		EntityDescriptor descriptor = (EntityDescriptor) idpssoDescriptor.getParent();
-		extensions = descriptor.getExtensions();
-		return signingMethods(extensions);
-	}
-
-	private XMLObject xmlObject(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 {
-			return unmarshaller.unmarshall(element);
-		}
-		catch (Exception ex) {
-			throw new Saml2Exception(ex);
-		}
-	}
-
-	private Document document(InputStream inputStream) {
-		try {
-			return this.parserPool.parse(inputStream);
-		}
-		catch (Exception ex) {
-			throw new Saml2Exception(ex);
-		}
-	}
-
-	private <T> List<T> signingMethods(Extensions extensions) {
-		if (extensions != null) {
-			return (List<T>) extensions.getUnknownXMLObjects(SigningMethod.DEFAULT_ELEMENT_NAME);
-		}
-		return new ArrayList<>();
-	}
-
-}

+ 198 - 6
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverter.java

@@ -17,19 +17,211 @@
 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.Arrays;
 import java.util.Collection;
+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.ext.saml2alg.SigningMethod;
+import org.opensaml.saml.saml2.metadata.EntitiesDescriptor;
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
+import org.opensaml.saml.saml2.metadata.Extensions;
+import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
+import org.opensaml.saml.saml2.metadata.KeyDescriptor;
+import org.opensaml.saml.saml2.metadata.SingleLogoutService;
+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 OpenSamlMetadataRelyingPartyRegistrationConverter {
 
-	private final OpenSamlMetadataAssertingPartyDetailsConverter converter = new OpenSamlMetadataAssertingPartyDetailsConverter();
+	static {
+		OpenSamlInitializationService.initialize();
+	}
+
+	private final XMLObjectProviderRegistry registry;
+
+	private final ParserPool parserPool;
+
+	/**
+	 * Creates a {@link OpenSamlMetadataRelyingPartyRegistrationConverter}
+	 */
+	OpenSamlMetadataRelyingPartyRegistrationConverter() {
+		this.registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
+		this.parserPool = this.registry.getParserPool();
+	}
+
+	OpenSamlRelyingPartyRegistration.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");
+		}
+		List<Saml2X509Credential> verification = new ArrayList<>();
+		List<Saml2X509Credential> encryption = new ArrayList<>();
+		for (KeyDescriptor keyDescriptor : idpssoDescriptor.getKeyDescriptors()) {
+			if (keyDescriptor.getUse().equals(UsageType.SIGNING)) {
+				List<X509Certificate> certificates = certificates(keyDescriptor);
+				for (X509Certificate certificate : certificates) {
+					verification.add(Saml2X509Credential.verification(certificate));
+				}
+			}
+			if (keyDescriptor.getUse().equals(UsageType.ENCRYPTION)) {
+				List<X509Certificate> certificates = certificates(keyDescriptor);
+				for (X509Certificate certificate : certificates) {
+					encryption.add(Saml2X509Credential.encryption(certificate));
+				}
+			}
+			if (keyDescriptor.getUse().equals(UsageType.UNSPECIFIED)) {
+				List<X509Certificate> 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");
+		}
+		OpenSamlRelyingPartyRegistration.Builder builder = OpenSamlRelyingPartyRegistration
+				.withAssertingPartyEntityDescriptor(descriptor)
+				.assertingPartyDetails((party) -> party.entityId(descriptor.getEntityID())
+						.wantAuthnRequestsSigned(Boolean.TRUE.equals(idpssoDescriptor.getWantAuthnRequestsSigned()))
+						.verificationX509Credentials((c) -> c.addAll(verification))
+						.encryptionX509Credentials((c) -> c.addAll(encryption)));
+
+		List<SigningMethod> signingMethods = signingMethods(idpssoDescriptor);
+		for (SigningMethod method : signingMethods) {
+			builder.assertingPartyDetails(
+					(party) -> party.signingAlgorithms((algorithms) -> algorithms.add(method.getAlgorithm())));
+		}
+		if (idpssoDescriptor.getSingleSignOnServices().isEmpty()) {
+			throw new Saml2Exception(
+					"Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests");
+		}
+		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));
+			break;
+		}
+		for (SingleLogoutService singleLogoutService : idpssoDescriptor.getSingleLogoutServices()) {
+			Saml2MessageBinding binding;
+			if (singleLogoutService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) {
+				binding = Saml2MessageBinding.POST;
+			}
+			else if (singleLogoutService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) {
+				binding = Saml2MessageBinding.REDIRECT;
+			}
+			else {
+				continue;
+			}
+			String responseLocation = (singleLogoutService.getResponseLocation() == null)
+					? singleLogoutService.getLocation() : singleLogoutService.getResponseLocation();
+			builder.assertingPartyDetails(
+					(party) -> party.singleLogoutServiceLocation(singleLogoutService.getLocation())
+							.singleLogoutServiceResponseLocation(responseLocation).singleLogoutServiceBinding(binding));
+			break;
+		}
+
+		return builder;
+	}
+
+	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()) {
+				if (descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) != null) {
+					builders.add(convert(descriptor));
+				}
+			}
+			if (builders.isEmpty()) {
+				throw new Saml2Exception("Metadata contains no IDPSSODescriptor elements");
+			}
+			return builders;
+		}
+		if (xmlObject instanceof EntityDescriptor) {
+			EntityDescriptor descriptor = (EntityDescriptor) xmlObject;
+			return Arrays.asList(convert(descriptor));
+		}
+		throw new Saml2Exception("Unsupported element of type " + xmlObject.getClass());
+	}
+
+	private List<X509Certificate> certificates(KeyDescriptor keyDescriptor) {
+		try {
+			return KeyInfoSupport.getCertificates(keyDescriptor.getKeyInfo());
+		}
+		catch (CertificateException ex) {
+			throw new Saml2Exception(ex);
+		}
+	}
+
+	private List<SigningMethod> signingMethods(IDPSSODescriptor idpssoDescriptor) {
+		Extensions extensions = idpssoDescriptor.getExtensions();
+		List<SigningMethod> result = signingMethods(extensions);
+		if (!result.isEmpty()) {
+			return result;
+		}
+		EntityDescriptor descriptor = (EntityDescriptor) idpssoDescriptor.getParent();
+		extensions = descriptor.getExtensions();
+		return signingMethods(extensions);
+	}
+
+	private XMLObject xmlObject(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 {
+			return unmarshaller.unmarshall(element);
+		}
+		catch (Exception ex) {
+			throw new Saml2Exception(ex);
+		}
+	}
+
+	private Document document(InputStream inputStream) {
+		try {
+			return this.parserPool.parse(inputStream);
+		}
+		catch (Exception ex) {
+			throw new Saml2Exception(ex);
+		}
+	}
 
-	Collection<RelyingPartyRegistration.Builder> convert(InputStream source) {
-		Collection<RelyingPartyRegistration.Builder> builders = new ArrayList<>();
-		for (RelyingPartyRegistration.AssertingPartyDetails.Builder builder : this.converter.convert(source)) {
-			builders.add(new RelyingPartyRegistration.Builder(builder));
+	private <T> List<T> signingMethods(Extensions extensions) {
+		if (extensions != null) {
+			return (List<T>) extensions.getUnknownXMLObjects(SigningMethod.DEFAULT_ELEMENT_NAME);
 		}
-		return builders;
+		return new ArrayList<>();
 	}
 
 }

+ 172 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistration.java

@@ -0,0 +1,172 @@
+/*
+ * 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.
+ * 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.util.Collection;
+import java.util.function.Consumer;
+
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
+
+import org.springframework.security.saml2.core.Saml2X509Credential;
+
+/**
+ * An OpenSAML implementation of {@link RelyingPartyRegistration} that contains OpenSAML
+ * objects like {@link EntityDescriptor}.
+ *
+ * @author Josh Cummings
+ * @since 6.1
+ */
+public final class OpenSamlRelyingPartyRegistration extends RelyingPartyRegistration {
+
+	OpenSamlRelyingPartyRegistration(RelyingPartyRegistration registration) {
+		super(registration.getRegistrationId(), registration.getEntityId(),
+				registration.getAssertionConsumerServiceLocation(), registration.getAssertionConsumerServiceBinding(),
+				registration.getSingleLogoutServiceLocation(), registration.getSingleLogoutServiceResponseLocation(),
+				registration.getSingleLogoutServiceBindings(), registration.getAssertingPartyDetails(),
+				registration.getNameIdFormat(), registration.getDecryptionX509Credentials(),
+				registration.getSigningX509Credentials());
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public OpenSamlRelyingPartyRegistration.Builder mutate() {
+		OpenSamlAssertingPartyDetails party = getAssertingPartyDetails();
+		return withAssertingPartyEntityDescriptor(party.getEntityDescriptor()).registrationId(getRegistrationId())
+				.entityId(getEntityId()).signingX509Credentials((c) -> c.addAll(getSigningX509Credentials()))
+				.decryptionX509Credentials((c) -> c.addAll(getDecryptionX509Credentials()))
+				.assertionConsumerServiceLocation(getAssertionConsumerServiceLocation())
+				.assertionConsumerServiceBinding(getAssertionConsumerServiceBinding())
+				.singleLogoutServiceLocation(getSingleLogoutServiceLocation())
+				.singleLogoutServiceResponseLocation(getSingleLogoutServiceResponseLocation())
+				.singleLogoutServiceBindings((c) -> c.addAll(getSingleLogoutServiceBindings()))
+				.nameIdFormat(getNameIdFormat())
+				.assertingPartyDetails((assertingParty) -> ((OpenSamlAssertingPartyDetails.Builder) assertingParty)
+						.entityId(party.getEntityId()).wantAuthnRequestsSigned(party.getWantAuthnRequestsSigned())
+						.signingAlgorithms((algorithms) -> algorithms.addAll(party.getSigningAlgorithms()))
+						.verificationX509Credentials((c) -> c.addAll(party.getVerificationX509Credentials()))
+						.encryptionX509Credentials((c) -> c.addAll(party.getEncryptionX509Credentials()))
+						.singleSignOnServiceLocation(party.getSingleSignOnServiceLocation())
+						.singleSignOnServiceBinding(party.getSingleSignOnServiceBinding())
+						.singleLogoutServiceLocation(party.getSingleLogoutServiceLocation())
+						.singleLogoutServiceResponseLocation(party.getSingleLogoutServiceResponseLocation())
+						.singleLogoutServiceBinding(party.getSingleLogoutServiceBinding()));
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public OpenSamlAssertingPartyDetails getAssertingPartyDetails() {
+		return (OpenSamlAssertingPartyDetails) super.getAssertingPartyDetails();
+	}
+
+	/**
+	 * Create a {@link Builder} from an entity descriptor
+	 * @param entityDescriptor the asserting party's {@link EntityDescriptor}
+	 * @return an {@link Builder}
+	 */
+	public static OpenSamlRelyingPartyRegistration.Builder withAssertingPartyEntityDescriptor(
+			EntityDescriptor entityDescriptor) {
+		return new Builder(entityDescriptor);
+	}
+
+	/**
+	 * An OpenSAML version of
+	 * {@link org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.AssertingPartyDetails.Builder}
+	 * that contains the underlying {@link EntityDescriptor}
+	 */
+	public static final class Builder extends RelyingPartyRegistration.Builder {
+
+		private Builder(EntityDescriptor entityDescriptor) {
+			super(entityDescriptor.getEntityID(), OpenSamlAssertingPartyDetails.withEntityDescriptor(entityDescriptor));
+		}
+
+		@Override
+		public Builder registrationId(String id) {
+			return (Builder) super.registrationId(id);
+		}
+
+		public Builder entityId(String entityId) {
+			return (Builder) super.entityId(entityId);
+		}
+
+		public Builder signingX509Credentials(Consumer<Collection<Saml2X509Credential>> credentialsConsumer) {
+			return (Builder) super.signingX509Credentials(credentialsConsumer);
+		}
+
+		@Override
+		public Builder decryptionX509Credentials(Consumer<Collection<Saml2X509Credential>> credentialsConsumer) {
+			return (Builder) super.decryptionX509Credentials(credentialsConsumer);
+		}
+
+		@Override
+		public Builder assertionConsumerServiceLocation(String assertionConsumerServiceLocation) {
+			return (Builder) super.assertionConsumerServiceLocation(assertionConsumerServiceLocation);
+		}
+
+		@Override
+		public Builder assertionConsumerServiceBinding(Saml2MessageBinding assertionConsumerServiceBinding) {
+			return (Builder) super.assertionConsumerServiceBinding(assertionConsumerServiceBinding);
+		}
+
+		@Override
+		public Builder singleLogoutServiceBinding(Saml2MessageBinding singleLogoutServiceBinding) {
+			return singleLogoutServiceBindings((saml2MessageBindings) -> {
+				saml2MessageBindings.clear();
+				saml2MessageBindings.add(singleLogoutServiceBinding);
+			});
+		}
+
+		@Override
+		public Builder singleLogoutServiceBindings(Consumer<Collection<Saml2MessageBinding>> bindingsConsumer) {
+			return (Builder) super.singleLogoutServiceBindings(bindingsConsumer);
+		}
+
+		@Override
+		public Builder singleLogoutServiceLocation(String singleLogoutServiceLocation) {
+			return (Builder) super.singleLogoutServiceLocation(singleLogoutServiceLocation);
+		}
+
+		public Builder singleLogoutServiceResponseLocation(String singleLogoutServiceResponseLocation) {
+			return (Builder) super.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation);
+		}
+
+		@Override
+		public Builder nameIdFormat(String nameIdFormat) {
+			return (Builder) super.nameIdFormat(nameIdFormat);
+		}
+
+		@Override
+		public Builder assertingPartyDetails(Consumer<AssertingPartyDetails.Builder> assertingPartyDetails) {
+			return (Builder) super.assertingPartyDetails(assertingPartyDetails);
+		}
+
+		/**
+		 * Build an {@link OpenSamlRelyingPartyRegistration}
+		 * {@link org.springframework.security.saml2.provider.service.registration.OpenSamlRelyingPartyRegistration}
+		 * @return an {@link OpenSamlRelyingPartyRegistration}
+		 */
+		@Override
+		public OpenSamlRelyingPartyRegistration build() {
+			return new OpenSamlRelyingPartyRegistration(super.build());
+		}
+
+	}
+
+}

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

@@ -62,13 +62,13 @@ public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter
 		OpenSamlInitializationService.initialize();
 	}
 
-	private final OpenSamlMetadataAssertingPartyDetailsConverter converter;
+	private final OpenSamlMetadataRelyingPartyRegistrationConverter converter;
 
 	/**
 	 * Creates a {@link OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter}
 	 */
 	public OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter() {
-		this.converter = new OpenSamlMetadataAssertingPartyDetailsConverter();
+		this.converter = new OpenSamlMetadataRelyingPartyRegistrationConverter();
 	}
 
 	@Override
@@ -89,7 +89,7 @@ public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter
 	@Override
 	public RelyingPartyRegistration.Builder read(Class<? extends RelyingPartyRegistration.Builder> clazz,
 			HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
-		return new RelyingPartyRegistration.Builder(this.converter.convert(inputMessage.getBody()).iterator().next());
+		return this.converter.convert(inputMessage.getBody()).iterator().next();
 	}
 
 	@Override

+ 2 - 3
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java

@@ -26,7 +26,6 @@ import java.util.function.Consumer;
 
 import org.opensaml.xmlsec.signature.support.SignatureConstants;
 
-import org.springframework.core.convert.converter.Converter;
 import org.springframework.security.saml2.core.Saml2X509Credential;
 import org.springframework.util.Assert;
 import org.springframework.util.CollectionUtils;
@@ -69,7 +68,7 @@ import org.springframework.util.CollectionUtils;
  * @author Josh Cummings
  * @since 5.2
  */
-public final class RelyingPartyRegistration {
+public class RelyingPartyRegistration {
 
 	private final String registrationId;
 
@@ -93,7 +92,7 @@ public final class RelyingPartyRegistration {
 
 	private final Collection<Saml2X509Credential> signingX509Credentials;
 
-	private RelyingPartyRegistration(String registrationId, String entityId, String assertionConsumerServiceLocation,
+	protected RelyingPartyRegistration(String registrationId, String entityId, String assertionConsumerServiceLocation,
 			Saml2MessageBinding assertionConsumerServiceBinding, String singleLogoutServiceLocation,
 			String singleLogoutServiceResponseLocation, Collection<Saml2MessageBinding> singleLogoutServiceBindings,
 			AssertingPartyDetails assertingPartyDetails, String nameIdFormat,

+ 0 - 179
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverterTests.java

@@ -1,179 +0,0 @@
-/*
- * 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.
- * 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.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.opensaml.saml.saml2.metadata.EntityDescriptor;
-import org.opensaml.xmlsec.signature.support.SignatureConstants;
-
-import org.springframework.security.saml2.Saml2Exception;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-
-public class OpenSamlMetadataAssertingPartyDetailsConverterTests {
-
-	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 = "<md:EntitiesDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\">\n%s</md:EntitiesDescriptor>";
-
-	private static final String ENTITY_DESCRIPTOR_TEMPLATE = "<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\" "
-			+ "xmlns:alg=\"urn:oasis:names:tc:SAML:metadata:algsupport\" " + "entityID=\"entity-id\" "
-			+ "ID=\"_bf133aac099b99b3d81286e1a341f2d34188043a77fe15bf4bf1487dae9b2ea3\">\n%s"
-			+ "</md:EntityDescriptor>";
-
-	private static final String IDP_SSO_DESCRIPTOR_TEMPLATE = "<md:IDPSSODescriptor protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n"
-			+ "%s\n" + "</md:IDPSSODescriptor>";
-
-	private static final String KEY_DESCRIPTOR_TEMPLATE = "<md:KeyDescriptor %s>\n"
-			+ "<ds:KeyInfo xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\">\n" + "<ds:X509Data>\n"
-			+ "<ds:X509Certificate>" + CERTIFICATE + "</ds:X509Certificate>\n" + "</ds:X509Data>\n" + "</ds:KeyInfo>\n"
-			+ "</md:KeyDescriptor>";
-
-	private static final String EXTENSIONS_TEMPLATE = "<md:Extensions>" + "<alg:SigningMethod Algorithm=\""
-			+ SignatureConstants.ALGO_ID_DIGEST_SHA512 + "\"/>" + "</md:Extensions>";
-
-	private static final String SINGLE_SIGN_ON_SERVICE_TEMPLATE = "<md:SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" "
-			+ "Location=\"sso-location\"/>";
-
-	private OpenSamlMetadataAssertingPartyDetailsConverter converter;
-
-	@BeforeEach
-	public void setup() {
-		this.converter = new OpenSamlMetadataAssertingPartyDetailsConverter();
-	}
-
-	@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\"") + EXTENSIONS_TEMPLATE
-								+ String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE)));
-		InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
-		RelyingPartyRegistration.AssertingPartyDetails details = this.converter.convert(inputStream).iterator().next()
-				.build();
-		assertThat(details.getWantAuthnRequestsSigned()).isFalse();
-		assertThat(details.getSigningAlgorithms()).containsExactly(SignatureConstants.ALGO_ID_DIGEST_SHA512);
-		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));
-		assertThat(details).isInstanceOf(OpenSamlAssertingPartyDetails.class);
-		OpenSamlAssertingPartyDetails openSamlDetails = (OpenSamlAssertingPartyDetails) details;
-		EntityDescriptor entityDescriptor = openSamlDetails.getEntityDescriptor();
-		assertThat(entityDescriptor).isNotNull();
-		assertThat(entityDescriptor.getEntityID()).isEqualTo(details.getEntityId());
-	}
-
-	// 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.AssertingPartyDetails details = this.converter.convert(inputStream).iterator().next()
-				.build();
-		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.AssertingPartyDetails details = this.converter.convert(inputStream).iterator().next()
-				.build();
-		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 = "<saml2:Assertion xmlns:saml2=\"https://some.endpoint\"/>";
-		InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
-		assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream))
-				.withMessage("Unsupported element of type saml2:Assertion");
-	}
-
-}

+ 142 - 0
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverterTests.java

@@ -21,17 +21,47 @@ import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.nio.charset.StandardCharsets;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
 import java.util.stream.Collectors;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
+import org.opensaml.xmlsec.signature.support.SignatureConstants;
 
 import org.springframework.core.io.ClassPathResource;
+import org.springframework.security.saml2.Saml2Exception;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 
 public class OpenSamlMetadataRelyingPartyRegistrationConverterTests {
 
+	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 = "<md:EntitiesDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\">\n%s</md:EntitiesDescriptor>";
+
+	private static final String ENTITY_DESCRIPTOR_TEMPLATE = "<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\" "
+			+ "xmlns:alg=\"urn:oasis:names:tc:SAML:metadata:algsupport\" " + "entityID=\"entity-id\" "
+			+ "ID=\"_bf133aac099b99b3d81286e1a341f2d34188043a77fe15bf4bf1487dae9b2ea3\">\n%s"
+			+ "</md:EntityDescriptor>";
+
+	private static final String IDP_SSO_DESCRIPTOR_TEMPLATE = "<md:IDPSSODescriptor protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n"
+			+ "%s\n" + "</md:IDPSSODescriptor>";
+
+	private static final String KEY_DESCRIPTOR_TEMPLATE = "<md:KeyDescriptor %s>\n"
+			+ "<ds:KeyInfo xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\">\n" + "<ds:X509Data>\n"
+			+ "<ds:X509Certificate>" + CERTIFICATE + "</ds:X509Certificate>\n" + "</ds:X509Data>\n" + "</ds:KeyInfo>\n"
+			+ "</md:KeyDescriptor>";
+
+	private static final String EXTENSIONS_TEMPLATE = "<md:Extensions>" + "<alg:SigningMethod Algorithm=\""
+			+ SignatureConstants.ALGO_ID_DIGEST_SHA512 + "\"/>" + "</md:Extensions>";
+
+	private static final String SINGLE_SIGN_ON_SERVICE_TEMPLATE = "<md:SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" "
+			+ "Location=\"sso-location\"/>";
+
 	private OpenSamlMetadataRelyingPartyRegistrationConverter converter = new OpenSamlMetadataRelyingPartyRegistrationConverter();
 
 	private String metadata;
@@ -54,4 +84,116 @@ public class OpenSamlMetadataRelyingPartyRegistrationConverterTests {
 		}
 	}
 
+	@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\"") + EXTENSIONS_TEMPLATE
+								+ String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE)));
+		InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
+		RelyingPartyRegistration.AssertingPartyDetails details = this.converter.convert(inputStream).iterator().next()
+				.build().getAssertingPartyDetails();
+		assertThat(details.getWantAuthnRequestsSigned()).isFalse();
+		assertThat(details.getSigningAlgorithms()).containsExactly(SignatureConstants.ALGO_ID_DIGEST_SHA512);
+		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));
+		assertThat(details).isInstanceOf(OpenSamlAssertingPartyDetails.class);
+		OpenSamlAssertingPartyDetails openSamlDetails = (OpenSamlAssertingPartyDetails) details;
+		EntityDescriptor entityDescriptor = openSamlDetails.getEntityDescriptor();
+		assertThat(entityDescriptor).isNotNull();
+		assertThat(entityDescriptor.getEntityID()).isEqualTo(details.getEntityId());
+	}
+
+	// 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.AssertingPartyDetails details = this.converter.convert(inputStream).iterator().next()
+				.build().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.AssertingPartyDetails details = this.converter.convert(inputStream).iterator().next()
+				.build().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 = "<saml2:Assertion xmlns:saml2=\"https://some.endpoint\"/>";
+		InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
+		assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream))
+				.withMessage("Unsupported element of type saml2:Assertion");
+	}
+
 }