Skip to content

Commit f3ec1cd

Browse files
committed
Implement initiating logout from s2repoze plugin.
Adds a new repoze config option path_logout to the challenge decider plugin. When a request is received on this url a global logout request is initiated. Responses to this request is received on the single_logout_service endpoint of the sp as configured in saml config of the sp. Tested with only on IdP and only using HTTP_REDIRECT bindings TODO: handle receiving logout requests on the single_logout_service endpoint
1 parent 5ae327b commit f3ec1cd

File tree

3 files changed

+73
-22
lines changed

3 files changed

+73
-22
lines changed

src/s2repoze/plugins/challenge_decider.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,25 +54,31 @@ def my_request_classifier(environ):
5454
zope.interface.directlyProvides(my_request_classifier, IRequestClassifier)
5555

5656
class MyChallengeDecider:
57-
def __init__(self, path_login=""):
57+
def __init__(self, path_login="", path_logout=""):
5858
self.path_login = path_login
59+
self.path_logout = path_logout
5960
def __call__(self, environ, status, _headers):
6061
if status.startswith('401 '):
6162
return True
6263
else:
63-
# logout : need to "forget" => require a peculiar challenge
64-
if environ.has_key('rwpc.logout'):
64+
if environ.has_key('samlsp.pending'):
6565
return True
6666

67+
uri = environ.get('REQUEST_URI', None)
68+
if uri is None:
69+
uri = construct_url(environ)
70+
71+
# require and challenge for logout and inform the challenge plugin that it is a logout we want
72+
for regex in self.path_logout:
73+
if regex.match(uri) is not None:
74+
environ['samlsp.logout'] = True
75+
return True
76+
6777
# If the user is already authent, whatever happens(except logout),
6878
# don't make a challenge
6979
if environ.has_key('repoze.who.identity'):
7080
return False
7181

72-
uri = environ.get('REQUEST_URI', None)
73-
if uri is None:
74-
uri = construct_url(environ)
75-
7682
# require a challenge for login
7783
for regex in self.path_login:
7884
if regex.match(uri) is not None:
@@ -82,7 +88,7 @@ def __call__(self, environ, status, _headers):
8288

8389

8490

85-
def make_plugin(path_login = None):
91+
def make_plugin(path_login = None, path_logout = None):
8692
if path_login is None:
8793
raise ValueError(
8894
'must include path_login in configuration')
@@ -94,7 +100,14 @@ def make_plugin(path_login = None):
94100
if carg != '':
95101
list_login.append(re.compile(carg))
96102

97-
plugin = MyChallengeDecider(list_login)
103+
list_logout = []
104+
if path_logout is not None:
105+
for arg in path_logout.splitlines():
106+
carg = arg.lstrip()
107+
if carg != '':
108+
list_logout.append(re.compile(carg))
109+
110+
plugin = MyChallengeDecider(list_login, list_logout)
98111

99112
return plugin
100113

src/s2repoze/plugins/sp.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
import platform
2525
import shelve
2626
import traceback
27-
from urlparse import parse_qs
27+
from urlparse import parse_qs, urlparse
2828

29-
from paste.httpexceptions import HTTPSeeOther
29+
from paste.httpexceptions import HTTPSeeOther, HTTPRedirection
3030
from paste.httpexceptions import HTTPNotImplemented
3131
from paste.httpexceptions import HTTPInternalServerError
3232
from paste.request import parse_dict_querystring
@@ -133,6 +133,7 @@ def __init__(self, rememberer_name, config, saml_client, wayf, cache,
133133
self.cache = cache
134134
self.discosrv = discovery
135135
self.idp_query_param = idp_query_param
136+
self.logout_endpoints = [urlparse(ep)[2] for ep in config.endpoint("single_logout_service")]
136137

137138
try:
138139
self.metadata = self.conf.metadata
@@ -282,10 +283,22 @@ def challenge(self, environ, _status, _app_headers, _forget_headers):
282283

283284
_cli = self.saml_client
284285

285-
# this challenge consist in logging out
286-
if 'rwpc.logout' in environ:
287-
# ignore right now?
288-
pass
286+
287+
if 'REMOTE_USER' in environ:
288+
name_id = decode(environ["REMOTE_USER"])
289+
290+
_cli = self.saml_client
291+
path_info = environ['PATH_INFO']
292+
293+
if 'samlsp.logout' in environ:
294+
responses = _cli.global_logout(name_id)
295+
return self._handle_logout(responses)
296+
297+
if 'samlsp.pending' in environ:
298+
response = environ['samlsp.pending']
299+
if isinstance(response, HTTPRedirection):
300+
response.headers += _forget_headers
301+
return response
289302

290303
#logger = environ.get('repoze.who.logger','')
291304

@@ -405,7 +418,8 @@ def identify(self, environ):
405418
"""
406419
#logger = environ.get('repoze.who.logger', '')
407420

408-
if "CONTENT_LENGTH" not in environ or not environ["CONTENT_LENGTH"]:
421+
query = parse_dict_querystring(environ)
422+
if ("CONTENT_LENGTH" not in environ or not environ["CONTENT_LENGTH"]) and "SAMLResponse" not in query:
409423
logger.debug('[identify] get or empty post')
410424
return {}
411425

@@ -443,10 +457,28 @@ def identify(self, environ):
443457
logger.info("[sp.identify] --- SAMLResponse ---")
444458
# check for SAML2 authN response
445459
#if self.debug:
460+
path_info = environ['PATH_INFO']
461+
logout = False
462+
if path_info in self.logout_endpoints:
463+
logout = True
446464
try:
447-
session_info = self._eval_authn_response(
448-
environ, cgi_field_storage_to_dict(post),
449-
binding=binding)
465+
if logout:
466+
response = self.saml_client.parse_logout_request_response(post["SAMLResponse"], binding)
467+
if response:
468+
action = self.saml_client.handle_logout_response(response)
469+
request = None
470+
if type(action) == dict:
471+
request = self._handle_logout(action)
472+
else:
473+
#logout complete
474+
request = HTTPSeeOther(headers=[('Location', "/")])
475+
if request:
476+
environ['samlsp.pending'] = request
477+
return {}
478+
else:
479+
session_info = self._eval_authn_response(
480+
environ, cgi_field_storage_to_dict(post),
481+
binding=binding)
450482
except Exception, err:
451483
environ["s2repoze.saml_error"] = err
452484
return {}
@@ -532,6 +564,13 @@ def authenticate(self, environ, identity=None):
532564
else:
533565
return None
534566

567+
def _handle_logout(self, responses):
568+
ht_args = responses[responses.keys()[0]][1]
569+
if not ht_args["data"] and ht_args["headers"][0][0] == "Location":
570+
logger.debug('redirect to: %s' % ht_args["headers"][0][1])
571+
return HTTPSeeOther(headers=ht_args["headers"])
572+
else:
573+
return ht_args["data"]
535574

536575
def make_plugin(remember_name=None, # plugin for remember
537576
cache="", # cache

src/saml2/client.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ def global_logout(self, name_id, reason="", expire=None, sign=None):
113113

114114
# find out which IdPs/AAs I should notify
115115
entity_ids = self.users.issuers_of_info(name_id)
116-
self.users.remove_person(name_id)
117116
return self.do_logout(name_id, entity_ids, reason, expire, sign)
118117

119118
def do_logout(self, name_id, entity_ids, reason, expire, sign=None):
@@ -232,11 +231,11 @@ def handle_logout_response(self, response):
232231
logger.info("issuer: %s" % issuer)
233232
del self.state[response.in_response_to]
234233
if status["entity_ids"] == [issuer]: # done
235-
self.local_logout(status["subject_id"])
234+
self.local_logout(status["name_id"])
236235
return 0, "200 Ok", [("Content-type", "text/html")], []
237236
else:
238237
status["entity_ids"].remove(issuer)
239-
return self.do_logout(status["subject_id"], status["entity_ids"],
238+
return self.do_logout(status["name_id"], status["entity_ids"],
240239
status["reason"], status["not_on_or_after"],
241240
status["sign"])
242241

0 commit comments

Comments
 (0)