From ff9a925e887f75394c34aa2ffae79d9716013cbc Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 2 Aug 2024 18:57:57 -0600 Subject: [PATCH] Use OpenSAML API for metadata Issue gh-11658 --- .../saml2/Saml2MetadataConfigurer.java | 6 +- .../saml2/Saml2MetadataConfigurerTests.java | 4 +- ...ing-security-saml2-service-provider.gradle | 6 + .../BaseOpenSamlMetadataResolver.java | 250 +++++++ .../metadata/OpenSaml4MetadataResolver.java | 117 ++++ .../service/metadata/OpenSaml4Template.java | 617 ++++++++++++++++++ .../metadata/OpenSamlMetadataResolver.java | 83 +-- .../service/metadata/OpenSamlOperations.java | 184 ++++++ .../metadata/OpenSamlSigningUtils.java | 206 ------ .../provider/service/metadata/Saml2Utils.java | 126 +++- .../OpenSaml4MetadataResolverTests.java | 185 ++++++ 11 files changed, 1513 insertions(+), 271 deletions(-) create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/BaseOpenSamlMetadataResolver.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSaml4MetadataResolver.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSaml4Template.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlOperations.java delete mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlSigningUtils.java create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSaml4MetadataResolverTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.java index de20083f1e..e2b9d66360 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.java @@ -22,7 +22,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver; +import org.springframework.security.saml2.provider.service.metadata.OpenSaml4MetadataResolver; import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; @@ -104,7 +104,7 @@ public class Saml2MetadataConfigurer> Assert.hasText(metadataUrl, "metadataUrl cannot be empty"); this.metadataResponseResolver = (registrations) -> { RequestMatcherMetadataResponseResolver metadata = new RequestMatcherMetadataResponseResolver(registrations, - new OpenSamlMetadataResolver()); + new OpenSaml4MetadataResolver()); metadata.setRequestMatcher(new AntPathRequestMatcher(metadataUrl)); return metadata; }; @@ -143,7 +143,7 @@ public class Saml2MetadataConfigurer> return metadataResponseResolver; } RelyingPartyRegistrationRepository registrations = getRelyingPartyRegistrationRepository(http); - return new RequestMatcherMetadataResponseResolver(registrations, new OpenSamlMetadataResolver()); + return new RequestMatcherMetadataResponseResolver(registrations, new OpenSaml4MetadataResolver()); } private RelyingPartyRegistrationRepository getRelyingPartyRegistrationRepository(H http) { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurerTests.java index a7ba5b53de..e75eff1f57 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurerTests.java @@ -30,7 +30,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; -import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver; +import org.springframework.security.saml2.provider.service.metadata.OpenSaml4MetadataResolver; import org.springframework.security.saml2.provider.service.metadata.RequestMatcherMetadataResponseResolver; import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponse; import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver; @@ -159,7 +159,7 @@ public class Saml2MetadataConfigurerTests { // should ignore @Bean Saml2MetadataResponseResolver metadataResponseResolver(RelyingPartyRegistrationRepository registrations) { - return new RequestMatcherMetadataResponseResolver(registrations, new OpenSamlMetadataResolver()); + return new RequestMatcherMetadataResponseResolver(registrations, new OpenSaml4MetadataResolver()); } } diff --git a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle index fa5c9fd1f4..3b1a60b309 100644 --- a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle +++ b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle @@ -19,6 +19,12 @@ sourceSets.configureEach { set -> filter { line -> line.replaceAll(".saml2.internal", ".saml2.provider.service.authentication") } with from } + + copy { + into "$projectDir/src/$set.name/java/org/springframework/security/saml2/provider/service/metadata" + filter { line -> line.replaceAll(".saml2.internal", ".saml2.provider.service.metadata") } + with from + } } dependencies { diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/BaseOpenSamlMetadataResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/BaseOpenSamlMetadataResolver.java new file mode 100644 index 0000000000..ceb1f484e9 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/BaseOpenSamlMetadataResolver.java @@ -0,0 +1,250 @@ +/* + * 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.metadata; + +import java.security.cert.CertificateEncodingException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.saml2.metadata.AssertionConsumerService; +import org.opensaml.saml.saml2.metadata.EntitiesDescriptor; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; +import org.opensaml.saml.saml2.metadata.KeyDescriptor; +import org.opensaml.saml.saml2.metadata.NameIDFormat; +import org.opensaml.saml.saml2.metadata.SPSSODescriptor; +import org.opensaml.saml.saml2.metadata.SingleLogoutService; +import org.opensaml.security.credential.UsageType; +import org.opensaml.xmlsec.signature.KeyInfo; +import org.opensaml.xmlsec.signature.X509Certificate; +import org.opensaml.xmlsec.signature.X509Data; + +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.util.Assert; + +/** + * Resolves the SAML 2.0 Relying Party Metadata for a given + * {@link RelyingPartyRegistration} using the OpenSAML API. + * + * @author Jakub Kubrynski + * @author Josh Cummings + * @since 5.4 + */ +final class BaseOpenSamlMetadataResolver implements Saml2MetadataResolver { + + static { + OpenSamlInitializationService.initialize(); + } + + private final Log logger = LogFactory.getLog(this.getClass()); + + private OpenSamlOperations saml; + + private Consumer entityDescriptorCustomizer = (parameters) -> { + }; + + private boolean usePrettyPrint = true; + + private boolean signMetadata = false; + + BaseOpenSamlMetadataResolver(OpenSamlOperations saml) { + this.saml = saml; + } + + @Override + public String resolve(RelyingPartyRegistration relyingPartyRegistration) { + EntityDescriptor entityDescriptor = entityDescriptor(relyingPartyRegistration); + return serialize(entityDescriptor); + } + + @Override + public String resolve(Iterable relyingPartyRegistrations) { + Collection entityDescriptors = new ArrayList<>(); + for (RelyingPartyRegistration registration : relyingPartyRegistrations) { + EntityDescriptor entityDescriptor = entityDescriptor(registration); + entityDescriptors.add(entityDescriptor); + } + if (entityDescriptors.size() == 1) { + return serialize(entityDescriptors.iterator().next()); + } + EntitiesDescriptor entities = this.saml.build(EntitiesDescriptor.DEFAULT_ELEMENT_NAME); + entities.getEntityDescriptors().addAll(entityDescriptors); + return serialize(entities); + } + + private EntityDescriptor entityDescriptor(RelyingPartyRegistration registration) { + EntityDescriptor entityDescriptor = this.saml.build(EntityDescriptor.DEFAULT_ELEMENT_NAME); + entityDescriptor.setEntityID(registration.getEntityId()); + SPSSODescriptor spSsoDescriptor = buildSpSsoDescriptor(registration); + entityDescriptor.getRoleDescriptors(SPSSODescriptor.DEFAULT_ELEMENT_NAME).add(spSsoDescriptor); + this.entityDescriptorCustomizer.accept(new EntityDescriptorParameters(entityDescriptor, registration)); + if (this.signMetadata) { + return this.saml.withSigningKeys(registration.getSigningX509Credentials()) + .algorithms(registration.getAssertingPartyMetadata().getSigningAlgorithms()) + .sign(entityDescriptor); + } + else { + this.logger.trace("Did not sign metadata since `signMetadata` is `false`"); + } + return entityDescriptor; + } + + /** + * Set a {@link Consumer} for modifying the OpenSAML {@link EntityDescriptor} + * @param entityDescriptorCustomizer a consumer that accepts an + * {@link EntityDescriptorParameters} + * @since 5.7 + */ + void setEntityDescriptorCustomizer(Consumer entityDescriptorCustomizer) { + Assert.notNull(entityDescriptorCustomizer, "entityDescriptorCustomizer cannot be null"); + this.entityDescriptorCustomizer = entityDescriptorCustomizer; + } + + /** + * Configure whether to pretty-print the metadata XML. This can be helpful when + * signing the metadata payload. + * + * @since 6.2 + **/ + void setUsePrettyPrint(boolean usePrettyPrint) { + this.usePrettyPrint = usePrettyPrint; + } + + private SPSSODescriptor buildSpSsoDescriptor(RelyingPartyRegistration registration) { + SPSSODescriptor spSsoDescriptor = this.saml.build(SPSSODescriptor.DEFAULT_ELEMENT_NAME); + spSsoDescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS); + spSsoDescriptor.getKeyDescriptors() + .addAll(buildKeys(registration.getSigningX509Credentials(), UsageType.SIGNING)); + spSsoDescriptor.getKeyDescriptors() + .addAll(buildKeys(registration.getDecryptionX509Credentials(), UsageType.ENCRYPTION)); + spSsoDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService(registration)); + if (registration.getSingleLogoutServiceLocation() != null) { + for (Saml2MessageBinding binding : registration.getSingleLogoutServiceBindings()) { + spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration, binding)); + } + } + if (registration.getNameIdFormat() != null) { + spSsoDescriptor.getNameIDFormats().add(buildNameIDFormat(registration)); + } + return spSsoDescriptor; + } + + private List buildKeys(Collection credentials, UsageType usageType) { + List list = new ArrayList<>(); + for (Saml2X509Credential credential : credentials) { + KeyDescriptor keyDescriptor = buildKeyDescriptor(usageType, credential.getCertificate()); + list.add(keyDescriptor); + } + return list; + } + + private KeyDescriptor buildKeyDescriptor(UsageType usageType, java.security.cert.X509Certificate certificate) { + KeyDescriptor keyDescriptor = this.saml.build(KeyDescriptor.DEFAULT_ELEMENT_NAME); + KeyInfo keyInfo = this.saml.build(KeyInfo.DEFAULT_ELEMENT_NAME); + X509Certificate x509Certificate = this.saml.build(X509Certificate.DEFAULT_ELEMENT_NAME); + X509Data x509Data = this.saml.build(X509Data.DEFAULT_ELEMENT_NAME); + try { + x509Certificate.setValue(new String(Base64.getEncoder().encode(certificate.getEncoded()))); + } + catch (CertificateEncodingException ex) { + throw new Saml2Exception("Cannot encode certificate " + certificate.toString()); + } + x509Data.getX509Certificates().add(x509Certificate); + keyInfo.getX509Datas().add(x509Data); + keyDescriptor.setUse(usageType); + keyDescriptor.setKeyInfo(keyInfo); + return keyDescriptor; + } + + private AssertionConsumerService buildAssertionConsumerService(RelyingPartyRegistration registration) { + AssertionConsumerService assertionConsumerService = this.saml + .build(AssertionConsumerService.DEFAULT_ELEMENT_NAME); + assertionConsumerService.setLocation(registration.getAssertionConsumerServiceLocation()); + assertionConsumerService.setBinding(registration.getAssertionConsumerServiceBinding().getUrn()); + assertionConsumerService.setIndex(1); + return assertionConsumerService; + } + + private SingleLogoutService buildSingleLogoutService(RelyingPartyRegistration registration, + Saml2MessageBinding binding) { + SingleLogoutService singleLogoutService = this.saml.build(SingleLogoutService.DEFAULT_ELEMENT_NAME); + singleLogoutService.setLocation(registration.getSingleLogoutServiceLocation()); + singleLogoutService.setResponseLocation(registration.getSingleLogoutServiceResponseLocation()); + singleLogoutService.setBinding(binding.getUrn()); + return singleLogoutService; + } + + private NameIDFormat buildNameIDFormat(RelyingPartyRegistration registration) { + NameIDFormat nameIdFormat = this.saml.build(NameIDFormat.DEFAULT_ELEMENT_NAME); + nameIdFormat.setURI(registration.getNameIdFormat()); + return nameIdFormat; + } + + private String serialize(EntityDescriptor entityDescriptor) { + return this.saml.serialize(entityDescriptor).prettyPrint(this.usePrettyPrint).serialize(); + } + + private String serialize(EntitiesDescriptor entities) { + return this.saml.serialize(entities).prettyPrint(this.usePrettyPrint).serialize(); + } + + /** + * Configure whether to sign the metadata, defaults to {@code false}. + * + * @since 6.4 + */ + void setSignMetadata(boolean signMetadata) { + this.signMetadata = signMetadata; + } + + /** + * A tuple containing an OpenSAML {@link EntityDescriptor} and its associated + * {@link RelyingPartyRegistration} + * + * @since 5.7 + */ + static final class EntityDescriptorParameters { + + private final EntityDescriptor entityDescriptor; + + private final RelyingPartyRegistration registration; + + EntityDescriptorParameters(EntityDescriptor entityDescriptor, RelyingPartyRegistration registration) { + this.entityDescriptor = entityDescriptor; + this.registration = registration; + } + + EntityDescriptor getEntityDescriptor() { + return this.entityDescriptor; + } + + RelyingPartyRegistration getRelyingPartyRegistration() { + return this.registration; + } + + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSaml4MetadataResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSaml4MetadataResolver.java new file mode 100644 index 0000000000..f8236bc1ff --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSaml4MetadataResolver.java @@ -0,0 +1,117 @@ +/* + * 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.metadata; + +import java.util.function.Consumer; + +import org.opensaml.saml.saml2.metadata.EntityDescriptor; + +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; + +/** + * Resolves the SAML 2.0 Relying Party Metadata for a given + * {@link RelyingPartyRegistration} using the OpenSAML API. + * + * @author Jakub Kubrynski + * @author Josh Cummings + * @since 5.4 + */ +public final class OpenSaml4MetadataResolver implements Saml2MetadataResolver { + + static { + OpenSamlInitializationService.initialize(); + } + + private final BaseOpenSamlMetadataResolver delegate; + + public OpenSaml4MetadataResolver() { + this.delegate = new BaseOpenSamlMetadataResolver(new OpenSaml4Template()); + } + + @Override + public String resolve(RelyingPartyRegistration relyingPartyRegistration) { + return this.delegate.resolve(relyingPartyRegistration); + } + + public String resolve(Iterable relyingPartyRegistrations) { + return this.delegate.resolve(relyingPartyRegistrations); + } + + /** + * Set a {@link Consumer} for modifying the OpenSAML {@link EntityDescriptor} + * @param entityDescriptorCustomizer a consumer that accepts an + * {@link EntityDescriptorParameters} + * @since 5.7 + */ + public void setEntityDescriptorCustomizer(Consumer entityDescriptorCustomizer) { + this.delegate.setEntityDescriptorCustomizer( + (parameters) -> entityDescriptorCustomizer.accept(new EntityDescriptorParameters(parameters))); + } + + /** + * Configure whether to pretty-print the metadata XML. This can be helpful when + * signing the metadata payload. + * + * @since 6.2 + **/ + public void setUsePrettyPrint(boolean usePrettyPrint) { + this.delegate.setUsePrettyPrint(usePrettyPrint); + } + + /** + * Configure whether to sign the metadata, defaults to {@code false}. + * + * @since 6.4 + */ + public void setSignMetadata(boolean signMetadata) { + this.delegate.setSignMetadata(signMetadata); + } + + /** + * A tuple containing an OpenSAML {@link EntityDescriptor} and its associated + * {@link RelyingPartyRegistration} + * + * @since 5.7 + */ + public static final class EntityDescriptorParameters { + + private final EntityDescriptor entityDescriptor; + + private final RelyingPartyRegistration registration; + + public EntityDescriptorParameters(EntityDescriptor entityDescriptor, RelyingPartyRegistration registration) { + this.entityDescriptor = entityDescriptor; + this.registration = registration; + } + + EntityDescriptorParameters(BaseOpenSamlMetadataResolver.EntityDescriptorParameters parameters) { + this.entityDescriptor = parameters.getEntityDescriptor(); + this.registration = parameters.getRelyingPartyRegistration(); + } + + public EntityDescriptor getEntityDescriptor() { + return this.entityDescriptor; + } + + public RelyingPartyRegistration getRelyingPartyRegistration() { + return this.registration; + } + + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSaml4Template.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSaml4Template.java new file mode 100644 index 0000000000..8cd40194fe --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSaml4Template.java @@ -0,0 +1,617 @@ +/* + * 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.metadata; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.xml.namespace.QName; + +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; +import net.shibboleth.utilities.java.support.xml.SerializeSupport; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.opensaml.core.criterion.EntityIdCriterion; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.XMLObjectBuilder; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.core.xml.io.Marshaller; +import org.opensaml.core.xml.io.MarshallingException; +import org.opensaml.core.xml.io.Unmarshaller; +import org.opensaml.core.xml.io.UnmarshallerFactory; +import org.opensaml.core.xml.util.XMLObjectSupport; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.criterion.ProtocolCriterion; +import org.opensaml.saml.ext.saml2delrestrict.Delegate; +import org.opensaml.saml.ext.saml2delrestrict.DelegationRestrictionType; +import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion; +import org.opensaml.saml.saml2.core.Assertion; +import org.opensaml.saml.saml2.core.Attribute; +import org.opensaml.saml.saml2.core.AttributeStatement; +import org.opensaml.saml.saml2.core.Condition; +import org.opensaml.saml.saml2.core.EncryptedAssertion; +import org.opensaml.saml.saml2.core.EncryptedAttribute; +import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.LogoutRequest; +import org.opensaml.saml.saml2.core.NameID; +import org.opensaml.saml.saml2.core.RequestAbstractType; +import org.opensaml.saml.saml2.core.Response; +import org.opensaml.saml.saml2.core.StatusResponseType; +import org.opensaml.saml.saml2.core.Subject; +import org.opensaml.saml.saml2.core.SubjectConfirmation; +import org.opensaml.saml.saml2.encryption.Decrypter; +import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver; +import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver; +import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; +import org.opensaml.security.SecurityException; +import org.opensaml.security.credential.BasicCredential; +import org.opensaml.security.credential.Credential; +import org.opensaml.security.credential.CredentialResolver; +import org.opensaml.security.credential.CredentialSupport; +import org.opensaml.security.credential.UsageType; +import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion; +import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion; +import org.opensaml.security.credential.impl.CollectionCredentialResolver; +import org.opensaml.security.criteria.UsageCriterion; +import org.opensaml.security.x509.BasicX509Credential; +import org.opensaml.xmlsec.SignatureSigningParameters; +import org.opensaml.xmlsec.SignatureSigningParametersResolver; +import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap; +import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion; +import org.opensaml.xmlsec.crypto.XMLSigningUtil; +import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.DecryptionException; +import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver; +import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration; +import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver; +import org.opensaml.xmlsec.keyinfo.KeyInfoGeneratorManager; +import org.opensaml.xmlsec.keyinfo.NamedKeyInfoGeneratorManager; +import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver; +import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory; +import org.opensaml.xmlsec.signature.SignableXMLObject; +import org.opensaml.xmlsec.signature.Signature; +import org.opensaml.xmlsec.signature.support.SignatureConstants; +import org.opensaml.xmlsec.signature.support.SignatureSupport; +import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; +import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.core.Saml2ErrorCodes; +import org.springframework.security.saml2.core.Saml2ParameterNames; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.util.Assert; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; + +/** + * For internal use only. Subject to breaking changes at any time. + */ +final class OpenSaml4Template implements OpenSamlOperations { + + private static final Log logger = LogFactory.getLog(OpenSaml4Template.class); + + @Override + public T build(QName elementName) { + XMLObjectBuilder builder = XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(elementName); + if (builder == null) { + throw new Saml2Exception("Unable to resolve Builder for " + elementName); + } + return (T) builder.buildObject(elementName); + } + + @Override + public T deserialize(String serialized) { + return deserialize(new ByteArrayInputStream(serialized.getBytes(StandardCharsets.UTF_8))); + } + + @Override + public T deserialize(InputStream serialized) { + try { + Document document = XMLObjectProviderRegistrySupport.getParserPool().parse(serialized); + Element element = document.getDocumentElement(); + UnmarshallerFactory factory = XMLObjectProviderRegistrySupport.getUnmarshallerFactory(); + Unmarshaller unmarshaller = factory.getUnmarshaller(element); + if (unmarshaller == null) { + throw new Saml2Exception("Unsupported element of type " + element.getTagName()); + } + return (T) unmarshaller.unmarshall(element); + } + catch (Saml2Exception ex) { + throw ex; + } + catch (Exception ex) { + throw new Saml2Exception("Failed to deserialize payload", ex); + } + } + + @Override + public OpenSaml4SerializationConfigurer serialize(XMLObject object) { + Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object); + try { + return serialize(marshaller.marshall(object)); + } + catch (MarshallingException ex) { + throw new Saml2Exception(ex); + } + } + + @Override + public OpenSaml4SerializationConfigurer serialize(Element element) { + return new OpenSaml4SerializationConfigurer(element); + } + + @Override + public OpenSaml4SignatureConfigurer withSigningKeys(Collection credentials) { + return new OpenSaml4SignatureConfigurer(credentials); + } + + @Override + public OpenSaml4VerificationConfigurer withVerificationKeys(Collection credentials) { + return new OpenSaml4VerificationConfigurer(credentials); + } + + @Override + public OpenSaml4DecryptionConfigurer withDecryptionKeys(Collection credentials) { + return new OpenSaml4DecryptionConfigurer(credentials); + } + + OpenSaml4Template() { + + } + + static final class OpenSaml4SerializationConfigurer + implements SerializationConfigurer { + + private final Element element; + + boolean pretty; + + OpenSaml4SerializationConfigurer(Element element) { + this.element = element; + } + + @Override + public OpenSaml4SerializationConfigurer prettyPrint(boolean pretty) { + this.pretty = pretty; + return this; + } + + @Override + public String serialize() { + if (this.pretty) { + return SerializeSupport.prettyPrintXML(this.element); + } + return SerializeSupport.nodeToString(this.element); + } + + } + + static final class OpenSaml4SignatureConfigurer implements SignatureConfigurer { + + private final Collection credentials; + + private final Map components = new LinkedHashMap<>(); + + private List algs = List.of(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); + + OpenSaml4SignatureConfigurer(Collection credentials) { + this.credentials = credentials; + } + + @Override + public OpenSaml4SignatureConfigurer algorithms(List algs) { + this.algs = algs; + return this; + } + + @Override + public O sign(O object) { + SignatureSigningParameters parameters = resolveSigningParameters(); + try { + SignatureSupport.signObject(object, parameters); + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + return object; + } + + @Override + public Map sign(Map params) { + SignatureSigningParameters parameters = resolveSigningParameters(); + this.components.putAll(params); + Credential credential = parameters.getSigningCredential(); + String algorithmUri = parameters.getSignatureAlgorithm(); + this.components.put(Saml2ParameterNames.SIG_ALG, algorithmUri); + UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); + for (Map.Entry component : this.components.entrySet()) { + builder.queryParam(component.getKey(), + UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1)); + } + String queryString = builder.build(true).toString().substring(1); + try { + byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri, + queryString.getBytes(StandardCharsets.UTF_8)); + String b64Signature = Saml2Utils.samlEncode(rawSignature); + this.components.put(Saml2ParameterNames.SIGNATURE, b64Signature); + } + catch (SecurityException ex) { + throw new Saml2Exception(ex); + } + return this.components; + } + + private SignatureSigningParameters resolveSigningParameters() { + List credentials = resolveSigningCredentials(); + List digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256); + String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS; + SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver(); + BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration(); + signingConfiguration.setSigningCredentials(credentials); + signingConfiguration.setSignatureAlgorithms(this.algs); + signingConfiguration.setSignatureReferenceDigestMethods(digests); + signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization); + signingConfiguration.setKeyInfoGeneratorManager(buildSignatureKeyInfoGeneratorManager()); + CriteriaSet criteria = new CriteriaSet(new SignatureSigningConfigurationCriterion(signingConfiguration)); + try { + SignatureSigningParameters parameters = resolver.resolveSingle(criteria); + Assert.notNull(parameters, "Failed to resolve any signing credential"); + return parameters; + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + + private NamedKeyInfoGeneratorManager buildSignatureKeyInfoGeneratorManager() { + final NamedKeyInfoGeneratorManager namedManager = new NamedKeyInfoGeneratorManager(); + + namedManager.setUseDefaultManager(true); + final KeyInfoGeneratorManager defaultManager = namedManager.getDefaultManager(); + + // Generator for X509Credentials + final X509KeyInfoGeneratorFactory x509Factory = new X509KeyInfoGeneratorFactory(); + x509Factory.setEmitEntityCertificate(true); + x509Factory.setEmitEntityCertificateChain(true); + + defaultManager.registerFactory(x509Factory); + + return namedManager; + } + + private List resolveSigningCredentials() { + List credentials = new ArrayList<>(); + for (Saml2X509Credential x509Credential : this.credentials) { + X509Certificate certificate = x509Credential.getCertificate(); + PrivateKey privateKey = x509Credential.getPrivateKey(); + BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey); + credential.setUsageType(UsageType.SIGNING); + credentials.add(credential); + } + return credentials; + } + + } + + static final class OpenSaml4VerificationConfigurer implements VerificationConfigurer { + + private final Collection credentials; + + private String entityId; + + OpenSaml4VerificationConfigurer(Collection credentials) { + this.credentials = credentials; + } + + @Override + public VerificationConfigurer entityId(String entityId) { + this.entityId = entityId; + return this; + } + + private SignatureTrustEngine trustEngine(Collection keys) { + Set credentials = new HashSet<>(); + for (Saml2X509Credential key : keys) { + BasicX509Credential cred = new BasicX509Credential(key.getCertificate()); + cred.setUsageType(UsageType.SIGNING); + cred.setEntityId(this.entityId); + credentials.add(cred); + } + CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials); + return new ExplicitKeySignatureTrustEngine(credentialsResolver, + DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver()); + } + + private CriteriaSet verificationCriteria(Issuer issuer) { + return new CriteriaSet(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer.getValue())), + new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS)), + new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING))); + } + + @Override + public Collection verify(SignableXMLObject signable) { + if (signable instanceof StatusResponseType response) { + return verifySignature(response.getID(), response.getIssuer(), response.getSignature()); + } + if (signable instanceof RequestAbstractType request) { + return verifySignature(request.getID(), request.getIssuer(), request.getSignature()); + } + if (signable instanceof Assertion assertion) { + return verifySignature(assertion.getID(), assertion.getIssuer(), assertion.getSignature()); + } + throw new Saml2Exception("Unsupported object of type: " + signable.getClass().getName()); + } + + private Collection verifySignature(String id, Issuer issuer, Signature signature) { + SignatureTrustEngine trustEngine = trustEngine(this.credentials); + CriteriaSet criteria = verificationCriteria(issuer); + Collection errors = new ArrayList<>(); + SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator(); + try { + profileValidator.validate(signature); + } + catch (Exception ex) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for object [" + id + "]: ")); + } + + try { + if (!trustEngine.validate(signature, criteria)) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for object [" + id + "]")); + } + } + catch (Exception ex) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for object [" + id + "]: ")); + } + + return errors; + } + + @Override + public Collection verify(RedirectParameters parameters) { + SignatureTrustEngine trustEngine = trustEngine(this.credentials); + CriteriaSet criteria = verificationCriteria(parameters.getIssuer()); + if (parameters.getAlgorithm() == null) { + return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Missing signature algorithm for object [" + parameters.getId() + "]")); + } + if (!parameters.hasSignature()) { + return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Missing signature for object [" + parameters.getId() + "]")); + } + Collection errors = new ArrayList<>(); + String algorithmUri = parameters.getAlgorithm(); + try { + if (!trustEngine.validate(parameters.getSignature(), parameters.getContent(), algorithmUri, criteria, + null)) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for object [" + parameters.getId() + "]")); + } + } + catch (Exception ex) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, + "Invalid signature for object [" + parameters.getId() + "]: ")); + } + return errors; + } + + } + + static final class OpenSaml4DecryptionConfigurer implements DecryptionConfigurer { + + private static final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver( + Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(), + new SimpleRetrievalMethodEncryptedKeyResolver())); + + private final Decrypter decrypter; + + OpenSaml4DecryptionConfigurer(Collection decryptionCredentials) { + this.decrypter = decrypter(decryptionCredentials); + } + + private static Decrypter decrypter(Collection decryptionCredentials) { + Collection credentials = new ArrayList<>(); + for (Saml2X509Credential key : decryptionCredentials) { + Credential cred = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey()); + credentials.add(cred); + } + KeyInfoCredentialResolver resolver = new CollectionKeyInfoCredentialResolver(credentials); + Decrypter decrypter = new Decrypter(null, resolver, encryptedKeyResolver); + decrypter.setRootInNewDocument(true); + return decrypter; + } + + @Override + public void decrypt(XMLObject object) { + if (object instanceof Response response) { + decryptResponse(response); + return; + } + if (object instanceof Assertion assertion) { + decryptAssertion(assertion); + } + if (object instanceof LogoutRequest request) { + decryptLogoutRequest(request); + } + } + + /* + * The methods that follow are adapted from OpenSAML's {@link DecryptAssertions}, + * {@link DecryptNameIDs}, and {@link DecryptAttributes}. + * + *

The reason that these OpenSAML classes are not used directly is because they + * reference {@link javax.servlet.http.HttpServletRequest} which is a lower + * Servlet API version than what Spring Security SAML uses. + * + * If OpenSAML 5 updates to {@link jakarta.servlet.http.HttpServletRequest}, then + * this arrangement can be revisited. + */ + + private void decryptResponse(Response response) { + Collection decrypteds = new ArrayList<>(); + Collection encrypteds = new ArrayList<>(); + + int count = 0; + int size = response.getEncryptedAssertions().size(); + for (EncryptedAssertion encrypted : response.getEncryptedAssertions()) { + logger.trace(String.format("Decrypting EncryptedAssertion (%d/%d) in Response [%s]", count, size, + response.getID())); + try { + Assertion decrypted = this.decrypter.decrypt(encrypted); + if (decrypted != null) { + encrypteds.add(encrypted); + decrypteds.add(decrypted); + } + count++; + } + catch (DecryptionException ex) { + throw new Saml2Exception(ex); + } + } + + response.getEncryptedAssertions().removeAll(encrypteds); + response.getAssertions().addAll(decrypteds); + + // Re-marshall the response so that any ID attributes within the decrypted + // Assertions + // will have their ID-ness re-established at the DOM level. + if (!decrypteds.isEmpty()) { + try { + XMLObjectSupport.marshall(response); + } + catch (final MarshallingException ex) { + throw new Saml2Exception(ex); + } + } + } + + private void decryptAssertion(Assertion assertion) { + for (AttributeStatement statement : assertion.getAttributeStatements()) { + decryptAttributes(statement); + } + decryptSubject(assertion.getSubject()); + if (assertion.getConditions() != null) { + for (Condition c : assertion.getConditions().getConditions()) { + if (!(c instanceof DelegationRestrictionType delegation)) { + continue; + } + for (Delegate d : delegation.getDelegates()) { + if (d.getEncryptedID() != null) { + try { + NameID decrypted = (NameID) this.decrypter.decrypt(d.getEncryptedID()); + if (decrypted != null) { + d.setNameID(decrypted); + d.setEncryptedID(null); + } + } + catch (DecryptionException ex) { + throw new Saml2Exception(ex); + } + } + } + } + } + } + + private void decryptAttributes(AttributeStatement statement) { + Collection decrypteds = new ArrayList<>(); + Collection encrypteds = new ArrayList<>(); + for (EncryptedAttribute encrypted : statement.getEncryptedAttributes()) { + try { + Attribute decrypted = this.decrypter.decrypt(encrypted); + if (decrypted != null) { + encrypteds.add(encrypted); + decrypteds.add(decrypted); + } + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + statement.getEncryptedAttributes().removeAll(encrypteds); + statement.getAttributes().addAll(decrypteds); + } + + private void decryptSubject(Subject subject) { + if (subject != null) { + if (subject.getEncryptedID() != null) { + try { + NameID decrypted = (NameID) this.decrypter.decrypt(subject.getEncryptedID()); + if (decrypted != null) { + subject.setNameID(decrypted); + subject.setEncryptedID(null); + } + } + catch (final DecryptionException ex) { + throw new Saml2Exception(ex); + } + } + + for (final SubjectConfirmation sc : subject.getSubjectConfirmations()) { + if (sc.getEncryptedID() != null) { + try { + NameID decrypted = (NameID) this.decrypter.decrypt(sc.getEncryptedID()); + if (decrypted != null) { + sc.setNameID(decrypted); + sc.setEncryptedID(null); + } + } + catch (final DecryptionException ex) { + throw new Saml2Exception(ex); + } + } + } + } + } + + private void decryptLogoutRequest(LogoutRequest request) { + if (request.getEncryptedID() != null) { + try { + NameID decrypted = (NameID) this.decrypter.decrypt(request.getEncryptedID()); + if (decrypted != null) { + request.setNameID(decrypted); + request.setEncryptedID(null); + } + } + catch (DecryptionException ex) { + throw new Saml2Exception(ex); + } + } + } + + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java index 147c402106..aebe13b37f 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java @@ -23,13 +23,8 @@ import java.util.Collection; import java.util.List; import java.util.function.Consumer; -import javax.xml.namespace.QName; - -import net.shibboleth.utilities.java.support.xml.SerializeSupport; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.opensaml.core.xml.XMLObjectBuilder; -import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; import org.opensaml.saml.common.xml.SAMLConstants; import org.opensaml.saml.saml2.metadata.AssertionConsumerService; import org.opensaml.saml.saml2.metadata.EntitiesDescriptor; @@ -38,13 +33,10 @@ import org.opensaml.saml.saml2.metadata.KeyDescriptor; import org.opensaml.saml.saml2.metadata.NameIDFormat; import org.opensaml.saml.saml2.metadata.SPSSODescriptor; import org.opensaml.saml.saml2.metadata.SingleLogoutService; -import org.opensaml.saml.saml2.metadata.impl.EntitiesDescriptorMarshaller; -import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller; import org.opensaml.security.credential.UsageType; import org.opensaml.xmlsec.signature.KeyInfo; import org.opensaml.xmlsec.signature.X509Certificate; import org.opensaml.xmlsec.signature.X509Data; -import org.w3c.dom.Element; import org.springframework.security.saml2.Saml2Exception; import org.springframework.security.saml2.core.OpenSamlInitializationService; @@ -60,7 +52,10 @@ import org.springframework.util.Assert; * @author Jakub Kubrynski * @author Josh Cummings * @since 5.4 + * @deprecated Please use version-specific {@link Saml2MetadataResolver} instead, for + * example {@code OpenSaml4MetadataResolver} */ +@Deprecated public final class OpenSamlMetadataResolver implements Saml2MetadataResolver { static { @@ -69,9 +64,7 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver { private final Log logger = LogFactory.getLog(this.getClass()); - private final EntityDescriptorMarshaller entityDescriptorMarshaller; - - private final EntitiesDescriptorMarshaller entitiesDescriptorMarshaller; + private OpenSamlOperations saml = new OpenSaml4Template(); private Consumer entityDescriptorCustomizer = (parameters) -> { }; @@ -81,14 +74,10 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver { private boolean signMetadata = false; public OpenSamlMetadataResolver() { - this.entityDescriptorMarshaller = (EntityDescriptorMarshaller) XMLObjectProviderRegistrySupport - .getMarshallerFactory() - .getMarshaller(EntityDescriptor.DEFAULT_ELEMENT_NAME); - Assert.notNull(this.entityDescriptorMarshaller, "entityDescriptorMarshaller cannot be null"); - this.entitiesDescriptorMarshaller = (EntitiesDescriptorMarshaller) XMLObjectProviderRegistrySupport - .getMarshallerFactory() - .getMarshaller(EntitiesDescriptor.DEFAULT_ELEMENT_NAME); - Assert.notNull(this.entitiesDescriptorMarshaller, "entitiesDescriptorMarshaller cannot be null"); + } + + OpenSamlMetadataResolver(OpenSamlOperations saml) { + this.saml = saml; } @Override @@ -106,19 +95,21 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver { if (entityDescriptors.size() == 1) { return serialize(entityDescriptors.iterator().next()); } - EntitiesDescriptor entities = build(EntitiesDescriptor.DEFAULT_ELEMENT_NAME); + EntitiesDescriptor entities = this.saml.build(EntitiesDescriptor.DEFAULT_ELEMENT_NAME); entities.getEntityDescriptors().addAll(entityDescriptors); return serialize(entities); } private EntityDescriptor entityDescriptor(RelyingPartyRegistration registration) { - EntityDescriptor entityDescriptor = build(EntityDescriptor.DEFAULT_ELEMENT_NAME); + EntityDescriptor entityDescriptor = this.saml.build(EntityDescriptor.DEFAULT_ELEMENT_NAME); entityDescriptor.setEntityID(registration.getEntityId()); SPSSODescriptor spSsoDescriptor = buildSpSsoDescriptor(registration); entityDescriptor.getRoleDescriptors(SPSSODescriptor.DEFAULT_ELEMENT_NAME).add(spSsoDescriptor); this.entityDescriptorCustomizer.accept(new EntityDescriptorParameters(entityDescriptor, registration)); if (this.signMetadata) { - return OpenSamlSigningUtils.sign(entityDescriptor, registration); + return this.saml.withSigningKeys(registration.getSigningX509Credentials()) + .algorithms(registration.getAssertingPartyMetadata().getSigningAlgorithms()) + .sign(entityDescriptor); } else { this.logger.trace("Did not sign metadata since `signMetadata` is `false`"); @@ -148,7 +139,7 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver { } private SPSSODescriptor buildSpSsoDescriptor(RelyingPartyRegistration registration) { - SPSSODescriptor spSsoDescriptor = build(SPSSODescriptor.DEFAULT_ELEMENT_NAME); + SPSSODescriptor spSsoDescriptor = this.saml.build(SPSSODescriptor.DEFAULT_ELEMENT_NAME); spSsoDescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS); spSsoDescriptor.getKeyDescriptors() .addAll(buildKeys(registration.getSigningX509Credentials(), UsageType.SIGNING)); @@ -176,10 +167,10 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver { } private KeyDescriptor buildKeyDescriptor(UsageType usageType, java.security.cert.X509Certificate certificate) { - KeyDescriptor keyDescriptor = build(KeyDescriptor.DEFAULT_ELEMENT_NAME); - KeyInfo keyInfo = build(KeyInfo.DEFAULT_ELEMENT_NAME); - X509Certificate x509Certificate = build(X509Certificate.DEFAULT_ELEMENT_NAME); - X509Data x509Data = build(X509Data.DEFAULT_ELEMENT_NAME); + KeyDescriptor keyDescriptor = this.saml.build(KeyDescriptor.DEFAULT_ELEMENT_NAME); + KeyInfo keyInfo = this.saml.build(KeyInfo.DEFAULT_ELEMENT_NAME); + X509Certificate x509Certificate = this.saml.build(X509Certificate.DEFAULT_ELEMENT_NAME); + X509Data x509Data = this.saml.build(X509Data.DEFAULT_ELEMENT_NAME); try { x509Certificate.setValue(new String(Base64.getEncoder().encode(certificate.getEncoded()))); } @@ -194,7 +185,8 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver { } private AssertionConsumerService buildAssertionConsumerService(RelyingPartyRegistration registration) { - AssertionConsumerService assertionConsumerService = build(AssertionConsumerService.DEFAULT_ELEMENT_NAME); + AssertionConsumerService assertionConsumerService = this.saml + .build(AssertionConsumerService.DEFAULT_ELEMENT_NAME); assertionConsumerService.setLocation(registration.getAssertionConsumerServiceLocation()); assertionConsumerService.setBinding(registration.getAssertionConsumerServiceBinding().getUrn()); assertionConsumerService.setIndex(1); @@ -203,7 +195,7 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver { private SingleLogoutService buildSingleLogoutService(RelyingPartyRegistration registration, Saml2MessageBinding binding) { - SingleLogoutService singleLogoutService = build(SingleLogoutService.DEFAULT_ELEMENT_NAME); + SingleLogoutService singleLogoutService = this.saml.build(SingleLogoutService.DEFAULT_ELEMENT_NAME); singleLogoutService.setLocation(registration.getSingleLogoutServiceLocation()); singleLogoutService.setResponseLocation(registration.getSingleLogoutServiceResponseLocation()); singleLogoutService.setBinding(binding.getUrn()); @@ -211,44 +203,17 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver { } private NameIDFormat buildNameIDFormat(RelyingPartyRegistration registration) { - NameIDFormat nameIdFormat = build(NameIDFormat.DEFAULT_ELEMENT_NAME); + NameIDFormat nameIdFormat = this.saml.build(NameIDFormat.DEFAULT_ELEMENT_NAME); nameIdFormat.setURI(registration.getNameIdFormat()); return nameIdFormat; } - @SuppressWarnings("unchecked") - private T build(QName elementName) { - XMLObjectBuilder builder = XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(elementName); - if (builder == null) { - throw new Saml2Exception("Unable to resolve Builder for " + elementName); - } - return (T) builder.buildObject(elementName); - } - private String serialize(EntityDescriptor entityDescriptor) { - try { - Element element = this.entityDescriptorMarshaller.marshall(entityDescriptor); - if (this.usePrettyPrint) { - return SerializeSupport.prettyPrintXML(element); - } - return SerializeSupport.nodeToString(element); - } - catch (Exception ex) { - throw new Saml2Exception(ex); - } + return this.saml.serialize(entityDescriptor).prettyPrint(this.usePrettyPrint).serialize(); } private String serialize(EntitiesDescriptor entities) { - try { - Element element = this.entitiesDescriptorMarshaller.marshall(entities); - if (this.usePrettyPrint) { - return SerializeSupport.prettyPrintXML(element); - } - return SerializeSupport.nodeToString(element); - } - catch (Exception ex) { - throw new Saml2Exception(ex); - } + return this.saml.serialize(entities).prettyPrint(this.usePrettyPrint).serialize(); } /** diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlOperations.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlOperations.java new file mode 100644 index 0000000000..f021de3c33 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlOperations.java @@ -0,0 +1,184 @@ +/* + * 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.metadata; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import javax.xml.namespace.QName; + +import org.opensaml.core.xml.XMLObject; +import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.RequestAbstractType; +import org.opensaml.saml.saml2.core.StatusResponseType; +import org.opensaml.xmlsec.signature.SignableXMLObject; +import org.w3c.dom.Element; + +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.core.Saml2ParameterNames; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.web.util.UriComponentsBuilder; + +interface OpenSamlOperations { + + T build(QName elementName); + + T deserialize(String serialized); + + T deserialize(InputStream serialized); + + SerializationConfigurer serialize(XMLObject object); + + SerializationConfigurer serialize(Element element); + + SignatureConfigurer withSigningKeys(Collection credentials); + + VerificationConfigurer withVerificationKeys(Collection credentials); + + DecryptionConfigurer withDecryptionKeys(Collection credentials); + + interface SerializationConfigurer> { + + B prettyPrint(boolean pretty); + + String serialize(); + + } + + interface SignatureConfigurer> { + + B algorithms(List algs); + + O sign(O object); + + Map sign(Map params); + + } + + interface VerificationConfigurer { + + VerificationConfigurer entityId(String entityId); + + Collection verify(SignableXMLObject signable); + + Collection verify(VerificationConfigurer.RedirectParameters parameters); + + final class RedirectParameters { + + private final String id; + + private final Issuer issuer; + + private final String algorithm; + + private final byte[] signature; + + private final byte[] content; + + RedirectParameters(Map parameters, String parametersQuery, RequestAbstractType request) { + this.id = request.getID(); + this.issuer = request.getIssuer(); + this.algorithm = parameters.get(Saml2ParameterNames.SIG_ALG); + if (parameters.get(Saml2ParameterNames.SIGNATURE) != null) { + this.signature = Saml2Utils.samlDecode(parameters.get(Saml2ParameterNames.SIGNATURE)); + } + else { + this.signature = null; + } + Map queryParams = UriComponentsBuilder.newInstance() + .query(parametersQuery) + .build(true) + .getQueryParams() + .toSingleValueMap(); + String relayState = parameters.get(Saml2ParameterNames.RELAY_STATE); + this.content = getContent(Saml2ParameterNames.SAML_REQUEST, relayState, queryParams); + } + + RedirectParameters(Map parameters, String parametersQuery, StatusResponseType response) { + this.id = response.getID(); + this.issuer = response.getIssuer(); + this.algorithm = parameters.get(Saml2ParameterNames.SIG_ALG); + if (parameters.get(Saml2ParameterNames.SIGNATURE) != null) { + this.signature = Saml2Utils.samlDecode(parameters.get(Saml2ParameterNames.SIGNATURE)); + } + else { + this.signature = null; + } + Map queryParams = UriComponentsBuilder.newInstance() + .query(parametersQuery) + .build(true) + .getQueryParams() + .toSingleValueMap(); + String relayState = parameters.get(Saml2ParameterNames.RELAY_STATE); + this.content = getContent(Saml2ParameterNames.SAML_RESPONSE, relayState, queryParams); + } + + static byte[] getContent(String samlObject, String relayState, final Map queryParams) { + if (Objects.nonNull(relayState)) { + return String + .format("%s=%s&%s=%s&%s=%s", samlObject, queryParams.get(samlObject), + Saml2ParameterNames.RELAY_STATE, queryParams.get(Saml2ParameterNames.RELAY_STATE), + Saml2ParameterNames.SIG_ALG, queryParams.get(Saml2ParameterNames.SIG_ALG)) + .getBytes(StandardCharsets.UTF_8); + } + else { + return String + .format("%s=%s&%s=%s", samlObject, queryParams.get(samlObject), Saml2ParameterNames.SIG_ALG, + queryParams.get(Saml2ParameterNames.SIG_ALG)) + .getBytes(StandardCharsets.UTF_8); + } + } + + String getId() { + return this.id; + } + + Issuer getIssuer() { + return this.issuer; + } + + byte[] getContent() { + return this.content; + } + + String getAlgorithm() { + return this.algorithm; + } + + byte[] getSignature() { + return this.signature; + } + + boolean hasSignature() { + return this.signature != null; + } + + } + + } + + interface DecryptionConfigurer { + + void decrypt(XMLObject object); + + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlSigningUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlSigningUtils.java deleted file mode 100644 index ae1fcb63fe..0000000000 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlSigningUtils.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * 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.metadata; - -import java.nio.charset.StandardCharsets; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import net.shibboleth.utilities.java.support.resolver.CriteriaSet; -import net.shibboleth.utilities.java.support.xml.SerializeSupport; -import org.opensaml.core.xml.XMLObject; -import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; -import org.opensaml.core.xml.io.Marshaller; -import org.opensaml.core.xml.io.MarshallingException; -import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver; -import org.opensaml.security.SecurityException; -import org.opensaml.security.credential.BasicCredential; -import org.opensaml.security.credential.Credential; -import org.opensaml.security.credential.CredentialSupport; -import org.opensaml.security.credential.UsageType; -import org.opensaml.xmlsec.SignatureSigningParameters; -import org.opensaml.xmlsec.SignatureSigningParametersResolver; -import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion; -import org.opensaml.xmlsec.crypto.XMLSigningUtil; -import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration; -import org.opensaml.xmlsec.keyinfo.KeyInfoGeneratorManager; -import org.opensaml.xmlsec.keyinfo.NamedKeyInfoGeneratorManager; -import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory; -import org.opensaml.xmlsec.signature.SignableXMLObject; -import org.opensaml.xmlsec.signature.support.SignatureConstants; -import org.opensaml.xmlsec.signature.support.SignatureSupport; -import org.w3c.dom.Element; - -import org.springframework.security.saml2.Saml2Exception; -import org.springframework.security.saml2.core.Saml2ParameterNames; -import org.springframework.security.saml2.core.Saml2X509Credential; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.util.Assert; -import org.springframework.web.util.UriComponentsBuilder; -import org.springframework.web.util.UriUtils; - -/** - * Utility methods for signing SAML components with OpenSAML - * - * For internal use only. - * - * @author Josh Cummings - * @since 6.3 - */ -final class OpenSamlSigningUtils { - - static String serialize(XMLObject object) { - try { - Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object); - Element element = marshaller.marshall(object); - return SerializeSupport.nodeToString(element); - } - catch (MarshallingException ex) { - throw new Saml2Exception(ex); - } - } - - static O sign(O object, RelyingPartyRegistration relyingPartyRegistration) { - List algorithms = relyingPartyRegistration.getAssertingPartyMetadata().getSigningAlgorithms(); - List credentials = resolveSigningCredentials(relyingPartyRegistration); - return sign(object, algorithms, credentials); - } - - static O sign(O object, List algorithms, List credentials) { - SignatureSigningParameters parameters = resolveSigningParameters(algorithms, credentials); - try { - SignatureSupport.signObject(object, parameters); - return object; - } - catch (Exception ex) { - throw new Saml2Exception(ex); - } - } - - static QueryParametersPartial sign(RelyingPartyRegistration registration) { - return new QueryParametersPartial(registration); - } - - private static SignatureSigningParameters resolveSigningParameters( - RelyingPartyRegistration relyingPartyRegistration) { - List credentials = resolveSigningCredentials(relyingPartyRegistration); - List algorithms = relyingPartyRegistration.getAssertingPartyMetadata().getSigningAlgorithms(); - return resolveSigningParameters(algorithms, credentials); - } - - private static SignatureSigningParameters resolveSigningParameters(List algorithms, - List credentials) { - List digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256); - String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS; - SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver(); - CriteriaSet criteria = new CriteriaSet(); - BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration(); - signingConfiguration.setSigningCredentials(credentials); - signingConfiguration.setSignatureAlgorithms(algorithms); - signingConfiguration.setSignatureReferenceDigestMethods(digests); - signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization); - signingConfiguration.setKeyInfoGeneratorManager(buildSignatureKeyInfoGeneratorManager()); - criteria.add(new SignatureSigningConfigurationCriterion(signingConfiguration)); - try { - SignatureSigningParameters parameters = resolver.resolveSingle(criteria); - Assert.notNull(parameters, "Failed to resolve any signing credential"); - return parameters; - } - catch (Exception ex) { - throw new Saml2Exception(ex); - } - } - - private static NamedKeyInfoGeneratorManager buildSignatureKeyInfoGeneratorManager() { - final NamedKeyInfoGeneratorManager namedManager = new NamedKeyInfoGeneratorManager(); - - namedManager.setUseDefaultManager(true); - final KeyInfoGeneratorManager defaultManager = namedManager.getDefaultManager(); - - // Generator for X509Credentials - final X509KeyInfoGeneratorFactory x509Factory = new X509KeyInfoGeneratorFactory(); - x509Factory.setEmitEntityCertificate(true); - x509Factory.setEmitEntityCertificateChain(true); - - defaultManager.registerFactory(x509Factory); - - return namedManager; - } - - private static List resolveSigningCredentials(RelyingPartyRegistration relyingPartyRegistration) { - List credentials = new ArrayList<>(); - for (Saml2X509Credential x509Credential : relyingPartyRegistration.getSigningX509Credentials()) { - X509Certificate certificate = x509Credential.getCertificate(); - PrivateKey privateKey = x509Credential.getPrivateKey(); - BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey); - credential.setEntityId(relyingPartyRegistration.getEntityId()); - credential.setUsageType(UsageType.SIGNING); - credentials.add(credential); - } - return credentials; - } - - private OpenSamlSigningUtils() { - - } - - static class QueryParametersPartial { - - final RelyingPartyRegistration registration; - - final Map components = new LinkedHashMap<>(); - - QueryParametersPartial(RelyingPartyRegistration registration) { - this.registration = registration; - } - - QueryParametersPartial param(String key, String value) { - this.components.put(key, value); - return this; - } - - Map parameters() { - SignatureSigningParameters parameters = resolveSigningParameters(this.registration); - Credential credential = parameters.getSigningCredential(); - String algorithmUri = parameters.getSignatureAlgorithm(); - this.components.put(Saml2ParameterNames.SIG_ALG, algorithmUri); - UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); - for (Map.Entry component : this.components.entrySet()) { - builder.queryParam(component.getKey(), - UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1)); - } - String queryString = builder.build(true).toString().substring(1); - try { - byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri, - queryString.getBytes(StandardCharsets.UTF_8)); - String b64Signature = Saml2Utils.samlEncode(rawSignature); - this.components.put(Saml2ParameterNames.SIGNATURE, b64Signature); - } - catch (SecurityException ex) { - throw new Saml2Exception(ex); - } - return this.components; - } - - } - -} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2Utils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2Utils.java index 10bcce078b..698b1a4124 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2Utils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2Utils.java @@ -19,6 +19,7 @@ package org.springframework.security.saml2.provider.service.metadata; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Base64; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; @@ -28,7 +29,11 @@ import java.util.zip.InflaterOutputStream; import org.springframework.security.saml2.Saml2Exception; /** - * @since 6.3 + * Utility methods for working with serialized SAML messages. + * + * For internal use only. + * + * @author Josh Cummings */ final class Saml2Utils { @@ -69,4 +74,123 @@ final class Saml2Utils { } } + static EncodingConfigurer withDecoded(String decoded) { + return new EncodingConfigurer(decoded); + } + + static DecodingConfigurer withEncoded(String encoded) { + return new DecodingConfigurer(encoded); + } + + static final class EncodingConfigurer { + + private final String decoded; + + private boolean deflate; + + private EncodingConfigurer(String decoded) { + this.decoded = decoded; + } + + EncodingConfigurer deflate(boolean deflate) { + this.deflate = deflate; + return this; + } + + String encode() { + byte[] bytes = (this.deflate) ? Saml2Utils.samlDeflate(this.decoded) + : this.decoded.getBytes(StandardCharsets.UTF_8); + return Saml2Utils.samlEncode(bytes); + } + + } + + static final class DecodingConfigurer { + + private static final Base64Checker BASE_64_CHECKER = new Base64Checker(); + + private final String encoded; + + private boolean inflate; + + private boolean requireBase64; + + private DecodingConfigurer(String encoded) { + this.encoded = encoded; + } + + DecodingConfigurer inflate(boolean inflate) { + this.inflate = inflate; + return this; + } + + DecodingConfigurer requireBase64(boolean requireBase64) { + this.requireBase64 = requireBase64; + return this; + } + + String decode() { + if (this.requireBase64) { + BASE_64_CHECKER.checkAcceptable(this.encoded); + } + byte[] bytes = Saml2Utils.samlDecode(this.encoded); + return (this.inflate) ? Saml2Utils.samlInflate(bytes) : new String(bytes, StandardCharsets.UTF_8); + } + + static class Base64Checker { + + private static final int[] values = genValueMapping(); + + Base64Checker() { + + } + + private static int[] genValueMapping() { + byte[] alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + .getBytes(StandardCharsets.ISO_8859_1); + + int[] values = new int[256]; + Arrays.fill(values, -1); + for (int i = 0; i < alphabet.length; i++) { + values[alphabet[i] & 0xff] = i; + } + return values; + } + + boolean isAcceptable(String s) { + int goodChars = 0; + int lastGoodCharVal = -1; + + // count number of characters from Base64 alphabet + for (int i = 0; i < s.length(); i++) { + int val = values[0xff & s.charAt(i)]; + if (val != -1) { + lastGoodCharVal = val; + goodChars++; + } + } + + // in cases of an incomplete final chunk, ensure the unused bits are zero + switch (goodChars % 4) { + case 0: + return true; + case 2: + return (lastGoodCharVal & 0b1111) == 0; + case 3: + return (lastGoodCharVal & 0b11) == 0; + default: + return false; + } + } + + void checkAcceptable(String ins) { + if (!isAcceptable(ins)) { + throw new IllegalArgumentException("Failed to decode SAMLResponse"); + } + } + + } + + } + } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSaml4MetadataResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSaml4MetadataResolverTests.java new file mode 100644 index 0000000000..928886eecd --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSaml4MetadataResolverTests.java @@ -0,0 +1,185 @@ +/* + * 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.metadata; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.security.saml2.core.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OpenSaml4MetadataResolver} + */ +public class OpenSaml4MetadataResolverTests { + + @Test + public void resolveWhenRelyingPartyThenMetadataMatches() { + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full() + .assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT) + .build(); + OpenSaml4MetadataResolver OpenSaml4MetadataResolver = new OpenSaml4MetadataResolver(); + String metadata = OpenSaml4MetadataResolver.resolve(relyingPartyRegistration); + assertThat(metadata).contains("") + .contains("") + .contains("MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh") + .contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"") + .contains("Location=\"https://rp.example.org/acs\" index=\"1\"") + .contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\""); + } + + @Test + public void resolveWhenRelyingPartyAndSignMetadataSetThenMetadataMatches() { + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full() + .assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT) + .build(); + OpenSaml4MetadataResolver OpenSaml4MetadataResolver = new OpenSaml4MetadataResolver(); + OpenSaml4MetadataResolver.setSignMetadata(true); + String metadata = OpenSaml4MetadataResolver.resolve(relyingPartyRegistration); + assertThat(metadata).contains("") + .contains("") + .contains("MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh") + .contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"") + .contains("Location=\"https://rp.example.org/acs\" index=\"1\"") + .contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"") + .contains("Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"") + .contains("CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#") + .contains("SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256") + .contains("Reference URI=\"\"") + .contains("Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature") + .contains("Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"") + .contains("DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"") + .contains("DigestValue") + .contains("SignatureValue"); + } + + @Test + public void resolveWhenRelyingPartyNoCredentialsThenMetadataMatches() { + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials() + .assertingPartyDetails((party) -> party + .verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) + .build(); + OpenSaml4MetadataResolver OpenSaml4MetadataResolver = new OpenSaml4MetadataResolver(); + String metadata = OpenSaml4MetadataResolver.resolve(relyingPartyRegistration); + assertThat(metadata).contains("") + .doesNotContain("") + .contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"") + .contains("Location=\"https://rp.example.org/acs\" index=\"1\"") + .contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\""); + } + + @Test + public void resolveWhenRelyingPartyNameIDFormatThenMetadataMatches() { + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full() + .nameIdFormat("format") + .build(); + OpenSaml4MetadataResolver OpenSaml4MetadataResolver = new OpenSaml4MetadataResolver(); + String metadata = OpenSaml4MetadataResolver.resolve(relyingPartyRegistration); + assertThat(metadata).contains("format"); + } + + @Test + public void resolveWhenRelyingPartyNoLogoutThenMetadataMatches() { + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full() + .singleLogoutServiceLocation(null) + .nameIdFormat("format") + .build(); + OpenSaml4MetadataResolver OpenSaml4MetadataResolver = new OpenSaml4MetadataResolver(); + String metadata = OpenSaml4MetadataResolver.resolve(relyingPartyRegistration); + assertThat(metadata).doesNotContain("ResponseLocation"); + } + + @Test + public void resolveWhenEntityDescriptorCustomizerThenUses() { + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full() + .entityId("originalEntityId") + .build(); + OpenSaml4MetadataResolver OpenSaml4MetadataResolver = new OpenSaml4MetadataResolver(); + OpenSaml4MetadataResolver.setEntityDescriptorCustomizer( + (parameters) -> parameters.getEntityDescriptor().setEntityID("overriddenEntityId")); + String metadata = OpenSaml4MetadataResolver.resolve(relyingPartyRegistration); + assertThat(metadata).contains("") + .contains("") + .contains("MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh") + .contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"") + .contains("Location=\"https://rp.example.org/acs\" index=\"1\"") + .contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\""); + } + + @Test + public void resolveIterableWhenRelyingPartiesAndSignMetadataSetThenMetadataMatches() { + RelyingPartyRegistration one = TestRelyingPartyRegistrations.full() + .assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT) + .build(); + RelyingPartyRegistration two = TestRelyingPartyRegistrations.full() + .entityId("two") + .assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT) + .build(); + OpenSaml4MetadataResolver OpenSaml4MetadataResolver = new OpenSaml4MetadataResolver(); + OpenSaml4MetadataResolver.setSignMetadata(true); + String metadata = OpenSaml4MetadataResolver.resolve(List.of(one, two)); + assertThat(metadata).contains("") + .contains("") + .contains("MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh") + .contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"") + .contains("Location=\"https://rp.example.org/acs\" index=\"1\"") + .contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"") + .contains("Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"") + .contains("CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#") + .contains("SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256") + .contains("Reference URI=\"\"") + .contains("Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature") + .contains("Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"") + .contains("DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"") + .contains("DigestValue") + .contains("SignatureValue"); + } + +}