Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def run_tests(self):
'requests >= 1.0.0',
'paste',
'zope.interface',
'repoze.who == 1.0.18',
'repoze.who >= 1.0.18',
'm2crypto'
]

Expand Down
31 changes: 22 additions & 9 deletions src/s2repoze/plugins/challenge_decider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -82,7 +88,7 @@ def __call__(self, environ, status, _headers):



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')
Expand All @@ -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

79 changes: 67 additions & 12 deletions src/s2repoze/plugins/sp.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
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
Expand Down Expand Up @@ -133,6 +133,7 @@ def __init__(self, rememberer_name, config, saml_client, wayf, cache,
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
Expand Down Expand Up @@ -282,10 +283,22 @@ def challenge(self, environ, _status, _app_headers, _forget_headers):

_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','')

Expand Down Expand Up @@ -405,7 +418,8 @@ def identify(self, environ):
"""
#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 {}

Expand All @@ -420,7 +434,7 @@ def identify(self, environ):
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:
Expand All @@ -433,7 +447,21 @@ def identify(self, environ):
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
Expand All @@ -444,9 +472,23 @@ def identify(self, environ):
# 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 {}
Expand Down Expand Up @@ -528,10 +570,23 @@ def _service_url(self, environ, qstr=None):
#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
Expand Down
13 changes: 10 additions & 3 deletions src/saml2/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ def global_logout(self, name_id, reason="", expire=None, sign=None):

# 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):
Expand Down Expand Up @@ -217,6 +216,14 @@ def local_logout(self, name_id):
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

Expand All @@ -232,11 +239,11 @@ def handle_logout_response(self, response):
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"])

Expand Down