From fdbd305fb78cccc9a7a9327f970914f296d26f27 Mon Sep 17 00:00:00 2001 From: Erick Tryzelaar Date: Thu, 5 Feb 2015 10:52:37 -0800 Subject: [PATCH] Add support for SingleSignOnService negotiating which binding to use --- src/saml2/client.py | 45 +++++++++++++++++++++++++++++-- src/saml2/client_base.py | 2 +- tests/test_51_client.py | 58 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 99 insertions(+), 6 deletions(-) diff --git a/src/saml2/client.py b/src/saml2/client.py index ebd744a..6ee5ef3 100644 --- a/src/saml2/client.py +++ b/src/saml2/client.py @@ -42,7 +42,7 @@ class Saml2Client(Base): """ The basic pySAML2 service provider class """ def prepare_for_authenticate(self, entityid=None, relay_state="", - binding=None, vorg="", + binding=saml2.BINDING_HTTP_REDIRECT, vorg="", nameid_format=None, scoping=None, consent=None, extensions=None, sign=None, @@ -64,6 +64,47 @@ class Saml2Client(Base): :return: session id and AuthnRequest info """ + reqid, negotiated_binding, info = self.prepare_for_negotiated_authenticate( + entityid=entityid, + relay_state=relay_state, + binding=binding, + vorg=vorg, + nameid_format=nameid_format, + scoping=scoping, + consent=consent, + extensions=extensions, + sign=sign, + response_binding=response_binding, + **kwargs) + + assert negotiated_binding == binding + + return reqid, info + + def prepare_for_negotiated_authenticate(self, entityid=None, relay_state="", + binding=None, vorg="", + nameid_format=None, + scoping=None, consent=None, extensions=None, + sign=None, + response_binding=saml2.BINDING_HTTP_POST, + **kwargs): + """ Makes all necessary preparations for an authentication request that negotiates + which binding to use for authentication. + + :param entityid: The entity ID of the IdP to send the request to + :param relay_state: To where the user should be returned after + successfull log in. + :param binding: Which binding to use for sending the request + :param vorg: The entity_id of the virtual organization I'm a member of + :param scoping: For which IdPs this query are aimed. + :param consent: Whether the principal have given her consent + :param extensions: Possible extensions + :param sign: Whether the request should be signed or not. + :param response_binding: Which binding to use for receiving the response + :param kwargs: Extra key word arguments + :return: session id and AuthnRequest info + """ + expected_binding = binding for binding in [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST]: @@ -88,7 +129,7 @@ class Saml2Client(Base): return reqid, binding, http_info else: - raise SignonError("No binding available for singon") + raise SignOnError("No supported bindings available for authentication") def global_logout(self, name_id, reason="", expire=None, sign=None): """ More or less a layer of indirection :-/ diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index 4f6c20c..4e9cbdf 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -71,7 +71,7 @@ class VerifyError(SAMLError): pass -class SignonError(SAMLError): +class SignOnError(SAMLError): pass diff --git a/tests/test_51_client.py b/tests/test_51_client.py index eebbccf..26b9da8 100644 --- a/tests/test_51_client.py +++ b/tests/test_51_client.py @@ -619,7 +619,26 @@ class TestClientWithDummy(): def test_do_authn(self): binding = BINDING_HTTP_REDIRECT response_binding = BINDING_HTTP_POST - sid, auth_binding, http_args = self.client.prepare_for_authenticate( + sid, http_args = self.client.prepare_for_authenticate( + IDP, "http://www.example.com/relay_state", + binding=binding, response_binding=response_binding) + + assert isinstance(sid, basestring) + assert len(http_args) == 4 + assert http_args["headers"][0][0] == "Location" + assert http_args["data"] == [] + redirect_url = http_args["headers"][0][1] + _, _, _, _, qs, _ = urlparse.urlparse(redirect_url) + qs_dict = urlparse.parse_qs(qs) + req = self.server.parse_authn_request(qs_dict["SAMLRequest"][0], + binding) + resp_args = self.server.response_args(req.message, [response_binding]) + assert resp_args["binding"] == response_binding + + def test_do_negotiated_authn(self): + binding = BINDING_HTTP_REDIRECT + response_binding = BINDING_HTTP_POST + sid, auth_binding, http_args = self.client.prepare_for_negotiated_authenticate( IDP, "http://www.example.com/relay_state", binding=binding, response_binding=response_binding) @@ -670,7 +689,40 @@ class TestClientWithDummy(): def test_post_sso(self): binding = BINDING_HTTP_POST response_binding = BINDING_HTTP_POST - sid, auth_binding, http_args = self.client.prepare_for_authenticate( + sid, http_args = self.client.prepare_for_authenticate( + "urn:mace:example.com:saml:roland:idp", relay_state="really", + binding=binding, response_binding=response_binding) + _dic = unpack_form(http_args["data"][3]) + + req = self.server.parse_authn_request(_dic["SAMLRequest"], binding) + resp_args = self.server.response_args(req.message, [response_binding]) + assert resp_args["binding"] == response_binding + + # Normally a response would now be sent back to the users web client + # Here I fake what the client will do + # create the form post + + http_args["data"] = urllib.urlencode(_dic) + http_args["method"] = "POST" + http_args["dummy"] = _dic["SAMLRequest"] + http_args["headers"] = [('Content-type', + 'application/x-www-form-urlencoded')] + + response = self.client.send(**http_args) + print response.text + _dic = unpack_form(response.text[3], "SAMLResponse") + resp = self.client.parse_authn_request_response(_dic["SAMLResponse"], + BINDING_HTTP_POST, + {sid: "/"}) + ac = resp.assertion.authn_statement[0].authn_context + assert ac.authenticating_authority[0].text == \ + 'http://www.example.com/login' + assert ac.authn_context_class_ref.text == INTERNETPROTOCOLPASSWORD + + def test_negotiated_post_sso(self): + binding = BINDING_HTTP_POST + response_binding = BINDING_HTTP_POST + sid, auth_binding, http_args = self.client.prepare_for_negotiated_authenticate( "urn:mace:example.com:saml:roland:idp", relay_state="really", binding=binding, response_binding=response_binding) _dic = unpack_form(http_args["data"][3]) @@ -711,4 +763,4 @@ class TestClientWithDummy(): if __name__ == "__main__": tc = TestClient() tc.setup_class() - tc.test_sign_then_encrypt_assertion_advice() \ No newline at end of file + tc.test_sign_then_encrypt_assertion_advice()