Skip to content

Commit 81e50ff

Browse files
author
Roland Hedberg
committed
Merge pull request #181 from erickt/sso-post
Add support for SingleSignOnService/prepare_for_authentication with HTTP-POST binding
2 parents 48c178f + fdbd305 commit 81e50ff

File tree

3 files changed

+122
-11
lines changed

3 files changed

+122
-11
lines changed

src/saml2/client.py

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,20 +64,72 @@ def prepare_for_authenticate(self, entityid=None, relay_state="",
6464
:return: session id and AuthnRequest info
6565
"""
6666

67-
destination = self._sso_location(entityid, binding)
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
6881

69-
reqid, req = self.create_authn_request(destination, vorg, scoping,
70-
response_binding, nameid_format,
71-
consent=consent,
72-
extensions=extensions, sign=sign,
73-
**kwargs)
74-
_req_str = "%s" % req
82+
return reqid, info
7583

76-
logger.info("AuthNReq: %s" % _req_str)
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.
7793
78-
info = self.apply_binding(binding, _req_str, destination, relay_state)
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+
"""
79107

80-
return reqid, info
108+
expected_binding = binding
109+
110+
for binding in [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST]:
111+
if expected_binding and binding != expected_binding:
112+
continue
113+
114+
destination = self._sso_location(entityid, binding)
115+
logger.info("destination to provider: %s" % destination)
116+
117+
reqid, request = self.create_authn_request(
118+
destination, vorg, scoping, response_binding, nameid_format,
119+
consent=consent,
120+
extensions=extensions, sign=sign,
121+
**kwargs)
122+
123+
_req_str = str(request)
124+
125+
logger.info("AuthNReq: %s" % _req_str)
126+
127+
http_info = self.apply_binding(binding, _req_str, destination,
128+
relay_state)
129+
130+
return reqid, binding, http_info
131+
else:
132+
raise SignOnError("No supported bindings available for authentication")
81133

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

src/saml2/client_base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ class VerifyError(SAMLError):
7171
pass
7272

7373

74+
class SignOnError(SAMLError):
75+
pass
76+
77+
7478
class LogoutError(SAMLError):
7579
pass
7680

tests/test_51_client.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,26 @@ def test_do_authn(self):
635635
resp_args = self.server.response_args(req.message, [response_binding])
636636
assert resp_args["binding"] == response_binding
637637

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(
642+
IDP, "http://www.example.com/relay_state",
643+
binding=binding, response_binding=response_binding)
644+
645+
assert binding == auth_binding
646+
assert isinstance(sid, basestring)
647+
assert len(http_args) == 4
648+
assert http_args["headers"][0][0] == "Location"
649+
assert http_args["data"] == []
650+
redirect_url = http_args["headers"][0][1]
651+
_, _, _, _, qs, _ = urlparse.urlparse(redirect_url)
652+
qs_dict = urlparse.parse_qs(qs)
653+
req = self.server.parse_authn_request(qs_dict["SAMLRequest"][0],
654+
binding)
655+
resp_args = self.server.response_args(req.message, [response_binding])
656+
assert resp_args["binding"] == response_binding
657+
638658
def test_do_attribute_query(self):
639659
response = self.client.do_attribute_query(
640660
IDP, "_e7b68a04488f715cda642fbdd90099f5",
@@ -699,6 +719,41 @@ def test_post_sso(self):
699719
'http://www.example.com/login'
700720
assert ac.authn_context_class_ref.text == INTERNETPROTOCOLPASSWORD
701721

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(
726+
"urn:mace:example.com:saml:roland:idp", relay_state="really",
727+
binding=binding, response_binding=response_binding)
728+
_dic = unpack_form(http_args["data"][3])
729+
730+
assert binding == auth_binding
731+
732+
req = self.server.parse_authn_request(_dic["SAMLRequest"], binding)
733+
resp_args = self.server.response_args(req.message, [response_binding])
734+
assert resp_args["binding"] == response_binding
735+
736+
# Normally a response would now be sent back to the users web client
737+
# Here I fake what the client will do
738+
# create the form post
739+
740+
http_args["data"] = urllib.urlencode(_dic)
741+
http_args["method"] = "POST"
742+
http_args["dummy"] = _dic["SAMLRequest"]
743+
http_args["headers"] = [('Content-type',
744+
'application/x-www-form-urlencoded')]
745+
746+
response = self.client.send(**http_args)
747+
print response.text
748+
_dic = unpack_form(response.text[3], "SAMLResponse")
749+
resp = self.client.parse_authn_request_response(_dic["SAMLResponse"],
750+
BINDING_HTTP_POST,
751+
{sid: "/"})
752+
ac = resp.assertion.authn_statement[0].authn_context
753+
assert ac.authenticating_authority[0].text == \
754+
'http://www.example.com/login'
755+
assert ac.authn_context_class_ref.text == INTERNETPROTOCOLPASSWORD
756+
702757

703758
# if __name__ == "__main__":
704759
# tc = TestClient()
@@ -708,4 +763,4 @@ def test_post_sso(self):
708763
if __name__ == "__main__":
709764
tc = TestClient()
710765
tc.setup_class()
711-
tc.test_sign_then_encrypt_assertion_advice()
766+
tc.test_sign_then_encrypt_assertion_advice()

0 commit comments

Comments
 (0)