From dcf5cc9e06f0821e2e5fe6b51d13378d3c6234bb Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Wed, 10 Jul 2024 16:56:14 -0600 Subject: [PATCH] Extract EntityDescriptor to AssertingPartyDetails Logic Closes gh-15090 --- .../OpenSamlAssertingPartyDetails.java | 119 ++++++++- ...dataRelyingPartyRegistrationConverter.java | 228 ------------------ .../registration/OpenSamlMetadataUtils.java | 79 ++++++ ...gistrationBuilderHttpMessageConverter.java | 13 +- .../RelyingPartyRegistrations.java | 20 +- ...elyingPartyRegistrationConverterTests.java | 208 ---------------- .../RelyingPartyRegistrationsTests.java | 15 ++ 7 files changed, 231 insertions(+), 451 deletions(-) delete mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverter.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataUtils.java delete mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverterTests.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyDetails.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyDetails.java index 0d780d0e0d..1f447f95ff 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyDetails.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyDetails.java @@ -16,12 +16,25 @@ package org.springframework.security.saml2.provider.service.registration; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.function.Consumer; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.ext.saml2alg.SigningMethod; 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.springframework.security.saml2.Saml2Exception; import org.springframework.security.saml2.core.Saml2X509Credential; /** @@ -62,7 +75,111 @@ public final class OpenSamlAssertingPartyDetails extends RelyingPartyRegistratio * for further configurations */ public static OpenSamlAssertingPartyDetails.Builder withEntityDescriptor(EntityDescriptor entity) { - return new OpenSamlAssertingPartyDetails.Builder(entity); + IDPSSODescriptor idpssoDescriptor = entity.getIDPSSODescriptor(SAMLConstants.SAML20P_NS); + if (idpssoDescriptor == null) { + throw new Saml2Exception("Metadata response is missing the necessary IDPSSODescriptor element"); + } + List verification = new ArrayList<>(); + List encryption = new ArrayList<>(); + for (KeyDescriptor keyDescriptor : idpssoDescriptor.getKeyDescriptors()) { + if (keyDescriptor.getUse().equals(UsageType.SIGNING)) { + List certificates = certificates(keyDescriptor); + for (X509Certificate certificate : certificates) { + verification.add(Saml2X509Credential.verification(certificate)); + } + } + if (keyDescriptor.getUse().equals(UsageType.ENCRYPTION)) { + List certificates = certificates(keyDescriptor); + for (X509Certificate certificate : certificates) { + encryption.add(Saml2X509Credential.encryption(certificate)); + } + } + if (keyDescriptor.getUse().equals(UsageType.UNSPECIFIED)) { + List certificates = certificates(keyDescriptor); + for (X509Certificate certificate : certificates) { + verification.add(Saml2X509Credential.verification(certificate)); + encryption.add(Saml2X509Credential.encryption(certificate)); + } + } + } + if (verification.isEmpty()) { + throw new Saml2Exception( + "Metadata response is missing verification certificates, necessary for verifying SAML assertions"); + } + OpenSamlAssertingPartyDetails.Builder builder = new OpenSamlAssertingPartyDetails.Builder(entity) + .entityId(entity.getEntityID()) + .wantAuthnRequestsSigned(Boolean.TRUE.equals(idpssoDescriptor.getWantAuthnRequestsSigned())) + .verificationX509Credentials((c) -> c.addAll(verification)) + .encryptionX509Credentials((c) -> c.addAll(encryption)); + + List signingMethods = signingMethods(idpssoDescriptor); + for (SigningMethod method : signingMethods) { + builder.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.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.singleLogoutServiceLocation(singleLogoutService.getLocation()) + .singleLogoutServiceResponseLocation(responseLocation) + .singleLogoutServiceBinding(binding); + break; + } + return builder; + } + + private static List certificates(KeyDescriptor keyDescriptor) { + try { + return KeyInfoSupport.getCertificates(keyDescriptor.getKeyInfo()); + } + catch (CertificateException ex) { + throw new Saml2Exception(ex); + } + } + + private static List signingMethods(IDPSSODescriptor idpssoDescriptor) { + Extensions extensions = idpssoDescriptor.getExtensions(); + List result = signingMethods(extensions); + if (!result.isEmpty()) { + return result; + } + EntityDescriptor descriptor = (EntityDescriptor) idpssoDescriptor.getParent(); + extensions = descriptor.getExtensions(); + return signingMethods(extensions); + } + + private static List signingMethods(Extensions extensions) { + if (extensions != null) { + return (List) extensions.getUnknownXMLObjects(SigningMethod.DEFAULT_ELEMENT_NAME); + } + return new ArrayList<>(); } @Override diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverter.java deleted file mode 100644 index a3ab016d0e..0000000000 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverter.java +++ /dev/null @@ -1,228 +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 OpenSamlMetadataRelyingPartyRegistrationConverter { - - 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 verification = new ArrayList<>(); - List encryption = new ArrayList<>(); - for (KeyDescriptor keyDescriptor : idpssoDescriptor.getKeyDescriptors()) { - if (keyDescriptor.getUse().equals(UsageType.SIGNING)) { - List certificates = certificates(keyDescriptor); - for (X509Certificate certificate : certificates) { - verification.add(Saml2X509Credential.verification(certificate)); - } - } - if (keyDescriptor.getUse().equals(UsageType.ENCRYPTION)) { - List certificates = certificates(keyDescriptor); - for (X509Certificate certificate : certificates) { - encryption.add(Saml2X509Credential.encryption(certificate)); - } - } - if (keyDescriptor.getUse().equals(UsageType.UNSPECIFIED)) { - List certificates = certificates(keyDescriptor); - for (X509Certificate certificate : certificates) { - verification.add(Saml2X509Credential.verification(certificate)); - encryption.add(Saml2X509Credential.encryption(certificate)); - } - } - } - if (verification.isEmpty()) { - throw new Saml2Exception( - "Metadata response is missing verification certificates, necessary for verifying SAML assertions"); - } - 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 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 convert(InputStream inputStream) { - List 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 certificates(KeyDescriptor keyDescriptor) { - try { - return KeyInfoSupport.getCertificates(keyDescriptor.getKeyInfo()); - } - catch (CertificateException ex) { - throw new Saml2Exception(ex); - } - } - - private List signingMethods(IDPSSODescriptor idpssoDescriptor) { - Extensions extensions = idpssoDescriptor.getExtensions(); - List 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 List signingMethods(Extensions extensions) { - if (extensions != null) { - return (List) extensions.getUnknownXMLObjects(SigningMethod.DEFAULT_ELEMENT_NAME); - } - return new ArrayList<>(); - } - -} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataUtils.java new file mode 100644 index 0000000000..b7efc22c40 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataUtils.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2024 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.util.Collection; +import java.util.Collections; + +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.core.xml.io.Unmarshaller; +import org.opensaml.saml.saml2.metadata.EntitiesDescriptor; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.OpenSamlInitializationService; + +final class OpenSamlMetadataUtils { + + static { + OpenSamlInitializationService.initialize(); + } + + private OpenSamlMetadataUtils() { + + } + + static Collection descriptors(InputStream metadata) { + XMLObject object = xmlObject(metadata); + if (object instanceof EntityDescriptor descriptor) { + return Collections.singleton(descriptor); + } + if (object instanceof EntitiesDescriptor descriptors) { + return descriptors.getEntityDescriptors(); + } + throw new Saml2Exception("Unsupported element type: " + object.getClass().getName()); + } + + static XMLObject xmlObject(InputStream inputStream) { + Document document = document(inputStream); + Element element = document.getDocumentElement(); + Unmarshaller unmarshaller = XMLObjectProviderRegistrySupport.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); + } + } + + static Document document(InputStream inputStream) { + try { + return XMLObjectProviderRegistrySupport.getParserPool().parse(inputStream); + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java index 3d4069ab8f..4437261e7f 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-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -62,15 +62,6 @@ public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter OpenSamlInitializationService.initialize(); } - private final OpenSamlMetadataRelyingPartyRegistrationConverter converter; - - /** - * Creates a {@link OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter} - */ - public OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter() { - this.converter = new OpenSamlMetadataRelyingPartyRegistrationConverter(); - } - @Override public boolean canRead(Class clazz, MediaType mediaType) { return RelyingPartyRegistration.Builder.class.isAssignableFrom(clazz); @@ -89,7 +80,7 @@ public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter @Override public RelyingPartyRegistration.Builder read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { - return this.converter.convert(inputMessage.getBody()).iterator().next(); + return RelyingPartyRegistrations.fromMetadata(inputMessage.getBody()); } @Override diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrations.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrations.java index d7382eafd3..13b8bfeb3a 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 @@ -18,8 +18,12 @@ package org.springframework.security.saml2.provider.service.registration; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.Collection; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; + import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.ResourceLoader; import org.springframework.security.saml2.Saml2Exception; @@ -34,8 +38,6 @@ import org.springframework.security.saml2.Saml2Exception; */ public final class RelyingPartyRegistrations { - private static final OpenSamlMetadataRelyingPartyRegistrationConverter relyingPartyRegistrationConverter = new OpenSamlMetadataRelyingPartyRegistrationConverter(); - private static final ResourceLoader resourceLoader = new DefaultResourceLoader(); private RelyingPartyRegistrations() { @@ -213,7 +215,19 @@ public final class RelyingPartyRegistrations { * @since 5.7 */ public static Collection collectionFromMetadata(InputStream source) { - return relyingPartyRegistrationConverter.convert(source); + Collection builders = new ArrayList<>(); + for (EntityDescriptor descriptor : OpenSamlMetadataUtils.descriptors(source)) { + if (descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) != null) { + OpenSamlAssertingPartyDetails assertingParty = OpenSamlAssertingPartyDetails + .withEntityDescriptor(descriptor) + .build(); + builders.add(new OpenSamlRelyingPartyRegistration.Builder(assertingParty)); + } + } + if (builders.isEmpty()) { + throw new Saml2Exception("Metadata response is missing the necessary IDPSSODescriptor element"); + } + return builders; } } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverterTests.java deleted file mode 100644 index ee270c8672..0000000000 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverterTests.java +++ /dev/null @@ -1,208 +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.BufferedReader; -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 = "\n%s"; - - private static final String ENTITY_DESCRIPTOR_TEMPLATE = "\n%s" - + ""; - - private static final String IDP_SSO_DESCRIPTOR_TEMPLATE = "\n" - + "%s\n" + ""; - - private static final String KEY_DESCRIPTOR_TEMPLATE = "\n" - + "\n" + "\n" - + "" + CERTIFICATE + "\n" + "\n" + "\n" - + ""; - - private static final String EXTENSIONS_TEMPLATE = "" + "" + ""; - - private static final String SINGLE_SIGN_ON_SERVICE_TEMPLATE = ""; - - private OpenSamlMetadataRelyingPartyRegistrationConverter converter = new OpenSamlMetadataRelyingPartyRegistrationConverter(); - - private String metadata; - - @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()); - } - } - - // gh-12667 - @Test - public void convertWhenDefaultsThenAssertingPartyInstanceOfOpenSaml() throws Exception { - try (InputStream source = new ByteArrayInputStream(this.metadata.getBytes(StandardCharsets.UTF_8))) { - this.converter.convert(source) - .forEach((registration) -> assertThat(registration.build().getAssertingPartyDetails()) - .isInstanceOf(OpenSamlAssertingPartyDetails.class)); - } - } - - @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 = ""; - InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); - assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream)) - .withMessage("Unsupported element of type saml2:Assertion"); - } - -} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationsTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationsTests.java index 7d89c99810..9ecbc0bd08 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 @@ -252,6 +252,21 @@ public class RelyingPartyRegistrationsTests { } } + @Test + public void fromMetadataLocationWhenResolvableThenUsesEntityIdAndOpenSamlRelyingPartyRegistration() + throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.enqueue(new MockResponse().setBody(this.metadata).setResponseCode(200)); + RelyingPartyRegistration registration = RelyingPartyRegistrations + .fromMetadataLocation(server.url("/").toString()) + .entityId("rp") + .build(); + RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails(); + assertThat(registration.getRegistrationId()).isEqualTo(details.getEntityId()); + assertThat(registration).isInstanceOf(OpenSamlRelyingPartyRegistration.class); + } + } + @Test public void collectionFromMetadataInputStreamWhenEmptyThenSaml2Exception() throws Exception { try (InputStream source = new ByteArrayInputStream("".getBytes())) {