Skip to content

Commit dfcfda5

Browse files
author
Roland Hedberg
committed
Merge pull request #44 from sigmunau/master
initial support for single logout in s2repoze
2 parents 5332811 + 523dfbb commit dfcfda5

File tree

4 files changed

+100
-25
lines changed

4 files changed

+100
-25
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def run_tests(self):
4242
'requests >= 1.0.0',
4343
'paste',
4444
'zope.interface',
45-
'repoze.who == 1.0.18',
45+
'repoze.who >= 1.0.18',
4646
'm2crypto'
4747
]
4848

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: 67 additions & 12 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 and "SAMLRequest" not in query:
409423
logger.debug('[identify] get or empty post')
410424
return {}
411425

@@ -420,7 +434,7 @@ def identify(self, environ):
420434
query = parse_dict_querystring(environ)
421435
logger.debug('[sp.identify] query: %s' % (query,))
422436

423-
if "SAMLResponse" in query:
437+
if "SAMLResponse" in query or "SAMLRequest" in query:
424438
post = query
425439
binding = BINDING_HTTP_REDIRECT
426440
else:
@@ -433,7 +447,21 @@ def identify(self, environ):
433447
pass
434448

435449
try:
436-
if "SAMLResponse" not in post:
450+
path_info = environ['PATH_INFO']
451+
logout = False
452+
if path_info in self.logout_endpoints:
453+
logout = True
454+
455+
if logout and "SAMLRequest" in post:
456+
print("logout request received")
457+
try:
458+
response = self.saml_client.handle_logout_request(post["SAMLRequest"], self.saml_client.users.subjects()[0], binding)
459+
environ['samlsp.pending'] = self._handle_logout(response)
460+
return {}
461+
except:
462+
import traceback
463+
traceback.print_exc()
464+
elif "SAMLResponse" not in post:
437465
logger.info("[sp.identify] --- NOT SAMLResponse ---")
438466
# Not for me, put the post back where next in line can
439467
# find it
@@ -444,9 +472,23 @@ def identify(self, environ):
444472
# check for SAML2 authN response
445473
#if self.debug:
446474
try:
447-
session_info = self._eval_authn_response(
448-
environ, cgi_field_storage_to_dict(post),
449-
binding=binding)
475+
if logout:
476+
response = self.saml_client.parse_logout_request_response(post["SAMLResponse"], binding)
477+
if response:
478+
action = self.saml_client.handle_logout_response(response)
479+
request = None
480+
if type(action) == dict:
481+
request = self._handle_logout(action)
482+
else:
483+
#logout complete
484+
request = HTTPSeeOther(headers=[('Location', "/")])
485+
if request:
486+
environ['samlsp.pending'] = request
487+
return {}
488+
else:
489+
session_info = self._eval_authn_response(
490+
environ, cgi_field_storage_to_dict(post),
491+
binding=binding)
450492
except Exception, err:
451493
environ["s2repoze.saml_error"] = err
452494
return {}
@@ -528,10 +570,23 @@ def _service_url(self, environ, qstr=None):
528570
#noinspection PyUnusedLocal
529571
def authenticate(self, environ, identity=None):
530572
if identity:
573+
tktuser = identity.get('repoze.who.plugins.auth_tkt.userid', None)
574+
if tktuser and self.saml_client.is_logged_in(decode(tktuser)):
575+
return tktuser
531576
return identity.get('login', None)
532577
else:
533578
return None
534579

580+
def _handle_logout(self, responses):
581+
if 'data' in responses:
582+
ht_args = responses
583+
else:
584+
ht_args = responses[responses.keys()[0]][1]
585+
if not ht_args["data"] and ht_args["headers"][0][0] == "Location":
586+
logger.debug('redirect to: %s' % ht_args["headers"][0][1])
587+
return HTTPSeeOther(headers=ht_args["headers"])
588+
else:
589+
return ht_args["data"]
535590

536591
def make_plugin(remember_name=None, # plugin for remember
537592
cache="", # cache

src/saml2/client.py

Lines changed: 10 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):
@@ -217,6 +216,14 @@ def local_logout(self, name_id):
217216
self.users.remove_person(name_id)
218217
return True
219218

219+
def is_logged_in(self, name_id):
220+
""" Check if user is in the cache
221+
222+
:param name_id: The identifier of the subject
223+
"""
224+
identity = self.users.get_identity(name_id)[0]
225+
return bool(identity)
226+
220227
def handle_logout_response(self, response):
221228
""" handles a Logout response
222229
@@ -232,11 +239,11 @@ def handle_logout_response(self, response):
232239
logger.info("issuer: %s" % issuer)
233240
del self.state[response.in_response_to]
234241
if status["entity_ids"] == [issuer]: # done
235-
self.local_logout(status["subject_id"])
242+
self.local_logout(status["name_id"])
236243
return 0, "200 Ok", [("Content-type", "text/html")], []
237244
else:
238245
status["entity_ids"].remove(issuer)
239-
return self.do_logout(status["subject_id"], status["entity_ids"],
246+
return self.do_logout(status["name_id"], status["entity_ids"],
240247
status["reason"], status["not_on_or_after"],
241248
status["sign"])
242249

0 commit comments

Comments
 (0)