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 new file mode 100644 index 0000000000..a7fd9a6f5f --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/LogoutRequestEncryptedIDUtils.java @@ -0,0 +1,81 @@ +/* + * 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.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/OpenSamlLogoutRequestValidator.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java index 69df68246a..e20082a760 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 @@ -25,6 +25,8 @@ 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; @@ -118,7 +120,7 @@ public final class OpenSamlLogoutRequestValidator implements Saml2LogoutRequestV return (errors) -> { validateIssuer(request, registration).accept(errors); validateDestination(request, registration).accept(errors); - validateName(request, authentication).accept(errors); + validateSubject(request, registration, authentication).accept(errors); }; } @@ -153,23 +155,44 @@ public final class OpenSamlLogoutRequestValidator implements Saml2LogoutRequestV }; } - private Consumer> validateName(LogoutRequest request, Authentication authentication) { + private Consumer> validateSubject(LogoutRequest request, + RelyingPartyRegistration registration, Authentication authentication) { return (errors) -> { if (authentication == null) { return; } NameID nameId = request.getNameID(); - if (nameId == null) { + EncryptedID encryptedID = request.getEncryptedID(); + if (nameId == null && encryptedID == null) { errors.add( new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND, "Failed to find subject in LogoutRequest")); return; } - 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")); + + if (nameId != null) { + validateNameID(nameId, authentication, errors); + } + else { + final NameID nameIDFromEncryptedID = decryptNameID(encryptedID, registration); + validateNameID(nameIDFromEncryptedID, authentication, errors); } }; } + 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")); + } + } + + 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/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java index 0f38823e22..49eae5891d 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java @@ -365,6 +365,24 @@ public final class TestOpenSamlObjects { return logoutRequest; } + public static LogoutRequest assertingPartyLogoutRequestNameIdInEncryptedId(RelyingPartyRegistration registration) { + LogoutRequestBuilder logoutRequestBuilder = new LogoutRequestBuilder(); + LogoutRequest logoutRequest = logoutRequestBuilder.buildObject(); + logoutRequest.setID("id"); + NameIDBuilder nameIdBuilder = new NameIDBuilder(); + NameID nameId = nameIdBuilder.buildObject(); + nameId.setValue("user"); + logoutRequest.setNameID(null); + logoutRequest.setEncryptedID(encrypted(nameId, + registration.getAssertingPartyDetails().getEncryptionX509Credentials().stream().findFirst().get())); + IssuerBuilder issuerBuilder = new IssuerBuilder(); + Issuer issuer = issuerBuilder.buildObject(); + issuer.setValue(registration.getAssertingPartyDetails().getEntityId()); + logoutRequest.setIssuer(issuer); + logoutRequest.setDestination(registration.getSingleLogoutServiceLocation()); + return logoutRequest; + } + public static LogoutResponse assertingPartyLogoutResponse(RelyingPartyRegistration registration) { LogoutResponseBuilder logoutResponseBuilder = new LogoutResponseBuilder(); LogoutResponse logoutResponse = logoutResponseBuilder.buildObject(); 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 e5c826fe37..8bd38f988e 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 @@ -60,6 +60,21 @@ public class OpenSamlLogoutRequestValidatorTests { assertThat(result.hasErrors()).isFalse(); } + @Test + public void handleWhenNameIdInEncryptedIdPostThenValidates() { + + RelyingPartyRegistration registration = registrationWithEncryption() + .assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)).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.manager.validate(parameters); + assertThat(result.hasErrors()).withFailMessage(() -> result.getErrors().toString()).isFalse().isFalse(); + + } + @Test public void handleWhenRedirectBindingThenValidatesSignatureParameter() { RelyingPartyRegistration registration = registration() @@ -134,6 +149,12 @@ public class OpenSamlLogoutRequestValidatorTests { .assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)); } + private RelyingPartyRegistration.Builder registrationWithEncryption() { + return signing(verifying(TestRelyingPartyRegistrations.full())) + .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())));