diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java index 960e9e1d71c..4d1291fd187 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java @@ -31,8 +31,8 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; -import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutRequestValidator; -import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutResponseValidator; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml4LogoutRequestValidator; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml4LogoutResponseValidator; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; @@ -398,7 +398,7 @@ public Saml2LogoutConfigurer and() { private Saml2LogoutRequestValidator logoutRequestValidator() { if (this.logoutRequestValidator == null) { - return new OpenSamlLogoutRequestValidator(); + return new OpenSaml4LogoutRequestValidator(); } return this.logoutRequestValidator; } @@ -474,7 +474,7 @@ public Saml2LogoutConfigurer and() { private Saml2LogoutResponseValidator logoutResponseValidator() { if (this.logoutResponseValidator == null) { - return new OpenSamlLogoutResponseValidator(); + return new OpenSaml4LogoutResponseValidator(); } return this.logoutResponseValidator; } 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 bc6fedd828b..884df70a51d 100644 --- a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle +++ b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle @@ -1,5 +1,20 @@ apply plugin: 'io.spring.convention.spring-module' +sourceSets.configureEach { set -> + if (!set.name.containsIgnoreCase("main")) { + return + } + def from = copySpec { + from("$projectDir/src/$set.name/java/org/springframework/security/saml2/internal") + } + + copy { + into "$projectDir/src/$set.name/java/org/springframework/security/saml2/provider/service/authentication/logout" + filter { line -> line.replaceAll(".saml2.internal", ".saml2.provider.service.authentication.logout") } + with from + } +} + dependencies { management platform(project(":spring-security-dependencies")) api project(':spring-security-web') diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/BaseOpenSamlLogoutRequestValidator.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/BaseOpenSamlLogoutRequestValidator.java new file mode 100644 index 00000000000..b96cb947c68 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/BaseOpenSamlLogoutRequestValidator.java @@ -0,0 +1,152 @@ +/* + * 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.authentication.logout; + +import java.util.Collection; +import java.util.function.Consumer; + +import org.opensaml.saml.saml2.core.LogoutRequest; +import org.opensaml.saml.saml2.core.NameID; + +import org.springframework.security.core.Authentication; +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.core.Saml2ErrorCodes; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.VerificationConfigurer; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.VerificationConfigurer.RedirectParameters; +import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; + +class BaseOpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator { + + static { + OpenSamlInitializationService.initialize(); + } + + private final OpenSamlOperations saml; + + BaseOpenSamlLogoutRequestValidator(OpenSamlOperations saml) { + this.saml = saml; + } + + /** + * {@inheritDoc} + */ + @Override + public Saml2LogoutValidatorResult validate(Saml2LogoutRequestValidatorParameters parameters) { + Saml2LogoutRequest request = parameters.getLogoutRequest(); + RelyingPartyRegistration registration = parameters.getRelyingPartyRegistration(); + Authentication authentication = parameters.getAuthentication(); + LogoutRequest logoutRequest = this.saml.deserialize(Saml2Utils.withEncoded(request.getSamlRequest()) + .inflate(request.getBinding() == Saml2MessageBinding.REDIRECT) + .decode()); + return Saml2LogoutValidatorResult.withErrors() + .errors(verifySignature(request, logoutRequest, registration)) + .errors(validateRequest(logoutRequest, registration, authentication)) + .build(); + } + + private Consumer> verifySignature(Saml2LogoutRequest request, LogoutRequest logoutRequest, + RelyingPartyRegistration registration) { + AssertingPartyMetadata details = registration.getAssertingPartyMetadata(); + Collection credentials = details.getVerificationX509Credentials(); + VerificationConfigurer verify = this.saml.withVerificationKeys(credentials).entityId(details.getEntityId()); + return (errors) -> { + if (logoutRequest.isSigned()) { + errors.addAll(verify.verify(logoutRequest)); + } + else { + RedirectParameters params = new RedirectParameters(request.getParameters(), + request.getParametersQuery(), logoutRequest); + errors.addAll(verify.verify(params)); + } + }; + } + + private Consumer> validateRequest(LogoutRequest request, + RelyingPartyRegistration registration, Authentication authentication) { + return (errors) -> { + validateIssuer(request, registration).accept(errors); + validateDestination(request, registration).accept(errors); + validateSubject(request, registration, authentication).accept(errors); + }; + } + + private Consumer> validateIssuer(LogoutRequest request, + RelyingPartyRegistration registration) { + return (errors) -> { + if (request.getIssuer() == null) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to find issuer in LogoutRequest")); + return; + } + String issuer = request.getIssuer().getValue(); + if (!issuer.equals(registration.getAssertingPartyMetadata().getEntityId())) { + errors + .add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer")); + } + }; + } + + private Consumer> validateDestination(LogoutRequest request, + RelyingPartyRegistration registration) { + return (errors) -> { + if (request.getDestination() == null) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION, + "Failed to find destination in LogoutRequest")); + return; + } + String destination = request.getDestination(); + if (!destination.equals(registration.getSingleLogoutServiceLocation())) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION, + "Failed to match destination to configured destination")); + } + }; + } + + private Consumer> validateSubject(LogoutRequest request, + RelyingPartyRegistration registration, Authentication authentication) { + return (errors) -> { + if (authentication == null) { + return; + } + NameID nameId = getNameId(request, registration); + if (nameId == null) { + errors + .add(new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND, "Failed to find subject in LogoutRequest")); + return; + } + + validateNameId(nameId, authentication, errors); + }; + } + + private NameID getNameId(LogoutRequest request, RelyingPartyRegistration registration) { + this.saml.withDecryptionKeys(registration.getDecryptionX509Credentials()).decrypt(request); + return request.getNameID(); + } + + private void validateNameId(NameID nameId, Authentication authentication, Collection errors) { + String name = nameId.getValue(); + if (!name.equals(authentication.getName())) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_REQUEST, + "Failed to match subject in LogoutRequest with currently logged in user")); + } + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/BaseOpenSamlLogoutResponseValidator.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/BaseOpenSamlLogoutResponseValidator.java new file mode 100644 index 00000000000..d464bac0b5c --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/BaseOpenSamlLogoutResponseValidator.java @@ -0,0 +1,152 @@ +/* + * 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.authentication.logout; + +import java.util.Collection; +import java.util.function.Consumer; + +import org.opensaml.saml.saml2.core.LogoutResponse; +import org.opensaml.saml.saml2.core.StatusCode; + +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.core.Saml2ErrorCodes; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.VerificationConfigurer; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.VerificationConfigurer.RedirectParameters; +import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; + +class BaseOpenSamlLogoutResponseValidator implements Saml2LogoutResponseValidator { + + static { + OpenSamlInitializationService.initialize(); + } + + private final OpenSamlOperations saml; + + BaseOpenSamlLogoutResponseValidator(OpenSamlOperations saml) { + this.saml = saml; + } + + @Override + public Saml2LogoutValidatorResult validate(Saml2LogoutResponseValidatorParameters parameters) { + Saml2LogoutResponse response = parameters.getLogoutResponse(); + Saml2LogoutRequest request = parameters.getLogoutRequest(); + RelyingPartyRegistration registration = parameters.getRelyingPartyRegistration(); + LogoutResponse logoutResponse = this.saml.deserialize(Saml2Utils.withEncoded(response.getSamlResponse()) + .inflate(response.getBinding() == Saml2MessageBinding.REDIRECT) + .decode()); + return Saml2LogoutValidatorResult.withErrors() + .errors(verifySignature(response, logoutResponse, registration)) + .errors(validateRequest(logoutResponse, registration)) + .errors(validateLogoutRequest(logoutResponse, request.getId())) + .build(); + } + + private Consumer> verifySignature(Saml2LogoutResponse response, + LogoutResponse logoutResponse, RelyingPartyRegistration registration) { + return (errors) -> { + AssertingPartyMetadata details = registration.getAssertingPartyMetadata(); + Collection credentials = details.getVerificationX509Credentials(); + VerificationConfigurer verify = this.saml.withVerificationKeys(credentials) + .entityId(details.getEntityId()) + .entityId(details.getEntityId()); + if (logoutResponse.isSigned()) { + errors.addAll(verify.verify(logoutResponse)); + } + else { + RedirectParameters params = new RedirectParameters(response.getParameters(), + response.getParametersQuery(), logoutResponse); + errors.addAll(verify.verify(params)); + } + }; + } + + private Consumer> validateRequest(LogoutResponse response, + RelyingPartyRegistration registration) { + return (errors) -> { + validateIssuer(response, registration).accept(errors); + validateDestination(response, registration).accept(errors); + validateStatus(response).accept(errors); + }; + } + + private Consumer> validateIssuer(LogoutResponse response, + RelyingPartyRegistration registration) { + return (errors) -> { + if (response.getIssuer() == null) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to find issuer in LogoutResponse")); + return; + } + String issuer = response.getIssuer().getValue(); + if (!issuer.equals(registration.getAssertingPartyMetadata().getEntityId())) { + errors + .add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer")); + } + }; + } + + private Consumer> validateDestination(LogoutResponse response, + RelyingPartyRegistration registration) { + return (errors) -> { + if (response.getDestination() == null) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION, + "Failed to find destination in LogoutResponse")); + return; + } + String destination = response.getDestination(); + if (!destination.equals(registration.getSingleLogoutServiceResponseLocation())) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION, + "Failed to match destination to configured destination")); + } + }; + } + + private Consumer> validateStatus(LogoutResponse response) { + return (errors) -> { + if (response.getStatus() == null) { + return; + } + if (response.getStatus().getStatusCode() == null) { + return; + } + if (StatusCode.SUCCESS.equals(response.getStatus().getStatusCode().getValue())) { + return; + } + if (StatusCode.PARTIAL_LOGOUT.equals(response.getStatus().getStatusCode().getValue())) { + return; + } + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE, "Response indicated logout failed")); + }; + } + + private Consumer> validateLogoutRequest(LogoutResponse response, String id) { + return (errors) -> { + if (response.getInResponseTo() == null) { + return; + } + if (response.getInResponseTo().equals(id)) { + return; + } + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE, + "LogoutResponse InResponseTo doesn't match ID of associated LogoutRequest")); + }; + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/LogoutRequestEncryptedIdUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/LogoutRequestEncryptedIdUtils.java deleted file mode 100644 index 5ff94a701b5..00000000000 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/LogoutRequestEncryptedIdUtils.java +++ /dev/null @@ -1,81 +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.authentication.logout; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; - -import org.opensaml.saml.common.SAMLObject; -import org.opensaml.saml.saml2.core.EncryptedID; -import org.opensaml.saml.saml2.encryption.Decrypter; -import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver; -import org.opensaml.security.credential.Credential; -import org.opensaml.security.credential.CredentialSupport; -import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver; -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.keyinfo.KeyInfoCredentialResolver; -import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver; - -import org.springframework.security.saml2.Saml2Exception; -import org.springframework.security.saml2.core.Saml2X509Credential; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; - -/** - * Utility methods for decrypting EncryptedID from SAML logout request with OpenSAML - * - * For internal use only. - * - * this is mainly a adapted copy of OpenSamlDecryptionUtils - * - * @author Robert Stoiber - */ -final class LogoutRequestEncryptedIdUtils { - - private static final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver( - Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(), - new SimpleRetrievalMethodEncryptedKeyResolver())); - - static SAMLObject decryptEncryptedId(EncryptedID encryptedId, RelyingPartyRegistration registration) { - Decrypter decrypter = decrypter(registration); - try { - return decrypter.decrypt(encryptedId); - - } - catch (Exception ex) { - throw new Saml2Exception(ex); - } - } - - private static Decrypter decrypter(RelyingPartyRegistration registration) { - Collection credentials = new ArrayList<>(); - for (Saml2X509Credential key : registration.getDecryptionX509Credentials()) { - 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; - } - - private LogoutRequestEncryptedIdUtils() { - } - -} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSaml4LogoutRequestValidator.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSaml4LogoutRequestValidator.java new file mode 100644 index 00000000000..24b9b8b0e00 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSaml4LogoutRequestValidator.java @@ -0,0 +1,30 @@ +/* + * 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.authentication.logout; + +public final class OpenSaml4LogoutRequestValidator implements Saml2LogoutRequestValidator { + + @SuppressWarnings("deprecation") + private final Saml2LogoutRequestValidator delegate = new BaseOpenSamlLogoutRequestValidator( + new OpenSaml4Template()); + + @Override + public Saml2LogoutValidatorResult validate(Saml2LogoutRequestValidatorParameters parameters) { + return this.delegate.validate(parameters); + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSaml4LogoutResponseValidator.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSaml4LogoutResponseValidator.java new file mode 100644 index 00000000000..4d901be72c8 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSaml4LogoutResponseValidator.java @@ -0,0 +1,30 @@ +/* + * 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.authentication.logout; + +public final class OpenSaml4LogoutResponseValidator implements Saml2LogoutResponseValidator { + + @SuppressWarnings("deprecation") + private final Saml2LogoutResponseValidator delegate = new BaseOpenSamlLogoutResponseValidator( + new OpenSaml4Template()); + + @Override + public Saml2LogoutValidatorResult validate(Saml2LogoutResponseValidatorParameters parameters) { + return this.delegate.validate(parameters); + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSaml4Template.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSaml4Template.java new file mode 100644 index 00000000000..5344d080dc5 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/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.authentication.logout; + +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/authentication/logout/OpenSamlLogoutRequestValidator.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java index 061c97d00aa..ac4229be667 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,29 +16,20 @@ package org.springframework.security.saml2.provider.service.authentication.logout; -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.function.Consumer; -import net.shibboleth.utilities.java.support.xml.ParserPool; -import org.opensaml.core.config.ConfigurationService; -import org.opensaml.core.xml.config.XMLObjectProviderRegistry; -import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; -import org.opensaml.saml.common.SAMLObject; -import org.opensaml.saml.saml2.core.EncryptedID; import org.opensaml.saml.saml2.core.LogoutRequest; import org.opensaml.saml.saml2.core.NameID; -import org.opensaml.saml.saml2.core.impl.LogoutRequestUnmarshaller; -import org.w3c.dom.Document; -import org.w3c.dom.Element; import org.springframework.security.core.Authentication; -import org.springframework.security.saml2.Saml2Exception; import org.springframework.security.saml2.core.OpenSamlInitializationService; import org.springframework.security.saml2.core.Saml2Error; import org.springframework.security.saml2.core.Saml2ErrorCodes; -import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlVerificationUtils.VerifierPartial; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.VerificationConfigurer; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.VerificationConfigurer.RedirectParameters; +import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; @@ -48,25 +39,22 @@ * * @author Josh Cummings * @since 5.6 + * @deprecated Please use the version-specific {@link Saml2LogoutRequestValidator} such as + * {@code OpenSaml4LogoutRequestValidator} */ +@Deprecated public final class OpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator { static { OpenSamlInitializationService.initialize(); } - private final ParserPool parserPool; - - private final LogoutRequestUnmarshaller unmarshaller; + private final OpenSamlOperations saml = new OpenSaml4Template(); /** * Constructs a {@link OpenSamlLogoutRequestValidator} */ public OpenSamlLogoutRequestValidator() { - XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class); - this.parserPool = registry.getParserPool(); - this.unmarshaller = (LogoutRequestUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory() - .getUnmarshaller(LogoutRequest.DEFAULT_ELEMENT_NAME); } /** @@ -77,42 +65,28 @@ public Saml2LogoutValidatorResult validate(Saml2LogoutRequestValidatorParameters Saml2LogoutRequest request = parameters.getLogoutRequest(); RelyingPartyRegistration registration = parameters.getRelyingPartyRegistration(); Authentication authentication = parameters.getAuthentication(); - byte[] b = Saml2Utils.samlDecode(request.getSamlRequest()); - LogoutRequest logoutRequest = parse(inflateIfRequired(request, b)); + LogoutRequest logoutRequest = this.saml.deserialize(Saml2Utils.withEncoded(request.getSamlRequest()) + .inflate(request.getBinding() == Saml2MessageBinding.REDIRECT) + .decode()); return Saml2LogoutValidatorResult.withErrors() .errors(verifySignature(request, logoutRequest, registration)) .errors(validateRequest(logoutRequest, registration, authentication)) .build(); } - private String inflateIfRequired(Saml2LogoutRequest request, byte[] b) { - if (request.getBinding() == Saml2MessageBinding.REDIRECT) { - return Saml2Utils.samlInflate(b); - } - return new String(b, StandardCharsets.UTF_8); - } - - private LogoutRequest parse(String request) throws Saml2Exception { - try { - Document document = this.parserPool - .parse(new ByteArrayInputStream(request.getBytes(StandardCharsets.UTF_8))); - Element element = document.getDocumentElement(); - return (LogoutRequest) this.unmarshaller.unmarshall(element); - } - catch (Exception ex) { - throw new Saml2Exception("Failed to deserialize LogoutRequest", ex); - } - } - private Consumer> verifySignature(Saml2LogoutRequest request, LogoutRequest logoutRequest, RelyingPartyRegistration registration) { + AssertingPartyMetadata details = registration.getAssertingPartyMetadata(); + Collection credentials = details.getVerificationX509Credentials(); + VerificationConfigurer verify = this.saml.withVerificationKeys(credentials).entityId(details.getEntityId()); return (errors) -> { - VerifierPartial partial = OpenSamlVerificationUtils.verifySignature(logoutRequest, registration); if (logoutRequest.isSigned()) { - errors.addAll(partial.post(logoutRequest.getSignature())); + errors.addAll(verify.verify(logoutRequest)); } else { - errors.addAll(partial.redirect(request)); + RedirectParameters params = new RedirectParameters(request.getParameters(), + request.getParametersQuery(), logoutRequest); + errors.addAll(verify.verify(params)); } }; } @@ -175,15 +149,8 @@ private Consumer> validateSubject(LogoutRequest request, } private NameID getNameId(LogoutRequest request, RelyingPartyRegistration registration) { - NameID nameId = request.getNameID(); - if (nameId != null) { - return nameId; - } - EncryptedID encryptedId = request.getEncryptedID(); - if (encryptedId == null) { - return null; - } - return decryptNameId(encryptedId, registration); + this.saml.withDecryptionKeys(registration.getDecryptionX509Credentials()).decrypt(request); + return request.getNameID(); } private void validateNameId(NameID nameId, Authentication authentication, Collection errors) { @@ -194,12 +161,4 @@ private void validateNameId(NameID nameId, Authentication authentication, Collec } } - private NameID decryptNameId(EncryptedID encryptedId, RelyingPartyRegistration registration) { - final SAMLObject decryptedId = LogoutRequestEncryptedIdUtils.decryptEncryptedId(encryptedId, registration); - if (decryptedId instanceof NameID) { - return ((NameID) decryptedId); - } - return null; - } - } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidator.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidator.java index 64041583b2f..c4c00d8d7b2 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidator.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidator.java @@ -16,26 +16,23 @@ package org.springframework.security.saml2.provider.service.authentication.logout; -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.function.Consumer; -import net.shibboleth.utilities.java.support.xml.ParserPool; import org.opensaml.core.config.ConfigurationService; import org.opensaml.core.xml.config.XMLObjectProviderRegistry; import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; import org.opensaml.saml.saml2.core.LogoutResponse; import org.opensaml.saml.saml2.core.StatusCode; import org.opensaml.saml.saml2.core.impl.LogoutResponseUnmarshaller; -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.Saml2Error; import org.springframework.security.saml2.core.Saml2ErrorCodes; -import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlVerificationUtils.VerifierPartial; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.VerificationConfigurer; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.VerificationConfigurer.RedirectParameters; +import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; @@ -45,23 +42,27 @@ * * @author Josh Cummings * @since 5.6 + * @deprecated Please use the version-specific {@link Saml2LogoutResponseValidator} + * instead such as {@code OpenSaml4LogoutResponseValidator} */ +@Deprecated public class OpenSamlLogoutResponseValidator implements Saml2LogoutResponseValidator { static { OpenSamlInitializationService.initialize(); } - private final ParserPool parserPool; + private final XMLObjectProviderRegistry registry; private final LogoutResponseUnmarshaller unmarshaller; + private final OpenSamlOperations saml = new OpenSaml4Template(); + /** * Constructs a {@link OpenSamlLogoutRequestValidator} */ public OpenSamlLogoutResponseValidator() { - XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class); - this.parserPool = registry.getParserPool(); + this.registry = ConfigurationService.get(XMLObjectProviderRegistry.class); this.unmarshaller = (LogoutResponseUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory() .getUnmarshaller(LogoutResponse.DEFAULT_ELEMENT_NAME); } @@ -74,8 +75,9 @@ public Saml2LogoutValidatorResult validate(Saml2LogoutResponseValidatorParameter Saml2LogoutResponse response = parameters.getLogoutResponse(); Saml2LogoutRequest request = parameters.getLogoutRequest(); RelyingPartyRegistration registration = parameters.getRelyingPartyRegistration(); - byte[] b = Saml2Utils.samlDecode(response.getSamlResponse()); - LogoutResponse logoutResponse = parse(inflateIfRequired(response, b)); + LogoutResponse logoutResponse = this.saml.deserialize(Saml2Utils.withEncoded(response.getSamlResponse()) + .inflate(response.getBinding() == Saml2MessageBinding.REDIRECT) + .decode()); return Saml2LogoutValidatorResult.withErrors() .errors(verifySignature(response, logoutResponse, registration)) .errors(validateRequest(logoutResponse, registration)) @@ -83,34 +85,19 @@ public Saml2LogoutValidatorResult validate(Saml2LogoutResponseValidatorParameter .build(); } - private String inflateIfRequired(Saml2LogoutResponse response, byte[] b) { - if (response.getBinding() == Saml2MessageBinding.REDIRECT) { - return Saml2Utils.samlInflate(b); - } - return new String(b, StandardCharsets.UTF_8); - } - - private LogoutResponse parse(String response) throws Saml2Exception { - try { - Document document = this.parserPool - .parse(new ByteArrayInputStream(response.getBytes(StandardCharsets.UTF_8))); - Element element = document.getDocumentElement(); - return (LogoutResponse) this.unmarshaller.unmarshall(element); - } - catch (Exception ex) { - throw new Saml2Exception("Failed to deserialize LogoutResponse", ex); - } - } - private Consumer> verifySignature(Saml2LogoutResponse response, LogoutResponse logoutResponse, RelyingPartyRegistration registration) { return (errors) -> { - VerifierPartial partial = OpenSamlVerificationUtils.verifySignature(logoutResponse, registration); + AssertingPartyMetadata details = registration.getAssertingPartyMetadata(); + Collection credentials = details.getVerificationX509Credentials(); + VerificationConfigurer verify = this.saml.withVerificationKeys(credentials).entityId(details.getEntityId()); if (logoutResponse.isSigned()) { - errors.addAll(partial.post(logoutResponse.getSignature())); + errors.addAll(verify.verify(logoutResponse)); } else { - errors.addAll(partial.redirect(response)); + RedirectParameters params = new RedirectParameters(response.getParameters(), + response.getParametersQuery(), logoutResponse); + errors.addAll(verify.verify(params)); } }; } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlOperations.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlOperations.java index 9d3ed17d979..972498d04a8 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlOperations.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlOperations.java @@ -79,7 +79,7 @@ interface VerificationConfigurer { Collection verify(SignableXMLObject signable); - Collection verify(RedirectParameters parameters); + Collection verify(VerificationConfigurer.RedirectParameters parameters); final class RedirectParameters { @@ -98,7 +98,7 @@ final class RedirectParameters { this.issuer = request.getIssuer(); this.algorithm = parameters.get(Saml2ParameterNames.SIG_ALG); if (parameters.get(Saml2ParameterNames.SIGNATURE) != null) { - this.signature = org.springframework.security.saml2.internal.Saml2Utils.samlDecode(parameters.get(Saml2ParameterNames.SIGNATURE)); + this.signature = Saml2Utils.samlDecode(parameters.get(Saml2ParameterNames.SIGNATURE)); } else { this.signature = null; @@ -117,7 +117,7 @@ final class RedirectParameters { this.issuer = response.getIssuer(); this.algorithm = parameters.get(Saml2ParameterNames.SIG_ALG); if (parameters.get(Saml2ParameterNames.SIGNATURE) != null) { - this.signature = org.springframework.security.saml2.internal.Saml2Utils.samlDecode(parameters.get(Saml2ParameterNames.SIGNATURE)); + this.signature = Saml2Utils.samlDecode(parameters.get(Saml2ParameterNames.SIGNATURE)); } else { this.signature = null; diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlVerificationUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlVerificationUtils.java deleted file mode 100644 index e0da9859310..00000000000 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlVerificationUtils.java +++ /dev/null @@ -1,256 +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.authentication.logout; - -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -import net.shibboleth.utilities.java.support.resolver.CriteriaSet; -import org.opensaml.core.criterion.EntityIdCriterion; -import org.opensaml.saml.common.xml.SAMLConstants; -import org.opensaml.saml.criterion.ProtocolCriterion; -import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion; -import org.opensaml.saml.saml2.core.Issuer; -import org.opensaml.saml.saml2.core.RequestAbstractType; -import org.opensaml.saml.saml2.core.StatusResponseType; -import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; -import org.opensaml.security.credential.Credential; -import org.opensaml.security.credential.CredentialResolver; -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.config.impl.DefaultSecurityConfigurationBootstrap; -import org.opensaml.xmlsec.signature.Signature; -import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; -import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine; - -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.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.web.util.UriComponentsBuilder; - -/** - * Utility methods for verifying SAML component signatures with OpenSAML - * - * For internal use only. - * - * @author Josh Cummings - */ - -final class OpenSamlVerificationUtils { - - static VerifierPartial verifySignature(StatusResponseType object, RelyingPartyRegistration registration) { - return new VerifierPartial(object, registration); - } - - static VerifierPartial verifySignature(RequestAbstractType object, RelyingPartyRegistration registration) { - return new VerifierPartial(object, registration); - } - - private OpenSamlVerificationUtils() { - - } - - static class VerifierPartial { - - private final String id; - - private final CriteriaSet criteria; - - private final SignatureTrustEngine trustEngine; - - VerifierPartial(StatusResponseType object, RelyingPartyRegistration registration) { - this.id = object.getID(); - this.criteria = verificationCriteria(object.getIssuer()); - this.trustEngine = trustEngine(registration); - } - - VerifierPartial(RequestAbstractType object, RelyingPartyRegistration registration) { - this.id = object.getID(); - this.criteria = verificationCriteria(object.getIssuer()); - this.trustEngine = trustEngine(registration); - } - - Collection redirect(Saml2LogoutRequest request) { - return redirect(new RedirectSignature(request)); - } - - Collection redirect(Saml2LogoutResponse response) { - return redirect(new RedirectSignature(response)); - } - - Collection redirect(RedirectSignature signature) { - if (signature.getAlgorithm() == null) { - return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, - "Missing signature algorithm for object [" + this.id + "]")); - } - if (!signature.hasSignature()) { - return Collections.singletonList(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, - "Missing signature for object [" + this.id + "]")); - } - Collection errors = new ArrayList<>(); - String algorithmUri = signature.getAlgorithm(); - try { - if (!this.trustEngine.validate(signature.getSignature(), signature.getContent(), algorithmUri, - this.criteria, null)) { - errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, - "Invalid signature for object [" + this.id + "]")); - } - } - catch (Exception ex) { - errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, - "Invalid signature for object [" + this.id + "]: ")); - } - return errors; - } - - Collection post(Signature signature) { - 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 [" + this.id + "]: ")); - } - - try { - if (!this.trustEngine.validate(signature, this.criteria)) { - errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, - "Invalid signature for object [" + this.id + "]")); - } - } - catch (Exception ex) { - errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, - "Invalid signature for object [" + this.id + "]: ")); - } - - return errors; - } - - private CriteriaSet verificationCriteria(Issuer issuer) { - CriteriaSet criteria = new CriteriaSet(); - criteria.add(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer.getValue()))); - criteria.add(new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS))); - criteria.add(new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING))); - return criteria; - } - - private SignatureTrustEngine trustEngine(RelyingPartyRegistration registration) { - Set credentials = new HashSet<>(); - Collection keys = registration.getAssertingPartyMetadata() - .getVerificationX509Credentials(); - for (Saml2X509Credential key : keys) { - BasicX509Credential cred = new BasicX509Credential(key.getCertificate()); - cred.setUsageType(UsageType.SIGNING); - cred.setEntityId(registration.getAssertingPartyMetadata().getEntityId()); - credentials.add(cred); - } - CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials); - return new ExplicitKeySignatureTrustEngine(credentialsResolver, - DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver()); - } - - private static class RedirectSignature { - - private final String algorithm; - - private final byte[] signature; - - private final byte[] content; - - RedirectSignature(Saml2LogoutRequest request) { - this.algorithm = request.getParameter(Saml2ParameterNames.SIG_ALG); - if (request.getParameter(Saml2ParameterNames.SIGNATURE) != null) { - this.signature = Saml2Utils.samlDecode(request.getParameter(Saml2ParameterNames.SIGNATURE)); - } - else { - this.signature = null; - } - Map queryParams = UriComponentsBuilder.newInstance() - .query(request.getParametersQuery()) - .build(true) - .getQueryParams() - .toSingleValueMap(); - this.content = getContent(Saml2ParameterNames.SAML_REQUEST, request.getRelayState(), queryParams); - } - - RedirectSignature(Saml2LogoutResponse response) { - this.algorithm = response.getParameter(Saml2ParameterNames.SIG_ALG); - if (response.getParameter(Saml2ParameterNames.SIGNATURE) != null) { - this.signature = Saml2Utils.samlDecode(response.getParameter(Saml2ParameterNames.SIGNATURE)); - } - else { - this.signature = null; - } - Map queryParams = UriComponentsBuilder.newInstance() - .query(response.getParametersQuery()) - .build(true) - .getQueryParams() - .toSingleValueMap(); - this.content = getContent(Saml2ParameterNames.SAML_RESPONSE, response.getRelayState(), 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); - } - } - - byte[] getContent() { - return this.content; - } - - String getAlgorithm() { - return this.algorithm; - } - - byte[] getSignature() { - return this.signature; - } - - boolean hasSignature() { - return this.signature != null; - } - - } - - } - -} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2Utils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2Utils.java index 3f1c9e0026d..01c0ff5ec00 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2Utils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2Utils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ 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; @@ -73,4 +74,123 @@ static String samlInflate(byte[] b) { } } + 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/authentication/logout/OpenSaml4LogoutRequestValidatorTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSaml4LogoutRequestValidatorTests.java new file mode 100644 index 00000000000..afe00cd539e --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSaml4LogoutRequestValidatorTests.java @@ -0,0 +1,223 @@ +/* + * 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.authentication.logout; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.saml.saml2.core.LogoutRequest; + +import org.springframework.security.core.Authentication; +import org.springframework.security.saml2.core.Saml2ErrorCodes; +import org.springframework.security.saml2.core.Saml2ParameterNames; +import org.springframework.security.saml2.core.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.SignatureConfigurer; +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 OpenSaml4LogoutRequestValidator} + * + * @author Josh Cummings + */ +public class OpenSaml4LogoutRequestValidatorTests { + + private final OpenSamlOperations saml = new OpenSaml4Template(); + + private final OpenSaml4LogoutRequestValidator validator = new OpenSaml4LogoutRequestValidator(); + + @Test + public void handleWhenPostBindingThenValidates() { + RelyingPartyRegistration registration = registration().build(); + LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration); + sign(logoutRequest, registration); + Saml2LogoutRequest request = post(logoutRequest, registration); + Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request, + registration, authentication(registration)); + Saml2LogoutValidatorResult result = this.validator.validate(parameters); + assertThat(result.hasErrors()).isFalse(); + } + + @Test + public void handleWhenNameIdIsEncryptedIdPostThenValidates() { + + RelyingPartyRegistration registration = decrypting(encrypting(registration())).build(); + LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequestNameIdInEncryptedId(registration); + sign(logoutRequest, registration); + Saml2LogoutRequest request = post(logoutRequest, registration); + Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request, + registration, authentication(registration)); + Saml2LogoutValidatorResult result = this.validator.validate(parameters); + assertThat(result.hasErrors()).withFailMessage(() -> result.getErrors().toString()).isFalse(); + + } + + @Test + public void handleWhenRedirectBindingThenValidatesSignatureParameter() { + RelyingPartyRegistration registration = registration() + .assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.REDIRECT)) + .build(); + LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration); + Saml2LogoutRequest request = redirect(logoutRequest, registration, + this.saml.withSigningKeys(registration.getSigningX509Credentials())); + Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request, + registration, authentication(registration)); + Saml2LogoutValidatorResult result = this.validator.validate(parameters); + assertThat(result.hasErrors()).isFalse(); + } + + @Test + public void handleWhenInvalidIssuerThenInvalidSignatureError() { + RelyingPartyRegistration registration = registration().build(); + LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration); + logoutRequest.getIssuer().setValue("wrong"); + sign(logoutRequest, registration); + Saml2LogoutRequest request = post(logoutRequest, registration); + Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request, + registration, authentication(registration)); + Saml2LogoutValidatorResult result = this.validator.validate(parameters); + assertThat(result.hasErrors()).isTrue(); + assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_SIGNATURE); + } + + @Test + public void handleWhenMismatchedUserThenInvalidRequestError() { + RelyingPartyRegistration registration = registration().build(); + LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration); + logoutRequest.getNameID().setValue("wrong"); + sign(logoutRequest, registration); + Saml2LogoutRequest request = post(logoutRequest, registration); + Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request, + registration, authentication(registration)); + Saml2LogoutValidatorResult result = this.validator.validate(parameters); + assertThat(result.hasErrors()).isTrue(); + assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_REQUEST); + } + + @Test + public void handleWhenMissingUserThenSubjectNotFoundError() { + RelyingPartyRegistration registration = registration().build(); + LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration); + logoutRequest.setNameID(null); + sign(logoutRequest, registration); + Saml2LogoutRequest request = post(logoutRequest, registration); + Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request, + registration, authentication(registration)); + Saml2LogoutValidatorResult result = this.validator.validate(parameters); + assertThat(result.hasErrors()).isTrue(); + assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.SUBJECT_NOT_FOUND); + } + + @Test + public void handleWhenMismatchedDestinationThenInvalidDestinationError() { + RelyingPartyRegistration registration = registration().build(); + LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration); + logoutRequest.setDestination("wrong"); + sign(logoutRequest, registration); + Saml2LogoutRequest request = post(logoutRequest, registration); + Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request, + registration, authentication(registration)); + Saml2LogoutValidatorResult result = this.validator.validate(parameters); + assertThat(result.hasErrors()).isTrue(); + assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_DESTINATION); + } + + // gh-10923 + @Test + public void handleWhenLogoutResponseHasLineBreaksThenHandles() { + RelyingPartyRegistration registration = registration().build(); + LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration); + sign(logoutRequest, registration); + String encoded = new StringBuffer( + Saml2Utils.samlEncode(serialize(logoutRequest).getBytes(StandardCharsets.UTF_8))) + .insert(10, "\r\n") + .toString(); + Saml2LogoutRequest request = Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .samlRequest(encoded) + .build(); + Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request, + registration, authentication(registration)); + Saml2LogoutValidatorResult result = this.validator.validate(parameters); + assertThat(result.hasErrors()).isFalse(); + } + + private RelyingPartyRegistration.Builder registration() { + return signing(verifying(TestRelyingPartyRegistrations.noCredentials())) + .assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)); + } + + private RelyingPartyRegistration.Builder decrypting(RelyingPartyRegistration.Builder builder) { + return builder + .decryptionX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyDecryptingCredential())); + } + + private RelyingPartyRegistration.Builder encrypting(RelyingPartyRegistration.Builder builder) { + return builder.assertingPartyDetails((party) -> party + .encryptionX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartyEncryptingCredential()))); + } + + private RelyingPartyRegistration.Builder verifying(RelyingPartyRegistration.Builder builder) { + return builder.assertingPartyDetails((party) -> party + .verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))); + } + + private RelyingPartyRegistration.Builder signing(RelyingPartyRegistration.Builder builder) { + return builder.signingX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartySigningCredential())); + } + + private Authentication authentication(RelyingPartyRegistration registration) { + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", new HashMap<>()); + principal.setRelyingPartyRegistrationId(registration.getRegistrationId()); + return new Saml2Authentication(principal, "response", new ArrayList<>()); + } + + private Saml2LogoutRequest post(LogoutRequest logoutRequest, RelyingPartyRegistration registration) { + return Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .samlRequest(Saml2Utils.samlEncode(serialize(logoutRequest).getBytes(StandardCharsets.UTF_8))) + .build(); + } + + private Saml2LogoutRequest redirect(LogoutRequest logoutRequest, RelyingPartyRegistration registration, + SignatureConfigurer configurer) { + String serialized = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(serialize(logoutRequest))); + Map parameters = configurer.sign(Map.of(Saml2ParameterNames.SAML_REQUEST, serialized)); + return Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .samlRequest(serialized) + .parameters((params) -> params.putAll(parameters)) + .build(); + } + + private void sign(LogoutRequest logoutRequest, RelyingPartyRegistration registration) { + TestOpenSamlObjects.signed(logoutRequest, registration.getSigningX509Credentials().iterator().next(), + registration.getAssertingPartyDetails().getEntityId()); + } + + private String serialize(XMLObject object) { + return this.saml.serialize(object).serialize(); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSaml4LogoutResponseValidatorTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSaml4LogoutResponseValidatorTests.java new file mode 100644 index 00000000000..bf946aaf029 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSaml4LogoutResponseValidatorTests.java @@ -0,0 +1,190 @@ +/* + * Copyright 2002-2021 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.authentication.logout; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.saml.saml2.core.LogoutResponse; +import org.opensaml.saml.saml2.core.StatusCode; + +import org.springframework.security.saml2.core.Saml2ErrorCodes; +import org.springframework.security.saml2.core.Saml2ParameterNames; +import org.springframework.security.saml2.core.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.SignatureConfigurer; +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 OpenSaml4LogoutResponseValidator} + * + * @author Josh Cummings + */ +public class OpenSaml4LogoutResponseValidatorTests { + + private final OpenSamlOperations saml = new OpenSaml4Template(); + + private final OpenSaml4LogoutResponseValidator manager = new OpenSaml4LogoutResponseValidator(); + + @Test + public void handleWhenAuthenticatedThenHandles() { + RelyingPartyRegistration registration = signing(verifying(registration())).build(); + Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .id("id") + .build(); + LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration); + sign(logoutResponse, registration); + Saml2LogoutResponse response = post(logoutResponse, registration); + Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response, + logoutRequest, registration); + this.manager.validate(parameters); + } + + @Test + public void handleWhenRedirectBindingThenValidatesSignatureParameter() { + RelyingPartyRegistration registration = signing(verifying(registration())) + .assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.REDIRECT)) + .build(); + Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .id("id") + .build(); + LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration); + Saml2LogoutResponse response = redirect(logoutResponse, registration, + this.saml.withSigningKeys(registration.getSigningX509Credentials())); + Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response, + logoutRequest, registration); + this.manager.validate(parameters); + } + + @Test + public void handleWhenInvalidIssuerThenInvalidSignatureError() { + RelyingPartyRegistration registration = registration().build(); + Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .id("id") + .build(); + LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration); + logoutResponse.getIssuer().setValue("wrong"); + sign(logoutResponse, registration); + Saml2LogoutResponse response = post(logoutResponse, registration); + Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response, + logoutRequest, registration); + Saml2LogoutValidatorResult result = this.manager.validate(parameters); + assertThat(result.hasErrors()).isTrue(); + assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_SIGNATURE); + } + + @Test + public void handleWhenMismatchedDestinationThenInvalidDestinationError() { + RelyingPartyRegistration registration = registration().build(); + Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .id("id") + .build(); + LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration); + logoutResponse.setDestination("wrong"); + sign(logoutResponse, registration); + Saml2LogoutResponse response = post(logoutResponse, registration); + Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response, + logoutRequest, registration); + Saml2LogoutValidatorResult result = this.manager.validate(parameters); + assertThat(result.hasErrors()).isTrue(); + assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_DESTINATION); + } + + @Test + public void handleWhenStatusNotSuccessThenInvalidResponseError() { + RelyingPartyRegistration registration = registration().build(); + Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .id("id") + .build(); + LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration); + logoutResponse.getStatus().getStatusCode().setValue(StatusCode.UNKNOWN_PRINCIPAL); + sign(logoutResponse, registration); + Saml2LogoutResponse response = post(logoutResponse, registration); + Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response, + logoutRequest, registration); + Saml2LogoutValidatorResult result = this.manager.validate(parameters); + assertThat(result.hasErrors()).isTrue(); + assertThat(result.getErrors().iterator().next().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_RESPONSE); + } + + // gh-10923 + @Test + public void handleWhenLogoutResponseHasLineBreaksThenHandles() { + RelyingPartyRegistration registration = signing(verifying(registration())).build(); + Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .id("id") + .build(); + LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration); + sign(logoutResponse, registration); + String encoded = new StringBuilder( + Saml2Utils.samlEncode(serialize(logoutResponse).getBytes(StandardCharsets.UTF_8))) + .insert(10, "\r\n") + .toString(); + Saml2LogoutResponse response = Saml2LogoutResponse.withRelyingPartyRegistration(registration) + .samlResponse(encoded) + .build(); + Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response, + logoutRequest, registration); + this.manager.validate(parameters); + } + + private RelyingPartyRegistration.Builder registration() { + return signing(verifying(TestRelyingPartyRegistrations.noCredentials())) + .assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)); + } + + private RelyingPartyRegistration.Builder verifying(RelyingPartyRegistration.Builder builder) { + return builder.assertingPartyDetails((party) -> party + .verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))); + } + + private RelyingPartyRegistration.Builder signing(RelyingPartyRegistration.Builder builder) { + return builder.signingX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartySigningCredential())); + } + + private Saml2LogoutResponse post(LogoutResponse logoutResponse, RelyingPartyRegistration registration) { + return Saml2LogoutResponse.withRelyingPartyRegistration(registration) + .samlResponse(Saml2Utils.samlEncode(serialize(logoutResponse).getBytes(StandardCharsets.UTF_8))) + .build(); + } + + private Saml2LogoutResponse redirect(LogoutResponse logoutResponse, RelyingPartyRegistration registration, + SignatureConfigurer configurer) { + String serialized = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(serialize(logoutResponse))); + Map parameters = configurer.sign(Map.of(Saml2ParameterNames.SAML_RESPONSE, serialized)); + return Saml2LogoutResponse.withRelyingPartyRegistration(registration) + .samlResponse(serialized) + .parameters((params) -> params.putAll(parameters)) + .build(); + } + + private void sign(LogoutResponse logoutResponse, RelyingPartyRegistration registration) { + TestOpenSamlObjects.signed(logoutResponse, registration.getSigningX509Credentials().iterator().next(), + registration.getAssertingPartyDetails().getEntityId()); + } + + private String serialize(XMLObject object) { + return this.saml.serialize(object).serialize(); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java index 875e9a8bb93..fbf0be7ad2a 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java @@ -32,7 +32,7 @@ import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects; -import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlSigningUtils.QueryParametersPartial; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.SignatureConfigurer; 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; @@ -46,6 +46,8 @@ */ public class OpenSamlLogoutRequestValidatorTests { + private final OpenSamlOperations saml = new OpenSaml4Template(); + private final OpenSamlLogoutRequestValidator manager = new OpenSamlLogoutRequestValidator(); @Test @@ -80,7 +82,8 @@ public void handleWhenRedirectBindingThenValidatesSignatureParameter() { .assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.REDIRECT)) .build(); LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration); - Saml2LogoutRequest request = redirect(logoutRequest, registration, OpenSamlSigningUtils.sign(registration)); + Saml2LogoutRequest request = redirect(logoutRequest, registration, + this.saml.withSigningKeys(registration.getSigningX509Credentials())); Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request, registration, authentication(registration)); Saml2LogoutValidatorResult result = this.manager.validate(parameters); @@ -199,9 +202,9 @@ private Saml2LogoutRequest post(LogoutRequest logoutRequest, RelyingPartyRegistr } private Saml2LogoutRequest redirect(LogoutRequest logoutRequest, RelyingPartyRegistration registration, - QueryParametersPartial partial) { + SignatureConfigurer configurer) { String serialized = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(serialize(logoutRequest))); - Map parameters = partial.param(Saml2ParameterNames.SAML_REQUEST, serialized).parameters(); + Map parameters = configurer.sign(Map.of(Saml2ParameterNames.SAML_REQUEST, serialized)); return Saml2LogoutRequest.withRelyingPartyRegistration(registration) .samlRequest(serialized) .parameters((params) -> params.putAll(parameters)) @@ -214,7 +217,7 @@ private void sign(LogoutRequest logoutRequest, RelyingPartyRegistration registra } private String serialize(XMLObject object) { - return OpenSamlSigningUtils.serialize(object); + return this.saml.serialize(object).serialize(); } } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidatorTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidatorTests.java index ea67920286b..70d18971f02 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidatorTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ import org.springframework.security.saml2.core.Saml2ParameterNames; import org.springframework.security.saml2.core.TestSaml2X509Credentials; import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects; -import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlSigningUtils.QueryParametersPartial; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlOperations.SignatureConfigurer; 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; @@ -42,6 +42,8 @@ */ public class OpenSamlLogoutResponseValidatorTests { + private final OpenSamlOperations saml = new OpenSaml4Template(); + private final OpenSamlLogoutResponseValidator manager = new OpenSamlLogoutResponseValidator(); @Test @@ -67,7 +69,8 @@ public void handleWhenRedirectBindingThenValidatesSignatureParameter() { .id("id") .build(); LogoutResponse logoutResponse = TestOpenSamlObjects.assertingPartyLogoutResponse(registration); - Saml2LogoutResponse response = redirect(logoutResponse, registration, OpenSamlSigningUtils.sign(registration)); + Saml2LogoutResponse response = redirect(logoutResponse, registration, + this.saml.withSigningKeys(registration.getSigningX509Credentials())); Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(response, logoutRequest, registration); this.manager.validate(parameters); @@ -166,9 +169,9 @@ private Saml2LogoutResponse post(LogoutResponse logoutResponse, RelyingPartyRegi } private Saml2LogoutResponse redirect(LogoutResponse logoutResponse, RelyingPartyRegistration registration, - QueryParametersPartial partial) { + SignatureConfigurer configurer) { String serialized = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(serialize(logoutResponse))); - Map parameters = partial.param(Saml2ParameterNames.SAML_RESPONSE, serialized).parameters(); + Map parameters = configurer.sign(Map.of(Saml2ParameterNames.SAML_RESPONSE, serialized)); return Saml2LogoutResponse.withRelyingPartyRegistration(registration) .samlResponse(serialized) .parameters((params) -> params.putAll(parameters)) @@ -181,7 +184,7 @@ private void sign(LogoutResponse logoutResponse, RelyingPartyRegistration regist } private String serialize(XMLObject object) { - return OpenSamlSigningUtils.serialize(object); + return this.saml.serialize(object).serialize(); } } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlSigningUtils.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlSigningUtils.java deleted file mode 100644 index 9855a39267b..00000000000 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlSigningUtils.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright 2002-2021 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.authentication.logout; - -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 - */ -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) { - SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration); - 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.getAssertingPartyDetails().getSigningAlgorithms(); - 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; - } - - } - -}