Merge pull request #181 from erickt/sso-post

Add support for SingleSignOnService/prepare_for_authentication with HTTP-POST binding
This commit is contained in:
Roland Hedberg
2015-04-24 10:12:02 -07:00
3 changed files with 123 additions and 12 deletions

View File

@@ -64,20 +64,72 @@ class Saml2Client(Base):
:return: session id and AuthnRequest info
"""
destination = self._sso_location(entityid, binding)
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)
reqid, req = self.create_authn_request(destination, vorg, scoping,
response_binding, nameid_format,
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]:
if expected_binding and binding != expected_binding:
continue
destination = self._sso_location(entityid, binding)
logger.info("destination to provider: %s" % destination)
reqid, request = self.create_authn_request(
destination, vorg, scoping, response_binding, nameid_format,
consent=consent,
extensions=extensions, sign=sign,
**kwargs)
_req_str = "%s" % req
_req_str = str(request)
logger.info("AuthNReq: %s" % _req_str)
info = self.apply_binding(binding, _req_str, destination, relay_state)
http_info = self.apply_binding(binding, _req_str, destination,
relay_state)
return reqid, info
return reqid, binding, http_info
else:
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 :-/

View File

@@ -71,6 +71,10 @@ class VerifyError(SAMLError):
pass
class SignOnError(SAMLError):
pass
class LogoutError(SAMLError):
pass

View File

@@ -635,6 +635,26 @@ class TestClientWithDummy():
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)
assert binding == auth_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_attribute_query(self):
response = self.client.do_attribute_query(
IDP, "_e7b68a04488f715cda642fbdd90099f5",
@@ -699,6 +719,41 @@ class TestClientWithDummy():
'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])
assert binding == auth_binding
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
# if __name__ == "__main__":
# tc = TestClient()