diff --git a/src/saml2/request.py b/src/saml2/request.py index e013e058c..2f53109d9 100644 --- a/src/saml2/request.py +++ b/src/saml2/request.py @@ -6,6 +6,8 @@ from saml2.response import IncorrectlySigned from saml2.s_utils import OtherError from saml2.s_utils import VersionMismatch +from saml2.saml import name_id_from_string +from saml2.sigver import DecryptError from saml2.sigver import verify_redirect_signature from saml2.validate import NotValid from saml2.validate import valid_instance @@ -195,6 +197,55 @@ def __init__(self, sec_context, receiver_addrs, attribute_converters=None, times Request.__init__(self, sec_context, receiver_addrs, attribute_converters, timeslack) self.signature_check = self.sec.correctly_signed_logout_request + def _loads( + self, + xmldata, + binding=None, + origdoc=None, + must=None, + only_valid_cert=False, + relay_state=None, + sigalg=None, + signature=None, + ): + super()._loads( + xmldata, + binding, + origdoc, + must, + only_valid_cert=only_valid_cert, + relay_state=relay_state, + sigalg=sigalg, + signature=signature, + ) + + if self.message.name_id is None and self.message.encrypted_id is not None: + self.message.name_id = self._decrypt_name_id(self.message.encrypted_id) + + return self + + def _decrypt_name_id(self, encrypted_id): + """Decrypt the EncryptedID of the request with our encryption keys. + + A Shibboleth IdP encrypts the NameID of a LogoutRequest whenever the + service provider publishes an encryption key in its metadata, so this + is what a service provider in an identity federation receives. Without + the NameID the request cannot be matched to a session and the logout + is answered with "Wrong user". + + :param encrypted_id: The EncryptedID element of the request + :return: The decrypted NameID, or None if no key could decrypt it + """ + try: + name_id_str = self.sec.decrypt_keys(encrypted_id.encrypted_data.to_string()) + except DecryptError as exc: + logger.warning("Could not decrypt the EncryptedID of the logout request: %s", exc) + return None + + name_id = name_id_from_string(name_id_str) + logger.debug("Decrypted NameID of the logout request: %s", name_id) + return name_id + @property def issuer(self): return self.message.issuer diff --git a/tests/test_51_client.py b/tests/test_51_client.py index e1eb85ecb..e852fd0de 100644 --- a/tests/test_51_client.py +++ b/tests/test_51_client.py @@ -21,6 +21,7 @@ from saml2 import saml from saml2 import samlp from saml2 import sigver +from saml2 import xmlenc from saml2.argtree import add_path from saml2.assertion import Assertion from saml2.authn_context import INTERNETPROTOCOLPASSWORD @@ -442,6 +443,55 @@ def test_logout_response(self): # Signature not found assert xml.decode("UTF-8").find(r"Signature") < 0 + def test_logout_request_with_encrypted_name_id(self): + # A Shibboleth IdP encrypts the NameID of a LogoutRequest whenever the + # SP publishes an encryption key, so this is what an SP in an identity + # federation receives. Without decryption the SP answers "Wrong user". + req_id, req = self.server.create_logout_request( + "http://localhost:8088/slo", + "urn:mace:example.com:saml:roland:sp", + name_id=nid, + reason="Tired", + expire=in_a_while(minutes=15), + session_indexes=["_foo"], + ) + + template = pre_encryption_part(msg_enc="http://www.w3.org/2001/04/xmlenc#aes128-cbc") + encrypted = self.server.sec.crypto.encrypt_assertion( + str(req.name_id), + full_path("test_1.crt"), + template, + key_type="aes-128", + node_xpath="/*[local-name()='NameID']", + ) + req.encrypted_id = saml.EncryptedID(encrypted_data=xmlenc.encrypted_data_from_string(rm_xmltag(encrypted))) + req.name_id = None + assert "EncryptedID" in str(req) + assert "NameID" not in str(req).replace("EncryptedID", "") + + info = self.client.apply_binding(BINDING_HTTP_POST, req, destination="", relay_state="relay2") + _dic_info = unpack_form(info["data"], "SAMLRequest") + samlreq = _dic_info["SAMLRequest"] + + parsed = self.client.parse_logout_request(samlreq, BINDING_HTTP_POST) + assert parsed.message.name_id == nid + assert parsed.subject_id() == nid + + # the session the SP stored at login, which the logout has to find + self.client.users.add_information_about_person( + { + "name_id": nid, + "issuer": self.server.config.entityid, + "not_on_or_after": in_a_while(minutes=15), + "ava": {}, + } + ) + resphttp = self.client.handle_logout_request(samlreq, nid, BINDING_HTTP_POST) + _dic = unpack_form(resphttp["data"], "SAMLResponse") + xml = b64decode(_dic["SAMLResponse"].encode("UTF-8")).decode("UTF-8") + assert "urn:oasis:names:tc:SAML:2.0:status:Success" in xml + assert "Wrong user" not in xml + def test_create_logout_request(self): req_id, req = self.client.create_logout_request( "http://localhost:8088/slo",