Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/saml2/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
50 changes: 50 additions & 0 deletions tests/test_51_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down