Merge pull request #44 from sigmunau/master
initial support for single logout in s2repoze
This commit is contained in:
2
setup.py
2
setup.py
@@ -42,7 +42,7 @@ install_requires = [
|
||||
'requests >= 1.0.0',
|
||||
'paste',
|
||||
'zope.interface',
|
||||
'repoze.who == 1.0.18',
|
||||
'repoze.who >= 1.0.18',
|
||||
'm2crypto'
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user