Merge pull request #44 from sigmunau/master

initial support for single logout in s2repoze
This commit is contained in:
Roland Hedberg
2013-06-22 01:21:19 -07:00
4 changed files with 100 additions and 25 deletions

View File

@@ -42,7 +42,7 @@ install_requires = [
'requests >= 1.0.0',
'paste',
'zope.interface',
'repoze.who == 1.0.18',
'repoze.who >= 1.0.18',
'm2crypto'
]

View File

@@ -54,25 +54,31 @@ def my_request_classifier(environ):
zope.interface.directlyProvides(my_request_classifier, IRequestClassifier)
class MyChallengeDecider:
def __init__(self, path_login=""):
def __init__(self, path_login="", path_logout=""):
self.path_login = path_login
self.path_logout = path_logout
def __call__(self, environ, status, _headers):
if status.startswith('401 '):
return True
else:
# logout : need to "forget" => require a peculiar challenge
if environ.has_key('rwpc.logout'):
if environ.has_key('samlsp.pending'):
return True
uri = environ.get('REQUEST_URI', None)
if uri is None:
uri = construct_url(environ)
# require and challenge for logout and inform the challenge plugin that it is a logout we want
for regex in self.path_logout:
if regex.match(uri) is not None:
environ['samlsp.logout'] = True
return True
# If the user is already authent, whatever happens(except logout),
# don't make a challenge
if environ.has_key('repoze.who.identity'):
return False
uri = environ.get('REQUEST_URI', None)
if uri is None:
uri = construct_url(environ)
# require a challenge for login
for regex in self.path_login:
if regex.match(uri) is not None:
@@ -82,7 +88,7 @@ class MyChallengeDecider:
def make_plugin(path_login = None):
def make_plugin(path_login = None, path_logout = None):
if path_login is None:
raise ValueError(
'must include path_login in configuration')
@@ -94,7 +100,14 @@ def make_plugin(path_login = None):
if carg != '':
list_login.append(re.compile(carg))
plugin = MyChallengeDecider(list_login)
list_logout = []
if path_logout is not None:
for arg in path_logout.splitlines():
carg = arg.lstrip()
if carg != '':
list_logout.append(re.compile(carg))
plugin = MyChallengeDecider(list_login, list_logout)
return plugin

View File

@@ -24,9 +24,9 @@ import sys
import platform
import shelve
import traceback
from urlparse import parse_qs
from urlparse import parse_qs, urlparse
from paste.httpexceptions import HTTPSeeOther
from paste.httpexceptions import HTTPSeeOther, HTTPRedirection
from paste.httpexceptions import HTTPNotImplemented
from paste.httpexceptions import HTTPInternalServerError
from paste.request import parse_dict_querystring
@@ -133,6 +133,7 @@ class SAML2Plugin(FormPluginBase):
self.cache = cache
self.discosrv = discovery
self.idp_query_param = idp_query_param
self.logout_endpoints = [urlparse(ep)[2] for ep in config.endpoint("single_logout_service")]
try:
self.metadata = self.conf.metadata
@@ -282,10 +283,22 @@ class SAML2Plugin(FormPluginBase):
_cli = self.saml_client
# this challenge consist in logging out
if 'rwpc.logout' in environ:
# ignore right now?
pass
if 'REMOTE_USER' in environ:
name_id = decode(environ["REMOTE_USER"])
_cli = self.saml_client
path_info = environ['PATH_INFO']
if 'samlsp.logout' in environ:
responses = _cli.global_logout(name_id)
return self._handle_logout(responses)
if 'samlsp.pending' in environ:
response = environ['samlsp.pending']
if isinstance(response, HTTPRedirection):
response.headers += _forget_headers
return response
#logger = environ.get('repoze.who.logger','')
@@ -405,7 +418,8 @@ class SAML2Plugin(FormPluginBase):
"""
#logger = environ.get('repoze.who.logger', '')
if "CONTENT_LENGTH" not in environ or not environ["CONTENT_LENGTH"]:
query = parse_dict_querystring(environ)
if ("CONTENT_LENGTH" not in environ or not environ["CONTENT_LENGTH"]) and "SAMLResponse" not in query and "SAMLRequest" not in query:
logger.debug('[identify] get or empty post')
return {}
@@ -420,7 +434,7 @@ class SAML2Plugin(FormPluginBase):
query = parse_dict_querystring(environ)
logger.debug('[sp.identify] query: %s' % (query,))
if "SAMLResponse" in query:
if "SAMLResponse" in query or "SAMLRequest" in query:
post = query
binding = BINDING_HTTP_REDIRECT
else:
@@ -433,7 +447,21 @@ class SAML2Plugin(FormPluginBase):
pass
try:
if "SAMLResponse" not in post:
path_info = environ['PATH_INFO']
logout = False
if path_info in self.logout_endpoints:
logout = True
if logout and "SAMLRequest" in post:
print("logout request received")
try:
response = self.saml_client.handle_logout_request(post["SAMLRequest"], self.saml_client.users.subjects()[0], binding)
environ['samlsp.pending'] = self._handle_logout(response)
return {}
except:
import traceback
traceback.print_exc()
elif "SAMLResponse" not in post:
logger.info("[sp.identify] --- NOT SAMLResponse ---")
# Not for me, put the post back where next in line can
# find it
@@ -444,9 +472,23 @@ class SAML2Plugin(FormPluginBase):
# check for SAML2 authN response
#if self.debug:
try:
session_info = self._eval_authn_response(
environ, cgi_field_storage_to_dict(post),
binding=binding)
if logout:
response = self.saml_client.parse_logout_request_response(post["SAMLResponse"], binding)
if response:
action = self.saml_client.handle_logout_response(response)
request = None
if type(action) == dict:
request = self._handle_logout(action)
else:
#logout complete
request = HTTPSeeOther(headers=[('Location', "/")])
if request:
environ['samlsp.pending'] = request
return {}
else:
session_info = self._eval_authn_response(
environ, cgi_field_storage_to_dict(post),
binding=binding)
except Exception, err:
environ["s2repoze.saml_error"] = err
return {}
@@ -528,10 +570,23 @@ class SAML2Plugin(FormPluginBase):
#noinspection PyUnusedLocal
def authenticate(self, environ, identity=None):
if identity:
tktuser = identity.get('repoze.who.plugins.auth_tkt.userid', None)
if tktuser and self.saml_client.is_logged_in(decode(tktuser)):
return tktuser
return identity.get('login', None)
else:
return None
def _handle_logout(self, responses):
if 'data' in responses:
ht_args = responses
else:
ht_args = responses[responses.keys()[0]][1]
if not ht_args["data"] and ht_args["headers"][0][0] == "Location":
logger.debug('redirect to: %s' % ht_args["headers"][0][1])
return HTTPSeeOther(headers=ht_args["headers"])
else:
return ht_args["data"]
def make_plugin(remember_name=None, # plugin for remember
cache="", # cache

View File

@@ -113,7 +113,6 @@ class Saml2Client(Base):
# find out which IdPs/AAs I should notify
entity_ids = self.users.issuers_of_info(name_id)
self.users.remove_person(name_id)
return self.do_logout(name_id, entity_ids, reason, expire, sign)
def do_logout(self, name_id, entity_ids, reason, expire, sign=None):
@@ -217,6 +216,14 @@ class Saml2Client(Base):
self.users.remove_person(name_id)
return True
def is_logged_in(self, name_id):
""" Check if user is in the cache
:param name_id: The identifier of the subject
"""
identity = self.users.get_identity(name_id)[0]
return bool(identity)
def handle_logout_response(self, response):
""" handles a Logout response
@@ -232,11 +239,11 @@ class Saml2Client(Base):
logger.info("issuer: %s" % issuer)
del self.state[response.in_response_to]
if status["entity_ids"] == [issuer]: # done
self.local_logout(status["subject_id"])
self.local_logout(status["name_id"])
return 0, "200 Ok", [("Content-type", "text/html")], []
else:
status["entity_ids"].remove(issuer)
return self.do_logout(status["subject_id"], status["entity_ids"],
return self.do_logout(status["name_id"], status["entity_ids"],
status["reason"], status["not_on_or_after"],
status["sign"])