From 42cece21b49e4167bf182197d3f30c92bdc95f4c Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 28 Feb 2023 15:58:01 -0700 Subject: [PATCH] Add OpenSamlRelyingPartyRegistration Issue gh-12841 --- .../OpenSamlAssertingPartyDetails.java | 88 ++++++- ...etadataAssertingPartyDetailsConverter.java | 220 ------------------ ...dataRelyingPartyRegistrationConverter.java | 204 +++++++++++++++- .../OpenSamlRelyingPartyRegistration.java | 172 ++++++++++++++ ...gistrationBuilderHttpMessageConverter.java | 6 +- .../RelyingPartyRegistration.java | 5 +- ...taAssertingPartyDetailsConverterTests.java | 179 -------------- ...elyingPartyRegistrationConverterTests.java | 142 +++++++++++ 8 files changed, 604 insertions(+), 412 deletions(-) delete mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverter.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistration.java delete mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverterTests.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 5a224ac868..25d5738a6c 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,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> signingMethodAlgorithmsConsumer) { + return (Builder) super.signingAlgorithms(signingMethodAlgorithmsConsumer); + } + + /** + * {@inheritDoc} + */ + @Override + public Builder verificationX509Credentials(Consumer> credentialsConsumer) { + return (Builder) super.verificationX509Credentials(credentialsConsumer); + } + + /** + * {@inheritDoc} + */ + @Override + public Builder encryptionX509Credentials(Consumer> 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} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverter.java deleted file mode 100644 index 3bc8d96e04..0000000000 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverter.java +++ /dev/null @@ -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 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()); - } - - 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 verification = new ArrayList<>(); - List encryption = new ArrayList<>(); - for (KeyDescriptor keyDescriptor : idpssoDescriptor.getKeyDescriptors()) { - if (keyDescriptor.getUse().equals(UsageType.SIGNING)) { - List certificates = certificates(keyDescriptor); - for (X509Certificate certificate : certificates) { - verification.add(Saml2X509Credential.verification(certificate)); - } - } - if (keyDescriptor.getUse().equals(UsageType.ENCRYPTION)) { - List certificates = certificates(keyDescriptor); - for (X509Certificate certificate : certificates) { - encryption.add(Saml2X509Credential.encryption(certificate)); - } - } - if (keyDescriptor.getUse().equals(UsageType.UNSPECIFIED)) { - List certificates = certificates(keyDescriptor); - for (X509Certificate certificate : certificates) { - verification.add(Saml2X509Credential.verification(certificate)); - encryption.add(Saml2X509Credential.encryption(certificate)); - } - } - } - if (verification.isEmpty()) { - throw new Saml2Exception( - "Metadata response is missing verification certificates, necessary for verifying SAML assertions"); - } - RelyingPartyRegistration.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 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 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/OpenSamlMetadataRelyingPartyRegistrationConverter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverter.java index 882707c4f9..5a7e5a00a6 100644 --- 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 @@ -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(); + } - Collection convert(InputStream source) { - Collection builders = new ArrayList<>(); - for (RelyingPartyRegistration.AssertingPartyDetails.Builder builder : this.converter.convert(source)) { - builders.add(new RelyingPartyRegistration.Builder(builder)); + 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"); } - return builders; + 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/OpenSamlRelyingPartyRegistration.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistration.java new file mode 100644 index 0000000000..ceb63ddd9d --- /dev/null +++ b/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> credentialsConsumer) { + return (Builder) super.signingX509Credentials(credentialsConsumer); + } + + @Override + public Builder decryptionX509Credentials(Consumer> 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> 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) { + 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()); + } + + } + +} 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 1996c6246d..3d4069ab8f 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 @@ -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 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 diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java index 7360e810ba..18bddcad34 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java +++ b/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 signingX509Credentials; - private RelyingPartyRegistration(String registrationId, String entityId, String assertionConsumerServiceLocation, + protected RelyingPartyRegistration(String registrationId, String entityId, String assertionConsumerServiceLocation, Saml2MessageBinding assertionConsumerServiceBinding, String singleLogoutServiceLocation, String singleLogoutServiceResponseLocation, Collection singleLogoutServiceBindings, AssertingPartyDetails assertingPartyDetails, String nameIdFormat, diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverterTests.java deleted file mode 100644 index 0c3c0a5e5c..0000000000 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverterTests.java +++ /dev/null @@ -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 = "\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 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 = ""; - 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/OpenSamlMetadataRelyingPartyRegistrationConverterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverterTests.java index c402162031..3a2a18d9e3 100644 --- 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 @@ -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 = "\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; @@ -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 = ""; + InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); + assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream)) + .withMessage("Unsupported element of type saml2:Assertion"); + } + }