Skip to content

Commit fdbd305

Browse files
committed
Add support for SingleSignOnService negotiating which binding to use
1 parent f416983 commit fdbd305

File tree

3 files changed

+99
-6
lines changed

3 files changed

+99
-6
lines changed

src/saml2/client.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class Saml2Client(Base):
4242
""" The basic pySAML2 service provider class """
4343

4444
def prepare_for_authenticate(self, entityid=None, relay_state="",
45-
binding=None, vorg="",
45+
binding=saml2.BINDING_HTTP_REDIRECT, vorg="",
4646
nameid_format=None,
4747
scoping=None, consent=None, extensions=None,
4848
sign=None,
@@ -64,6 +64,47 @@ def prepare_for_authenticate(self, entityid=None, relay_state="",
6464
:return: session id and AuthnRequest info
6565
"""
6666

67+
reqid, negotiated_binding, info = self.prepare_for_negotiated_authenticate(
68+
entityid=entityid,
69+
relay_state=relay_state,
70+
binding=binding,
71+
vorg=vorg,
72+
nameid_format=nameid_format,
73+
scoping=scoping,
74+
consent=consent,
75+
extensions=extensions,
76+
sign=sign,
77+
response_binding=response_binding,
78+
**kwargs)
79+
80+
assert negotiated_binding == binding
81+
82+
return reqid, info
83+
84+
def prepare_for_negotiated_authenticate(self, entityid=None, relay_state="",
85+
binding=None, vorg="",
86+
nameid_format=None,
87+
scoping=None, consent=None, extensions=None,
88+
sign=None,
89+
response_binding=saml2.BINDING_HTTP_POST,
90+
**kwargs):
91+
""" Makes all necessary preparations for an authentication request that negotiates
92+
which binding to use for authentication.
93+
94+
:param entityid: The entity ID of the IdP to send the request to
95+
:param relay_state: To where the user should be returned after
96+
successfull log in.
97+
:param binding: Which binding to use for sending the request
98+
:param vorg: The entity_id of the virtual organization I'm a member of
99+
:param scoping: For which IdPs this query are aimed.
100+
:param consent: Whether the principal have given her consent
101+
:param extensions: Possible extensions
102+
:param sign: Whether the request should be signed or not.
103+
:param response_binding: Which binding to use for receiving the response
104+
:param kwargs: Extra key word arguments
105+
:return: session id and AuthnRequest info
106+
"""
107+
67108
expected_binding = binding
68109

69110
for binding in [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST]:
@@ -88,7 +129,7 @@ def prepare_for_authenticate(self, entityid=None, relay_state="",
88129

89130
return reqid, binding, http_info
90131
else:
91-
raise SignonError("No binding available for singon")
132+
raise SignOnError("No supported bindings available for authentication")
92133

93134
def global_logout(self, name_id, reason="", expire=None, sign=None):
94135
""" More or less a layer of indirection :-/

src/saml2/client_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class VerifyError(SAMLError):
7171
pass
7272

7373

74-
class SignonError(SAMLError):
74+
class SignOnError(SAMLError):
7575
pass
7676

7777

tests/test_51_client.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,26 @@ def setup_class(self):
619619
def test_do_authn(self):
620620
binding = BINDING_HTTP_REDIRECT
621621
response_binding = BINDING_HTTP_POST
622-
sid, auth_binding, http_args = self.client.prepare_for_authenticate(
622+
sid, http_args = self.client.prepare_for_authenticate(
623+
IDP, "http://www.example.com/relay_state",
624+
binding=binding, response_binding=response_binding)
625+
626+
assert isinstance(sid, basestring)
627+
assert len(http_args) == 4
628+
assert http_args["headers"][0][0] == "Location"
629+
assert http_args["data"] == []
630+
redirect_url = http_args["headers"][0][1]
631+
_, _, _, _, qs, _ = urlparse.urlparse(redirect_url)
632+
qs_dict = urlparse.parse_qs(qs)
633+
req = self.server.parse_authn_request(qs_dict["SAMLRequest"][0],
634+
binding)
635+
resp_args = self.server.response_args(req.message, [response_binding])
636+
assert resp_args["binding"] == response_binding
637+
638+
def test_do_negotiated_authn(self):
639+
binding = BINDING_HTTP_REDIRECT
640+
response_binding = BINDING_HTTP_POST
641+
sid, auth_binding, http_args = self.client.prepare_for_negotiated_authenticate(
623642
IDP, "http://www.example.com/relay_state",
624643
binding=binding, response_binding=response_binding)
625644

@@ -670,7 +689,40 @@ def test_logout_1(self):
670689
def test_post_sso(self):
671690
binding = BINDING_HTTP_POST
672691
response_binding = BINDING_HTTP_POST
673-
sid, auth_binding, http_args = self.client.prepare_for_authenticate(
692+
sid, http_args = self.client.prepare_for_authenticate(
693+
"urn:mace:example.com:saml:roland:idp", relay_state="really",
694+
binding=binding, response_binding=response_binding)
695+
_dic = unpack_form(http_args["data"][3])
696+
697+
req = self.server.parse_authn_request(_dic["SAMLRequest"], binding)
698+
resp_args = self.server.response_args(req.message, [response_binding])
699+
assert resp_args["binding"] == response_binding
700+
701+
# Normally a response would now be sent back to the users web client
702+
# Here I fake what the client will do
703+
# create the form post
704+
705+
http_args["data"] = urllib.urlencode(_dic)
706+
http_args["method"] = "POST"
707+
http_args["dummy"] = _dic["SAMLRequest"]
708+
http_args["headers"] = [('Content-type',
709+
'application/x-www-form-urlencoded')]
710+
711+
response = self.client.send(**http_args)
712+
print response.text
713+
_dic = unpack_form(response.text[3], "SAMLResponse")
714+
resp = self.client.parse_authn_request_response(_dic["SAMLResponse"],
715+
BINDING_HTTP_POST,
716+
{sid: "/"})
717+
ac = resp.assertion.authn_statement[0].authn_context
718+
assert ac.authenticating_authority[0].text == \
719+
'http://www.example.com/login'
720+
assert ac.authn_context_class_ref.text == INTERNETPROTOCOLPASSWORD
721+
722+
def test_negotiated_post_sso(self):
723+
binding = BINDING_HTTP_POST
724+
response_binding = BINDING_HTTP_POST
725+
sid, auth_binding, http_args = self.client.prepare_for_negotiated_authenticate(
674726
"urn:mace:example.com:saml:roland:idp", relay_state="really",
675727
binding=binding, response_binding=response_binding)
676728
_dic = unpack_form(http_args["data"][3])
@@ -711,4 +763,4 @@ def test_post_sso(self):
711763
if __name__ == "__main__":
712764
tc = TestClient()
713765
tc.setup_class()
714-
tc.test_sign_then_encrypt_assertion_advice()
766+
tc.test_sign_then_encrypt_assertion_advice()

0 commit comments

Comments
 (0)