From 85b3fc307c08910dba996cd3b679eebb07663c29 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Mon, 14 Jan 2013 12:09:17 +0100 Subject: [PATCH] Fixed manage_name_id request-response Added assert_id request-response --- src/saml2/client_base.py | 14 +++-- src/saml2/entity.py | 31 +++++++--- src/saml2/request.py | 14 ++++- src/saml2/response.py | 39 ++++++++++++ src/saml2/server.py | 7 ++- src/saml2/sigver.py | 12 ++++ src/saml2/soap.py | 17 +++++- tests/test_67_manage_name_id.py | 43 +++++++++++++ tests/test_68_assertion_id.py | 104 ++++++++++++++++++++++++++++++++ 9 files changed, 262 insertions(+), 19 deletions(-) create mode 100644 tests/test_68_assertion_id.py diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index 028716c..f1c8c32 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -24,7 +24,6 @@ from saml2.mdstore import destinations from saml2.saml import AssertionIDRef from saml2.saml import NAMEID_FORMAT_TRANSIENT from saml2.samlp import AuthnQuery -from saml2.samlp import Response from saml2.samlp import AssertionIDRequest from saml2.samlp import NameIDMappingRequest from saml2.samlp import AttributeQuery @@ -48,6 +47,9 @@ from saml2 import saml from saml2.population import Population from saml2.response import AttributeResponse +from saml2.response import AuthzResponse +from saml2.response import AssertionIDResponse +from saml2.response import AuthnQueryResponse from saml2.response import NameIDMappingResponse from saml2.response import AuthnResponse @@ -491,17 +493,21 @@ class Base(Entity): """ Verify that the response is OK """ - return self._parse_response(response, Response, "", binding) + return self._parse_response(response, AuthzResponse, "", binding) def parse_authn_query_response(self, response, binding=BINDING_SOAP): """ Verify that the response is OK """ - return self._parse_response(response, Response, "", binding) + return self._parse_response(response, AuthnQueryResponse, "", binding) def parse_assertion_id_request_response(self, response, binding): """ Verify that the response is OK """ - return self._parse_response(response, Response, "", binding) + kwargs = {"entity_id": self.config.entityid, + "attribute_converters": self.config.attribute_converters} + + return self._parse_response(response, AssertionIDResponse, "", binding, + **kwargs) # ------------------------------------------------------------------------ diff --git a/src/saml2/entity.py b/src/saml2/entity.py index 905f31f..1427c09 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -146,6 +146,7 @@ class Entity(HTTPBase): entity_id = request.issuer.text.strip() sfunc = getattr(self.metadata, service) + if bindings is None: bindings = self.config.preferred_binding[service] @@ -175,8 +176,9 @@ class Entity(HTTPBase): return {"id":id, "version":VERSION, "issue_instant":instant(), "issuer":self._issuer()} - def response_args(self, message, bindings, descr_type=""): + def response_args(self, message, bindings=None, descr_type=""): info = {"in_response_to": message.id} + if isinstance(message, AuthnRequest): rsrv = "assertion_consumer_service" descr_type = "sp_sso" @@ -189,7 +191,9 @@ class Entity(HTTPBase): descr_type = "sp_sso" elif isinstance(message, ManageNameIDRequest): rsrv = "manage_name_id_service" - # The once below are solely SOAP + # The once below are solely SOAP so no return destination needed + elif isinstance(message, AssertionIDRequest): + rsrv = "" elif isinstance(message, ArtifactResolve): rsrv = "" elif isinstance(message, AssertionIDRequest): @@ -209,6 +213,7 @@ class Entity(HTTPBase): binding, destination = self.pick_binding(rsrv, bindings, descr_type=descr_type, request=message) + #info["binding"] = binding info["destination"] = destination return info @@ -488,7 +493,8 @@ class Entity(HTTPBase): def create_manage_name_id_request(self, destination, id=0, consent=None, extensions=None, sign=False, name_id=None, new_id=None, - encrypted_id=None, new_encrypted_id=None): + encrypted_id=None, new_encrypted_id=None, + terminate=None): """ :param destination: @@ -500,6 +506,7 @@ class Entity(HTTPBase): :param new_id: :param encrypted_id: :param new_encrypted_id: + :param terminate: :return: """ kwargs = self.message_args(id) @@ -515,8 +522,10 @@ class Entity(HTTPBase): kwargs["new_id"] = new_id elif new_encrypted_id: kwargs["new_encrypted_id"] = new_encrypted_id + elif terminate: + kwargs["terminate"] = terminate else: - kwargs["terminate"] = "" + raise AttributeError("One of NewID, NewEncryptedNameID or Terminate has to be provided") return self._message(ManageNameIDRequest, destination, consent=consent, extensions=extensions, sign=sign, **kwargs) @@ -534,13 +543,13 @@ class Entity(HTTPBase): return self._parse_request(xmlstr, request.ManageNameIDRequest, "manage_name_id_service", binding) - def create_manage_name_id_response(self, request, bindings, status=None, - sign=False, issuer=None): + def create_manage_name_id_response(self, request, bindings=None, + status=None, sign=False, issuer=None): rinfo = self.response_args(request, bindings) response = self._status_response(samlp.ManageNameIDResponse, issuer, - status, sign=False, **rinfo) + status, sign, **rinfo) logger.info("Response: %s" % (response,)) @@ -582,7 +591,7 @@ class Entity(HTTPBase): response = response_cls(self.sec, **kwargs) except Exception, exc: logger.info("%s" % exc) - return None + raise xmlstr = self.unravel(xmlstr, binding, response_cls.msgtype) @@ -698,4 +707,8 @@ class Entity(HTTPBase): # should just be one elems = extension_elements_to_elements(resp.response.extension_elements, [samlp, saml]) - return elems[0] \ No newline at end of file + return elems[0] + + def parse_manage_name_id_response(self, xmlstr, binding=BINDING_SOAP): + return self._parse_response(xmlstr, response.ManageNameIDResponse, + "manage_name_id_service", binding) diff --git a/src/saml2/request.py b/src/saml2/request.py index 93ad47c..17a2d28 100644 --- a/src/saml2/request.py +++ b/src/saml2/request.py @@ -155,7 +155,19 @@ class AuthnRequest(Request): def attributes(self): return to_local(self.attribute_converters, self.message) - + + +class AssertionIDRequest(Request): + msgtype = "assertion_id_request" + def __init__(self, sec_context, receiver_addrs, attribute_converters, + timeslack=0): + Request.__init__(self, sec_context, receiver_addrs, + attribute_converters, timeslack) + self.signature_check = self.sec.correctly_signed_assertion_id_request + + def attributes(self): + return to_local(self.attribute_converters, self.message) + class AuthzDecisionQuery(Request): msgtype = "authz_decision_query" diff --git a/src/saml2/response.py b/src/saml2/response.py index 7777bc6..d00da61 100644 --- a/src/saml2/response.py +++ b/src/saml2/response.py @@ -256,6 +256,16 @@ class NameIDMappingResponse(StatusResponse): request_id, asynchop) self.signature_check = self.sec.correctly_signed_name_id_mapping_response +class ManageNameIDResponse(StatusResponse): + msgtype = "manage_name_id_response" + + def __init__(self, sec_context, return_addr=None, timeslack=0, + request_id=0, asynchop=True): + StatusResponse.__init__(self, sec_context, return_addr, timeslack, + request_id, asynchop) + self.signature_check = self.sec.correctly_signed_manage_name_id_response + + # ---------------------------------------------------------------------------- class AuthnResponse(StatusResponse): @@ -632,6 +642,35 @@ class AuthnResponse(StatusResponse): def __str__(self): return "%s" % self.xmlstr +class AssertionIDResponse(AuthnResponse): + msgtype = "assertion_id_response" + + def __init__(self, sec_context, attribute_converters, entity_id, + return_addr=None, timeslack=0, asynchop=False, test=False): + + AuthnResponse.__init__(self, sec_context, attribute_converters, + entity_id, return_addr, timeslack=timeslack, + asynchop=asynchop, test=test) + self.entity_id = entity_id + self.attribute_converters = attribute_converters + self.assertion = None + self.context = "AssertionIdResponse" + +class AuthnQueryResponse(AuthnResponse): + msgtype = "authn_query_response" + + def __init__(self, sec_context, attribute_converters, entity_id, + return_addr=None, timeslack=0, asynchop=False, test=False): + + AuthnResponse.__init__(self, sec_context, attribute_converters, + entity_id, return_addr, timeslack=timeslack, + asynchop=asynchop, test=test) + self.entity_id = entity_id + self.attribute_converters = attribute_converters + self.assertion = None + self.context = "AuthnQueryResponse" + + class AttributeResponse(AuthnResponse): msgtype = "attribute_response" diff --git a/src/saml2/server.py b/src/saml2/server.py index 44b7f9c..86e61e1 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -23,9 +23,8 @@ import logging import shelve import sys import memcache -from saml2.soap import parse_soap_enveloped_saml_name_id_mapping_request -from saml2.samlp import AuthzDecisionQuery, NameIDMappingResponse -from saml2.samlp import AssertionIDRequest +from saml2.samlp import AuthzDecisionQuery +from saml2.samlp import NameIDMappingResponse from saml2.samlp import AuthnQuery from saml2.entity import Entity @@ -34,6 +33,7 @@ from saml2 import class_name from saml2 import BINDING_HTTP_REDIRECT from saml2.request import AuthnRequest +from saml2.request import AssertionIDRequest from saml2.request import AttributeQuery from saml2.request import NameIDMappingRequest @@ -619,3 +619,4 @@ class Server(Entity): else: logger.info("Message: %s" % _resp) return _resp + diff --git a/src/saml2/sigver.py b/src/saml2/sigver.py index c1a82f9..3aa016b 100644 --- a/src/saml2/sigver.py +++ b/src/saml2/sigver.py @@ -822,6 +822,18 @@ class SecurityContext(object): "manage_name_id_request", must, origdoc) + def correctly_signed_manage_name_id_response(self, decoded_xml, must=False, + origdoc=None): + return self.correctly_signed_message(decoded_xml, + "manage_name_id_response", must, + origdoc) + + def correctly_signed_assertion_id_request(self, decoded_xml, must=False, + origdoc=None): + return self.correctly_signed_message(decoded_xml, + "assertion_id_request", must, + origdoc) + def correctly_signed_response(self, decoded_xml, must=False, origdoc=None): """ Check if a instance is correctly signed, if we have metadata for the IdP that sent the info use that, if not use the key that are in diff --git a/src/saml2/soap.py b/src/saml2/soap.py index 260ecd8..88f3fdf 100644 --- a/src/saml2/soap.py +++ b/src/saml2/soap.py @@ -79,6 +79,19 @@ def parse_soap_enveloped_saml_manage_name_id_request(text): expected_tag = '{%s}ManageNameIDRequest' % SAMLP_NAMESPACE return parse_soap_enveloped_saml_thingy(text, [expected_tag]) +def parse_soap_enveloped_saml_manage_name_id_response(text): + expected_tag = '{%s}ManageNameIDResponse' % SAMLP_NAMESPACE + return parse_soap_enveloped_saml_thingy(text, [expected_tag]) + +def parse_soap_enveloped_saml_assertion_id_request(text): + expected_tag = '{%s}AssertionIDRequest' % SAMLP_NAMESPACE + return parse_soap_enveloped_saml_thingy(text, [expected_tag]) + +def parse_soap_enveloped_saml_assertion_id_response(text): + tags = ['{%s}Response' % SAMLP_NAMESPACE, + '{%s}AssertionIDResponse' % SAMLP_NAMESPACE] + return parse_soap_enveloped_saml_thingy(text, tags) + #def parse_soap_enveloped_saml_logout_response(text): # expected_tag = '{%s}LogoutResponse' % SAMLP_NAMESPACE # return parse_soap_enveloped_saml_thingy(text, [expected_tag]) @@ -110,8 +123,8 @@ def parse_soap_enveloped_saml_thingy(text, expected_tags): if saml_part.tag in expected_tags: return ElementTree.tostring(saml_part, encoding="UTF-8") else: - raise WrongMessageType("Was '%s' expected '%s'" % (saml_part.tag, - expected_tags)) + raise WrongMessageType("Was '%s' expected one of %s" % (saml_part.tag, + expected_tags)) import re diff --git a/tests/test_67_manage_name_id.py b/tests/test_67_manage_name_id.py index f532fdb..db0a764 100644 --- a/tests/test_67_manage_name_id.py +++ b/tests/test_67_manage_name_id.py @@ -1,3 +1,4 @@ +from saml2 import BINDING_SOAP from saml2.samlp import NewID from saml2.saml import NameID, NAMEID_FORMAT_TRANSIENT from saml2.client import Saml2Client @@ -29,3 +30,45 @@ def test_basic(): print _req.message assert mid.id == _req.message.id + +def test_flow(): + sp = Saml2Client(config_file="servera_conf") + idp = Server(config_file="idp_all_conf") + + binding, destination = sp.pick_binding("manage_name_id_service", + entity_id=idp.config.entityid) + + nameid = NameID(format=NAMEID_FORMAT_TRANSIENT, text="foobar") + newid = NewID(text="Barfoo") + + mid = sp.create_manage_name_id_request(destination, name_id=nameid, + new_id=newid) + + print mid + rargs = sp.apply_binding(binding, "%s" % mid, destination, "") + + # --------- @IDP -------------- + + _req = idp.parse_manage_name_id_request(rargs["data"], binding) + + print _req.message + + mnir = idp.create_manage_name_id_response(_req.message, None) + + if binding != BINDING_SOAP: + binding, destination = idp.pick_binding("manage_name_id_service", + entity_id=sp.config.entityid) + else: + destination = "" + + respargs = idp.apply_binding(binding, "%s" % mnir, destination, "") + + print respargs + + # ---------- @SP --------------- + + _response = sp.parse_manage_name_id_response(respargs["data"], binding) + + print _response.response + + assert _response.response.id == mnir.id \ No newline at end of file diff --git a/tests/test_68_assertion_id.py b/tests/test_68_assertion_id.py new file mode 100644 index 0000000..0bfc383 --- /dev/null +++ b/tests/test_68_assertion_id.py @@ -0,0 +1,104 @@ +from urlparse import parse_qs +from urlparse import urlparse +from saml2.samlp import AuthnRequest +from saml2.samlp import NameIDPolicy +from saml2.saml import AUTHN_PASSWORD +from saml2.saml import NAMEID_FORMAT_TRANSIENT +from saml2 import BINDING_HTTP_POST +from saml2 import BINDING_SOAP +from saml2.client import Saml2Client +from saml2.server import Server + +__author__ = 'rolandh' + +TAG1 = "name=\"SAMLRequest\" value=" + +def get_msg(hinfo, binding): + if binding == BINDING_SOAP: + xmlstr = hinfo["data"] + elif binding == BINDING_HTTP_POST: + _inp = hinfo["data"][3] + i = _inp.find(TAG1) + i += len(TAG1) + 1 + j = _inp.find('"', i) + xmlstr = _inp[i:j] + else: # BINDING_HTTP_REDIRECT + parts = urlparse(hinfo["headers"][0][1]) + xmlstr = parse_qs(parts.query)["SAMLRequest"][0] + + return xmlstr + +def test_basic_flow(): + sp = Saml2Client(config_file="servera_conf") + idp = Server(config_file="idp_all_conf") + + # -------- @IDP ------------- + + relay_state = "FOO" + # -- dummy request --- + orig_req = AuthnRequest(issuer=sp._issuer(), + name_id_policy=NameIDPolicy(allow_create="true", + format=NAMEID_FORMAT_TRANSIENT)) + + # == Create an AuthnRequest response + + name_id = idp.ident.transient_nameid(sp.config.entityid, "id12") + binding, destination = idp.pick_binding("assertion_consumer_service", + entity_id=sp.config.entityid) + resp = idp.create_authn_response({"eduPersonEntitlement": "Short stop", + "surName": "Jeter", + "givenName": "Derek", + "mail": "derek.jeter@nyy.mlb.com", + "title": "The man"}, + "id-123456789", + destination, + sp.config.entityid, + name_id=name_id, + authn=(AUTHN_PASSWORD, + "http://www.example.com/login")) + + hinfo = idp.apply_binding(binding, "%s" % resp, destination, relay_state) + + # --------- @SP ------------- + + xmlstr = get_msg(hinfo, binding) + + aresp = sp.parse_authn_request_response(xmlstr, binding, + {resp.in_response_to :"/"}) + + # == Look for assertion X + + asid = aresp.assertion.id + + binding, destination = sp.pick_binding("assertion_id_request_service", + entity_id=idp.config.entityid) + + _req = sp.create_assertion_id_request([asid], destination) + + hinfo = sp.apply_binding(binding, "%s" % _req, destination, + "realy_stat") + + # ---------- @IDP ------------ + + xmlstr = get_msg(hinfo, binding) + + rr = idp.parse_assertion_id_request(xmlstr, binding) + + print rr + + # == construct response + + aids = [x.text for x in rr.message.assertion_id_ref] + resp_args = idp.response_args(rr.message) + + resp = idp.create_assertion_id_request_response(aids, **resp_args) + + hinfo = idp.apply_binding(binding, "%s" % resp, None, "", "SAMLResponse") + + # ----------- @SP ------------- + + xmlstr = get_msg(hinfo, binding) + + final = sp.parse_assertion_id_request_response(xmlstr, binding) + + print final \ No newline at end of file