summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLorenzo Gil <lgs@yaco.es>2012-06-02 21:19:31 +0200
committerLorenzo Gil <lgs@yaco.es>2012-06-02 21:19:31 +0200
commitd18d58ee5ff0d8675e141a757aa3a78bb05382c4 (patch)
treed0f61c96a5f9775316d798e4315c1728d33f410e
parenteee2d96f2f971fa4799c5f79b47c2747b2b57105 (diff)
downloadpysaml2-d18d58ee5ff0d8675e141a757aa3a78bb05382c4.tar.gz
Clean the logout API: there is 3 public methods now, one for initiating a logout and two for answering from a IdP. The IdP can send a logout request or can send a logout response
-rw-r--r--src/s2repoze/plugins/sp.py6
-rw-r--r--src/saml2/client.py536
-rw-r--r--tests/test_51_client.py18
3 files changed, 271 insertions, 289 deletions
diff --git a/src/s2repoze/plugins/sp.py b/src/s2repoze/plugins/sp.py
index 46d4afda..a0addcb3 100644
--- a/src/s2repoze/plugins/sp.py
+++ b/src/s2repoze/plugins/sp.py
@@ -344,9 +344,9 @@ class SAML2Plugin(FormPluginBase):
try:
# Evaluate the response, returns a AuthnResponse instance
try:
- authresp = self.saml_client.response(post,
- self.outstanding_queries,
- self.log)
+ authresp = self.saml_client.authn_response(post,
+ self.outstanding_queries,
+ self.log)
except Exception, excp:
if self.log:
self.log.error("Exception: %s" % (excp,))
diff --git a/src/saml2/client.py b/src/saml2/client.py
index 3b01f52d..ba759311 100644
--- a/src/saml2/client.py
+++ b/src/saml2/client.py
@@ -358,62 +358,6 @@ class Saml2Client(object):
else:
return None
- def response(self, post, outstanding, log=None, decode=True,
- asynchop=True):
- """ Deal with an AuthnResponse or LogoutResponse
-
- :param post: The reply as a dictionary
- :param outstanding: A dictionary with session IDs as keys and
- the original web request from the user before redirection
- as values.
- :param log: where loggin should go.
- :param decode: Whether the response is Base64 encoded or not
- :param asynchop: Whether the response was return over a asynchronous
- connection. SOAP for instance is synchronous
- :return: An response.AuthnResponse or response.LogoutResponse instance
- """
- # If the request contains a samlResponse, try to validate it
- try:
- saml_response = post['SAMLResponse']
- except KeyError:
- return None
-
- try:
- _ = self.config.entityid
- except KeyError:
- raise Exception("Missing entity_id specification")
-
- if log is None:
- log = self.logger
-
- reply_addr = self.service_url()
-
- resp = None
- if saml_response:
- try:
- resp = response_factory(saml_response, self.config,
- reply_addr, outstanding, log,
- debug=self.debug, decode=decode,
- asynchop=asynchop,
- allow_unsolicited=self.allow_unsolicited)
- except Exception, exc:
- if log:
- log.error("%s" % exc)
- return None
- if log:
- log.debug(">> %s", resp)
-
- resp = resp.verify()
- if isinstance(resp, AuthnResponse):
- self.users.add_information_about_person(resp.session_info())
- if log:
- log.info("--- ADDED person info ----")
- elif isinstance(resp, LogoutResponse):
- self.handle_logout_response(resp, log)
- elif log:
- log.error("Response type not supported: %s" % saml2.class_name(resp))
- return resp
-
def authenticate(self, entityid=None, relay_state="",
binding=saml2.BINDING_HTTP_REDIRECT,
log=None, vorg="", scoping=None, sign=None):
@@ -457,155 +401,64 @@ class Saml2Client(object):
raise Exception("Unkown binding type: %s" % binding)
return session_id, response
- def create_attribute_query(self, session_id, subject_id, destination,
- issuer_id=None, attribute=None, sp_name_qualifier=None,
- name_qualifier=None, nameid_format=None, sign=False):
- """ Constructs an AttributeQuery
-
- :param session_id: The identifier of the session
- :param subject_id: The identifier of the subject
- :param destination: To whom the query should be sent
- :param issuer_id: Identifier of the issuer
- :param attribute: A dictionary of attributes and values that is
- asked for. The key are one of 4 variants:
- 3-tuple of name_format,name and friendly_name,
- 2-tuple of name_format and name,
- 1-tuple with name or
- just the name as a string.
- :param sp_name_qualifier: The unique identifier of the
- service provider or affiliation of providers for whom the
- identifier was generated.
- :param name_qualifier: The unique identifier of the identity
- provider that generated the identifier.
- :param nameid_format: The format of the name ID
- :param sign: Whether the query should be signed or not.
- :return: An AttributeQuery instance
- """
-
-
- subject = saml.Subject(
- name_id = saml.NameID(
- text=subject_id,
- format=nameid_format,
- sp_name_qualifier=sp_name_qualifier,
- name_qualifier=name_qualifier),
- )
-
- query = samlp.AttributeQuery(
- id=session_id,
- version=VERSION,
- issue_instant=instant(),
- destination=destination,
- issuer=self._issuer(issuer_id),
- subject=subject,
- )
-
- if sign:
- query.signature = pre_signature_part(query.id, self.sec.my_cert, 1)
-
- if attribute:
- query.attribute = do_attributes(attribute)
-
- if sign:
- signed_query = self.sec.sign_attribute_query_using_xmlsec(
- "%s" % query)
- return samlp.attribute_query_from_string(signed_query)
- else:
- return query
-
-
- def attribute_query(self, subject_id, destination, issuer_id=None,
- attribute=None, sp_name_qualifier=None, name_qualifier=None,
- nameid_format=None, log=None, real_id=None):
- """ Does a attribute request to an attribute authority, this is
- by default done over SOAP. Other bindings could be used but not
- supported right now.
-
- :param subject_id: The identifier of the subject
- :param destination: To whom the query should be sent
- :param issuer_id: Who is sending this query
- :param attribute: A dictionary of attributes and values that is asked for
- :param sp_name_qualifier: The unique identifier of the
- service provider or affiliation of providers for whom the
- identifier was generated.
- :param name_qualifier: The unique identifier of the identity
- provider that generated the identifier.
- :param nameid_format: The format of the name ID
- :param log: Function to use for logging
- :param real_id: The identifier which is the key to this entity in the
- identity database
- :return: The attributes returned
+ def authn_response(self, post, outstanding, log=None, decode=True,
+ asynchop=True):
+ """ Deal with an AuthnResponse
+
+ :param post: The reply as a dictionary
+ :param outstanding: A dictionary with session IDs as keys and
+ the original web request from the user before redirection
+ as values.
+ :param log: where loggin should go.
+ :param decode: Whether the response is Base64 encoded or not
+ :param asynchop: Whether the response was return over a asynchronous
+ connection. SOAP for instance is synchronous
+ :return: An response.AuthnResponse instance
"""
+ # If the request contains a samlResponse, try to validate it
+ try:
+ saml_response = post['SAMLResponse']
+ except KeyError:
+ return None
+
+ try:
+ _ = self.config.entityid
+ except KeyError:
+ raise Exception("Missing entity_id specification")
if log is None:
log = self.logger
- session_id = sid()
- issuer = self._issuer(issuer_id)
-
- request = self.create_attribute_query(session_id, subject_id,
- destination, issuer, attribute, sp_name_qualifier,
- name_qualifier, nameid_format=nameid_format)
-
- if log:
- log.info("Request, created: %s" % request)
-
- soapclient = SOAPClient(destination, self.config.key_file,
- self.config.cert_file,
- ca_certs=self.config.ca_certs)
- if log:
- log.info("SOAP client initiated")
+ reply_addr = self.service_url()
- try:
- response = soapclient.send(request)
- except Exception, exc:
- if log:
- log.info("SoapClient exception: %s" % (exc,))
- return None
-
- if log:
- log.info("SOAP request sent and got response: %s" % response)
-# fil = open("response.xml", "w")
-# fil.write(response)
-# fil.close()
-
- if response:
- if log:
- log.info("Verifying response")
-
+ resp = None
+ if saml_response:
try:
- # synchronous operation
- aresp = attribute_response(self.config, issuer, log=log)
+ resp = response_factory(saml_response, self.config,
+ reply_addr, outstanding, log,
+ debug=self.debug, decode=decode,
+ asynchop=asynchop,
+ allow_unsolicited=self.allow_unsolicited)
except Exception, exc:
if log:
- log.error("%s", (exc,))
- return None
-
- _resp = aresp.loads(response, False, soapclient.response).verify()
- if _resp is None:
- if log:
- log.error("Didn't like the response")
+ log.error("%s" % exc)
return None
-
- session_info = _resp.session_info()
- if session_info:
- if real_id is not None:
- session_info["name_id"] = real_id
- self.users.add_information_about_person(session_info)
-
if log:
- log.info("session: %s" % session_info)
- return session_info
- else:
+ log.debug(">> %s", resp)
+
+ resp = resp.verify()
+
+ self.users.add_information_about_person(resp.session_info())
if log:
- log.info("No response")
- return None
-
- def global_logout(self, subject_id, reason="", expire=None, # ***
- sign=None, log=None, return_to="/"):
+ log.info("--- ADDED person info ----")
+
+ return resp
+
+ def logout(self, subject_id, reason="", expire=None,
+ sign=None, log=None, return_to="/"):
"""Creates a Logout Request.
-
+
:param subject_id: The identifier of the subject that wants to be
logged out.
:param reason: Why the subject wants to log out
@@ -618,7 +471,7 @@ class Saml2Client(object):
:return: Depends on which binding is used:
If the HTTP redirect binding then a HTTP redirect,
if SOAP binding has been used the just the result of that
- conversation.
+ conversation.
"""
if log is None:
log = self.logger
@@ -634,7 +487,7 @@ class Saml2Client(object):
# Do the local logout anyway
self.users.remove_person(subject_id)
return 0, "504 Gateway Timeout", [], []
-
+
# for all where I can use the SOAP binding, do those first
not_done = entity_ids[:]
response = False
@@ -652,12 +505,12 @@ class Saml2Client(object):
continue
destination = destinations[0]
-
+
if log:
log.info("destination to provider: %s" % destination)
request = self._logout_request(subject_id, destination,
entity_id, reason, expire)
-
+
to_sign = []
#if sign and binding != BINDING_HTTP_REDIRECT:
@@ -668,14 +521,14 @@ class Saml2Client(object):
request.signature = pre_signature_part(request.id,
self.sec.my_cert, 1)
to_sign = [(class_name(request), request.id)]
-
+
if log:
log.info("REQUEST: %s" % request)
request = signed_instance_factory(request, self.sec, to_sign)
-
+
if binding == BINDING_SOAP:
- response = send_using_soap(request, destination,
+ response = send_using_soap(request, destination,
self.config.key_file,
self.config.cert_file,
log=log,
@@ -706,70 +559,38 @@ class Saml2Client(object):
"not_on_of_after": expire,
"sign": sign,
"return_to": return_to}
-
+
if binding == BINDING_HTTP_POST:
- (head, body) = http_post_message(request,
- destination,
- rstate)
+ (head, body) = http_post_message(request,
+ destination,
+ rstate)
code = "200 OK"
else:
- (head, body) = http_redirect_message(request,
- destination,
- rstate)
+ (head, body) = http_redirect_message(request,
+ destination,
+ rstate)
code = "302 Found"
-
+
return session_id, code, head, body
-
+
if not_done:
# upstream should try later
raise LogoutError("%s" % (entity_ids,))
-
- return 0, "", [], response
- def handle_logout_response(self, response, log):
- """ handles a Logout response
-
- :param response: A response.Response instance
- :param log: A logging function
- :return: 4-tuple of (session_id of the last sent logout request,
- response message, response headers and message)
- """
- if log is None:
- log = self.logger
+ return 0, "", [], response
- if log:
- log.info("state: %s" % (self.state,))
- status = self.state[response.in_response_to]
- if log:
- log.info("status: %s" % (status,))
- issuer = response.issuer()
- if log:
- log.info("issuer: %s" % issuer)
- del self.state[response.in_response_to]
- if status["entity_ids"] == [issuer]: # done
- self.users.remove_person(status["subject_id"])
- return 0, "200 Ok", [("Content-type","text/html")], []
- else:
- status["entity_ids"].remove(issuer)
- return self.global_logout(status["subject_id"],
- status["entity_ids"],
- status["reason"],
- status["not_on_or_after"],
- status["sign"],
- log, )
-
- def logout_response(self, xmlstr, log=None, binding=BINDING_SOAP): # ***
+ def logout_response(self, xmlstr, log=None, binding=BINDING_SOAP):
""" Deal with a LogoutResponse
:param xmlstr: The response as a xml string
:param log: logging function
:param binding: What type of binding this message came through.
:return: None if the reply doesn't contain a valid SAML LogoutResponse,
- otherwise the reponse if the logout was successful and None if it
+ otherwise the reponse if the logout was successful and None if it
was not.
"""
-
+
response = None
if log is None:
log = self.logger
@@ -783,7 +604,7 @@ class Saml2Client(object):
if log:
log.info("Not supposed to handle this!")
return None
-
+
try:
response = LogoutResponse(self.sec, return_addr,
debug=self.debug, log=log)
@@ -791,7 +612,7 @@ class Saml2Client(object):
if log:
log.info("%s" % exc)
return None
-
+
if binding == BINDING_HTTP_REDIRECT:
xmlstr = decode_base64_and_inflate(xmlstr)
elif binding == BINDING_HTTP_POST:
@@ -804,44 +625,73 @@ class Saml2Client(object):
if response:
response = response.verify()
-
+
if not response:
return None
-
+
if log:
log.debug(response)
- return self.handle_logout_response(response, log)
+ status = self.state[response.in_response_to]
+ issuer = response.issuer()
+
+ if log:
+ log.info("state: %s" % (self.state,))
+ log.info("status: %s" % (status,))
+ log.info("issuer: %s" % issuer)
+
+ del self.state[response.in_response_to]
+
+ if status["entity_ids"] == [issuer]: # done
+ self.users.remove_person(status["subject_id"])
+ return 0, "200 Ok", [("Content-type","text/html")], []
+ else:
+ status["entity_ids"].remove(issuer)
+ return self.logout(status["subject_id"],
+ status["reason"],
+ status["not_on_or_after"],
+ status["sign"],
+ log,
+ status['return_to'])
return response
- def http_redirect_logout_request(self, get, subject_id, log=None):
- """ Deal with a LogoutRequest received through HTTP redirect
+ def logout_request(self, request, subject_id, log=None,
+ binding=BINDING_HTTP_REDIRECT):
+ """ Deal with a LogoutRequest
- :param get: The request as a dictionary
+ :param request: The request. The format depends on which binding is
+ used.
:param subject_id: the id of the current logged user
- :return: a tuple with a list of header tuples (presently only location)
- and a status which will be True in case of success or False
- otherwise.
+ :return: What is returned also depends on which binding is used.
"""
+ if log is None:
+ log = self.logger
+
+ if binding != BINDING_HTTP_REDIRECT:
+ if log:
+ log.error("Sorry, only HTTP_REDIRECT is supported for logout")
+
+ return
+
headers = []
success = False
if log is None:
log = self.logger
try:
- saml_request = get['SAMLRequest']
+ saml_request = request['SAMLRequest']
except KeyError:
return None
if saml_request:
xml = decode_base64_and_inflate(saml_request)
- request = samlp.logout_request_from_string(xml)
+ logout_request = samlp.logout_request_from_string(xml)
if log:
- log.debug(request)
+ log.debug(logout_request)
- if request.name_id.text == subject_id:
+ if logout_request.name_id.text == subject_id:
status = samlp.STATUS_SUCCESS
self.users.remove_person(subject_id)
success = True
@@ -849,45 +699,175 @@ class Saml2Client(object):
status = samlp.STATUS_REQUEST_DENIED
response, destination = self._logout_response(
- request.issuer.text,
- request.id,
+ logout_request.issuer.text,
+ logout_request.id,
status
)
if log:
log.info("RESPONSE: {0:>s}".format(response))
- if 'RelayState' in get:
- rstate = get['RelayState']
+ if 'RelayState' in request:
+ rstate = request['RelayState']
else:
rstate = ""
-
- (headers, _body) = http_redirect_message(str(response),
- destination,
- rstate, 'SAMLResponse')
- return headers, success
+ (headers, _body) = http_redirect_message(str(response),
+ destination,
+ rstate, 'SAMLResponse')
- def logout_request(self, request, subject_id, log=None, # ***
- binding=BINDING_HTTP_REDIRECT):
- """ Deal with a LogoutRequest
+ return headers, success
- :param request: The request. The format depends on which binding is
- used.
- :param subject_id: the id of the current logged user
- :return: What is returned also depends on which binding is used.
+ def create_attribute_query(self, session_id, subject_id, destination,
+ issuer_id=None, attribute=None, sp_name_qualifier=None,
+ name_qualifier=None, nameid_format=None, sign=False):
+ """ Constructs an AttributeQuery
+
+ :param session_id: The identifier of the session
+ :param subject_id: The identifier of the subject
+ :param destination: To whom the query should be sent
+ :param issuer_id: Identifier of the issuer
+ :param attribute: A dictionary of attributes and values that is
+ asked for. The key are one of 4 variants:
+ 3-tuple of name_format,name and friendly_name,
+ 2-tuple of name_format and name,
+ 1-tuple with name or
+ just the name as a string.
+ :param sp_name_qualifier: The unique identifier of the
+ service provider or affiliation of providers for whom the
+ identifier was generated.
+ :param name_qualifier: The unique identifier of the identity
+ provider that generated the identifier.
+ :param nameid_format: The format of the name ID
+ :param sign: Whether the query should be signed or not.
+ :return: An AttributeQuery instance
+ """
+
+
+ subject = saml.Subject(
+ name_id = saml.NameID(
+ text=subject_id,
+ format=nameid_format,
+ sp_name_qualifier=sp_name_qualifier,
+ name_qualifier=name_qualifier),
+ )
+
+ query = samlp.AttributeQuery(
+ id=session_id,
+ version=VERSION,
+ issue_instant=instant(),
+ destination=destination,
+ issuer=self._issuer(issuer_id),
+ subject=subject,
+ )
+
+ if sign:
+ query.signature = pre_signature_part(query.id, self.sec.my_cert, 1)
+
+ if attribute:
+ query.attribute = do_attributes(attribute)
+
+ if sign:
+ signed_query = self.sec.sign_attribute_query_using_xmlsec(
+ "%s" % query)
+ return samlp.attribute_query_from_string(signed_query)
+ else:
+ return query
+
+
+ def attribute_query(self, subject_id, destination, issuer_id=None,
+ attribute=None, sp_name_qualifier=None, name_qualifier=None,
+ nameid_format=None, log=None, real_id=None):
+ """ Does a attribute request to an attribute authority, this is
+ by default done over SOAP. Other bindings could be used but not
+ supported right now.
+
+ :param subject_id: The identifier of the subject
+ :param destination: To whom the query should be sent
+ :param issuer_id: Who is sending this query
+ :param attribute: A dictionary of attributes and values that is asked for
+ :param sp_name_qualifier: The unique identifier of the
+ service provider or affiliation of providers for whom the
+ identifier was generated.
+ :param name_qualifier: The unique identifier of the identity
+ provider that generated the identifier.
+ :param nameid_format: The format of the name ID
+ :param log: Function to use for logging
+ :param real_id: The identifier which is the key to this entity in the
+ identity database
+ :return: The attributes returned
"""
+
if log is None:
log = self.logger
- if binding == BINDING_HTTP_REDIRECT:
- return self.http_redirect_logout_request(request, subject_id, log)
+ session_id = sid()
+ issuer = self._issuer(issuer_id)
+
+ request = self.create_attribute_query(session_id, subject_id,
+ destination, issuer, attribute, sp_name_qualifier,
+ name_qualifier, nameid_format=nameid_format)
+
+ if log:
+ log.info("Request, created: %s" % request)
+
+ soapclient = SOAPClient(destination, self.config.key_file,
+ self.config.cert_file,
+ ca_certs=self.config.ca_certs)
+ if log:
+ log.info("SOAP client initiated")
+
+ try:
+ response = soapclient.send(request)
+ except Exception, exc:
+ if log:
+ log.info("SoapClient exception: %s" % (exc,))
+ return None
+
+ if log:
+ log.info("SOAP request sent and got response: %s" % response)
+# fil = open("response.xml", "w")
+# fil.write(response)
+# fil.close()
+
+ if response:
+ if log:
+ log.info("Verifying response")
+
+ try:
+ # synchronous operation
+ aresp = attribute_response(self.config, issuer, log=log)
+ except Exception, exc:
+ if log:
+ log.error("%s", (exc,))
+ return None
+
+ _resp = aresp.loads(response, False, soapclient.response).verify()
+ if _resp is None:
+ if log:
+ log.error("Didn't like the response")
+ return None
+
+ session_info = _resp.session_info()
+
+ if session_info:
+ if real_id is not None:
+ session_info["name_id"] = real_id
+ self.users.add_information_about_person(session_info)
+
+ if log:
+ log.info("session: %s" % session_info)
+ return session_info
+ else:
+ if log:
+ log.info("No response")
+ return None
def add_vo_information_about_user(self, subject_id):
""" Add information to the knowledge I have about the user. This is
for Virtual organizations.
-
- :param subject_id: The subject identifier
+
+ :param subject_id: The subject identifier
:return: A possibly extended knowledge.
"""
diff --git a/tests/test_51_client.py b/tests/test_51_client.py
index 4e54bbe9..8c66362f 100644
--- a/tests/test_51_client.py
+++ b/tests/test_51_client.py
@@ -262,8 +262,10 @@ class TestClient:
resp_str = base64.encodestring(resp_str)
- authn_response = self.client.response({"SAMLResponse":resp_str},
- {"id1":"http://foo.example.com/service"})
+ authn_response = self.client.authn_response(
+ {"SAMLResponse":resp_str},
+ {"id1":"http://foo.example.com/service"}
+ )
assert authn_response is not None
assert authn_response.issuer() == IDP
@@ -300,8 +302,8 @@ class TestClient:
resp_str = base64.encodestring(resp_str)
- self.client.response({"SAMLResponse":resp_str},
- {"id2":"http://foo.example.com/service"})
+ self.client.authn_response({"SAMLResponse":resp_str},
+ {"id2":"http://foo.example.com/service"})
# Two persons in the cache
assert len(self.client.users.subjects()) == 2
@@ -383,7 +385,7 @@ class TestClient:
self.client.users.add_information_about_person(session_info)
entity_ids = self.client.users.issuers_of_info("123456")
assert entity_ids == ["urn:mace:example.com:saml:roland:idp"]
- resp = self.client.global_logout("123456", "Tired", in_a_while(minutes=5))
+ resp = self.client.logout("123456", "Tired", in_a_while(minutes=5))
print resp
assert resp
assert resp[0] # a session_id
@@ -427,7 +429,7 @@ class TestClient:
assert destinations == ['http://localhost:8088/slo']
# Will raise an error since there is noone at the other end.
- raises(LogoutError, 'client.global_logout("123456", "Tired", in_a_while(minutes=5))')
+ raises(LogoutError, 'client.logout("123456", "Tired", in_a_while(minutes=5))')
def test_logout_3(self):
""" two or more IdP/AA with BINDING_HTTP_REDIRECT"""
@@ -460,7 +462,7 @@ class TestClient:
entity_ids = client.users.issuers_of_info("123456")
assert _leq(entity_ids, ["urn:mace:example.com:saml:roland:idp",
"urn:mace:example.com:saml:roland:aa"])
- resp = client.global_logout("123456", "Tired", in_a_while(minutes=5))
+ resp = client.logout("123456", "Tired", in_a_while(minutes=5))
print resp
assert resp
assert resp[0] # a session_id
@@ -571,7 +573,7 @@ class TestClient:
resp_str = base64.encodestring(resp_str)
self.client.allow_unsolicited = True
- authn_response = self.client.response({"SAMLResponse":resp_str}, ())
+ authn_response = self.client.authn_response({"SAMLResponse":resp_str}, ())
assert authn_response is not None
assert authn_response.issuer() == IDP