diff --git a/.gitignore b/.gitignore
index 54ce3bb..a8ddb96 100644
--- a/.gitignore
+++ b/.gitignore
@@ -177,3 +177,5 @@ example/sp-repoze/idp_test.xml
example/sp-repoze/sp_conf_example.py
example/idp2/idp_conf_example.py
+
+example/sp-wsgi/sp_conf.py
diff --git a/example/idp2/idp.py b/example/idp2/idp.py
index 8e6af4f..efa8e92 100755
--- a/example/idp2/idp.py
+++ b/example/idp2/idp.py
@@ -570,14 +570,13 @@ class SLO(Service):
def do(self, request, binding, relay_state="", encrypt_cert=None):
logger.info("--- Single Log Out Service ---")
try:
- _, body = request.split("\n")
- logger.debug("req: '%s'" % body)
- req_info = IDP.parse_logout_request(body, binding)
+ logger.debug("req: '%s'" % request)
+ req_info = IDP.parse_logout_request(request, binding)
except Exception as exc:
logger.error("Bad request: %s" % exc)
resp = BadRequest("%s" % exc)
return resp(self.environ, self.start_response)
-
+
msg = req_info.message
if msg.name_id:
lid = IDP.ident.find_local_id(msg.name_id)
@@ -591,14 +590,24 @@ class SLO(Service):
try:
IDP.session_db.remove_authn_statements(msg.name_id)
except KeyError as exc:
- logger.error("ServiceError: %s" % exc)
- resp = ServiceError("%s" % exc)
+ logger.error("Unknown session: %s" % exc)
+ resp = ServiceError("Unknown session: %s" % exc)
return resp(self.environ, self.start_response)
-
+
resp = IDP.create_logout_response(msg, [binding])
-
+
+ if binding == BINDING_SOAP:
+ destination = ""
+ response = False
+ else:
+ binding, destination = IDP.pick_binding("single_logout_service",
+ [binding], "spsso",
+ req_info)
+ response = True
+
try:
- hinfo = IDP.apply_binding(binding, "%s" % resp, "", relay_state)
+ hinfo = IDP.apply_binding(binding, "%s" % resp, destination, relay_state,
+ response=response)
except Exception as exc:
logger.error("ServiceError: %s" % exc)
resp = ServiceError("%s" % exc)
@@ -609,8 +618,18 @@ class SLO(Service):
if delco:
hinfo["headers"].append(delco)
logger.info("Header: %s" % (hinfo["headers"],))
- resp = Response(hinfo["data"], headers=hinfo["headers"])
- return resp(self.environ, self.start_response)
+
+ if binding == BINDING_HTTP_REDIRECT:
+ for key, value in hinfo['headers']:
+ if key.lower() == 'location':
+ resp = Redirect(value, headers=hinfo["headers"])
+ return resp(self.environ, self.start_response)
+
+ resp = ServiceError('missing Location header')
+ return resp(self.environ, self.start_response)
+ else:
+ resp = Response(hinfo["data"], headers=hinfo["headers"])
+ return resp(self.environ, self.start_response)
# ----------------------------------------------------------------------------
# Manage Name ID service
diff --git a/example/sp-wsgi/sp.py b/example/sp-wsgi/sp.py
index 5fa31da..d9f59f5 100755
--- a/example/sp-wsgi/sp.py
+++ b/example/sp-wsgi/sp.py
@@ -156,10 +156,12 @@ class Cache(object):
self.user = {}
self.result = {}
- def kaka2user(self, kaka):
- logger.debug("KAKA: %s" % kaka)
- if kaka:
- cookie_obj = SimpleCookie(kaka)
+ def get_user(self, environ):
+ cookie = environ.get("HTTP_COOKIE", '')
+
+ logger.debug("Cookie: %s" % cookie)
+ if cookie:
+ cookie_obj = SimpleCookie(cookie)
morsel = cookie_obj.get(self.cookie_name, None)
if morsel:
try:
@@ -167,26 +169,26 @@ class Cache(object):
except KeyError:
return None
else:
- logger.debug("No spauthn cookie")
+ logger.debug("No %s cookie", self.cookie_name)
+
return None
- def delete_cookie(self, environ=None, kaka=None):
- if not kaka:
- kaka = environ.get("HTTP_COOKIE", '')
- logger.debug("delete KAKA: %s" % kaka)
- if kaka:
+ def delete_cookie(self, environ):
+ cookie = environ.get("HTTP_COOKIE", '')
+ logger.debug("delete cookie: %s" % cookie)
+ if cookie:
_name = self.cookie_name
- cookie_obj = SimpleCookie(kaka)
+ cookie_obj = SimpleCookie(cookie)
morsel = cookie_obj.get(_name, None)
cookie = SimpleCookie()
cookie[_name] = ""
cookie[_name]['path'] = "/"
logger.debug("Expire: %s" % morsel)
- cookie[_name]["expires"] = _expiration("dawn")
- return tuple(cookie.output().split(": ", 1))
+ cookie[_name]["expires"] = _expiration("now")
+ return cookie.output().split(": ", 1)
return None
- def user2kaka(self, user):
+ def set_cookie(self, user):
uid = rndstr(32)
self.uid2user[uid] = user
cookie = SimpleCookie()
@@ -194,7 +196,7 @@ class Cache(object):
cookie[self.cookie_name]['path'] = "/"
cookie[self.cookie_name]["expires"] = _expiration(480)
logger.debug("Cookie expires: %s" % cookie[self.cookie_name]["expires"])
- return tuple(cookie.output().split(": ", 1))
+ return cookie.output().split(": ", 1)
# -----------------------------------------------------------------------------
@@ -318,6 +320,12 @@ class Service(object):
# -----------------------------------------------------------------------------
+class User(object):
+ def __init__(self, name_id, data):
+ self.name_id = name_id
+ self.data = data
+
+
class ACS(Service):
def __init__(self, sp, environ, start_response, cache=None, **kwargs):
Service.__init__(self, environ, start_response)
@@ -357,7 +365,14 @@ class ACS(Service):
return resp(self.environ, self.start_response)
logger.info("AVA: %s" % self.response.ava)
- resp = Response(dict_to_table(self.response.ava))
+
+ user = User(self.response.name_id, self.response.ava)
+ cookie = self.cache.set_cookie(user)
+
+ resp = Redirect("/", headers=[
+ ("Location", "/"),
+ cookie,
+ ])
return resp(self.environ, self.start_response)
def verify_attributes(self, ava):
@@ -543,7 +558,6 @@ class SSO(object):
ht_args = _cli.apply_binding(_binding, "%s" % req, destination,
relay_state=_rstate)
_sid = req_id
- logger.debug("ht_args: %s" % ht_args)
except Exception, exc:
logger.exception(exc)
resp = ServiceError(
@@ -582,6 +596,19 @@ class SSO(object):
# ----------------------------------------------------------------------------
+class SLO(Service):
+ def __init__(self, sp, environ, start_response, cache=None):
+ Service.__init__(self, environ, start_response)
+ self.sp = sp
+ self.cache = cache
+
+ def do(self, response, binding, relay_state="", mtype="response"):
+ req_info = self.sp.parse_logout_request_response(response, binding)
+ return finish_logout(self.environ, self.start_response)
+
+# ----------------------------------------------------------------------------
+
+
#noinspection PyUnusedLocal
def not_found(environ, start_response):
"""Called if no URL matches."""
@@ -593,9 +620,18 @@ def not_found(environ, start_response):
#noinspection PyUnusedLocal
-def main(environ, start_response, _sp):
- _sso = SSO(_sp, environ, start_response, cache=CACHE, **ARGS)
- return _sso.do()
+def main(environ, start_response, sp):
+ user = CACHE.get_user(environ)
+
+ if user is None:
+ sso = SSO(sp, environ, start_response, cache=CACHE, **ARGS)
+ return sso.do()
+
+ body = dict_to_table(user.data)
+ body += '
logout'
+
+ resp = Response(body)
+ return resp(environ, start_response)
def disco(environ, start_response, _sp):
@@ -613,12 +649,67 @@ def disco(environ, start_response, _sp):
# ----------------------------------------------------------------------------
+
+#noinspection PyUnusedLocal
+def logout(environ, start_response, sp):
+ user = CACHE.get_user(environ)
+
+ if user is None:
+ sso = SSO(sp, environ, start_response, cache=CACHE, **ARGS)
+ return sso.do()
+
+ logger.info("[logout] subject_id: '%s'" % (user.name_id,))
+
+ # What if more than one
+ data = sp.global_logout(user.name_id)
+ logger.info("[logout] global_logout > %s" % data)
+
+ for entity_id, logout_info in data.items():
+ if isinstance(logout_info, tuple):
+ binding, http_info = logout_info
+
+ if binding == BINDING_HTTP_POST:
+ body = ''.join(http_info['data'])
+ resp = Response(body)
+ return resp(environ, start_response)
+ elif binding == BINDING_HTTP_REDIRECT:
+ for key, value in http_info['headers']:
+ if key.lower() == 'location':
+ resp = Redirect(value)
+ return resp(environ, start_response)
+
+ resp = ServiceError('missing Location header')
+ return resp(environ, start_response)
+ else:
+ resp = ServiceError('unknown logout binding: %s', binding)
+ return resp(environ, start_response)
+ else: # result from logout, should be OK
+ pass
+
+ return finish_logout(environ, start_response)
+
+
+def finish_logout(environ, start_response):
+ logger.info("[logout done] environ: %s" % environ)
+ logger.info("[logout done] remaining subjects: %s" % CACHE.uid2user.values())
+
+ # remove cookie and stored info
+ cookie = CACHE.delete_cookie(environ)
+
+ resp = Response('You are now logged out of this service', headers=[
+ cookie,
+ ])
+ return resp(environ, start_response)
+
+# ----------------------------------------------------------------------------
+
# map urls to functions
urls = [
# Hmm, place holder, NOT used
('place', ("holder", None)),
(r'^$', main),
- (r'^disco', disco)
+ (r'^disco', disco),
+ (r'^logout$', logout),
]
@@ -630,6 +721,13 @@ def add_urls():
urls.append(("%s/redirect$" % base, (ACS, "redirect", SP)))
urls.append(("%s/redirect/(.*)$" % base, (ACS, "redirect", SP)))
+ base = "slo"
+
+ urls.append(("%s/post$" % base, (SLO, "post", SP)))
+ urls.append(("%s/post/(.*)$" % base, (SLO, "post", SP)))
+ urls.append(("%s/redirect$" % base, (SLO, "redirect", SP)))
+ urls.append(("%s/redirect/(.*)$" % base, (SLO, "redirect", SP)))
+
# ----------------------------------------------------------------------------
diff --git a/example/sp-wsgi/sp.xml b/example/sp-wsgi/sp.xml
index 6c28d9c..93df2be 100644
--- a/example/sp-wsgi/sp.xml
+++ b/example/sp-wsgi/sp.xml
@@ -1,5 +1,5 @@
-http://www.geant.net/uri/dataprotection-code-of-conduct/v1MIIC8jCCAlugAwIBAgIJAJHg2V5J31I8MA0GCSqGSIb3DQEBBQUAMFoxCzAJBgNV
+http://www.geant.net/uri/dataprotection-code-of-conduct/v1MIIC8jCCAlugAwIBAgIJAJHg2V5J31I8MA0GCSqGSIb3DQEBBQUAMFoxCzAJBgNV
BAYTAlNFMQ0wCwYDVQQHEwRVbWVhMRgwFgYDVQQKEw9VbWVhIFVuaXZlcnNpdHkx
EDAOBgNVBAsTB0lUIFVuaXQxEDAOBgNVBAMTB1Rlc3QgU1AwHhcNMDkxMDI2MTMz
MTE1WhcNMTAxMDI2MTMzMTE1WjBaMQswCQYDVQQGEwJTRTENMAsGA1UEBxMEVW1l
@@ -31,4 +31,4 @@ AxMHVGVzdCBTUIIJAJHg2V5J31I8MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF
BQADgYEAMuRwwXRnsiyWzmRikpwinnhTmbooKm5TINPE7A7gSQ710RxioQePPhZO
zkM27NnHTrCe2rBVg0EGz7QTd1JIwLPvgoj4VTi/fSha/tXrYUaqc9AqU1kWI4WN
+vffBGQ09mo+6CffuFTZYeOhzP/2stAPwCTU4kxEoiy0KpZMANI=
-My SP serviceExample SP
+
diff --git a/example/sp-wsgi/sp_conf.py.example b/example/sp-wsgi/sp_conf.py.example
index 96d1585..46adaa9 100644
--- a/example/sp-wsgi/sp_conf.py.example
+++ b/example/sp-wsgi/sp_conf.py.example
@@ -23,10 +23,16 @@ CONFIG = {
"description": "Example SP",
"service": {
"sp": {
+ "authn_requests_signed": True,
+ "logout_requests_signed": True,
"endpoints": {
"assertion_consumer_service": [
("%s/acs/post" % BASE, BINDING_HTTP_POST)
],
+ "single_logout_service": [
+ ("%s/slo/redirect" % BASE, BINDING_HTTP_REDIRECT),
+ ("%s/slo/post" % BASE, BINDING_HTTP_POST),
+ ],
}
},
},
diff --git a/src/saml2/client.py b/src/saml2/client.py
index b498d44..ca83bf9 100644
--- a/src/saml2/client.py
+++ b/src/saml2/client.py
@@ -22,7 +22,6 @@ from saml2.samlp import STATUS_REQUEST_DENIED
from saml2.samlp import STATUS_UNKNOWN_PRINCIPAL
from saml2.time_util import not_on_or_after
from saml2.saml import AssertionIDRef
-from saml2.saml import NAMEID_FORMAT_PERSISTENT
from saml2.client_base import Base
from saml2.client_base import LogoutError
from saml2.client_base import NoServiceDefined
@@ -44,7 +43,7 @@ class Saml2Client(Base):
def prepare_for_authenticate(self, entityid=None, relay_state="",
binding=saml2.BINDING_HTTP_REDIRECT, vorg="",
- nameid_format=NAMEID_FORMAT_PERSISTENT,
+ nameid_format=None,
scoping=None, consent=None, extensions=None,
sign=None,
response_binding=saml2.BINDING_HTTP_POST,
@@ -178,7 +177,7 @@ class Saml2Client(Base):
not_done.remove(entity_id)
response = response.text
logger.info("Response: %s" % response)
- res = self.parse_logout_request_response(response)
+ res = self.parse_logout_request_response(response, binding)
responses[entity_id] = res
else:
logger.info("NOT OK response from %s" % destination)
diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py
index 793e3f7..5e026d0 100644
--- a/src/saml2/client_base.py
+++ b/src/saml2/client_base.py
@@ -193,7 +193,7 @@ class Base(Entity):
def create_authn_request(self, destination, vorg="", scoping=None,
binding=saml2.BINDING_HTTP_POST,
- nameid_format=NAMEID_FORMAT_TRANSIENT,
+ nameid_format=None,
service_url_binding=None, message_id=0,
consent=None, extensions=None, sign=None,
allow_create=False, sign_prepare=False, **kwargs):
@@ -261,13 +261,19 @@ class Base(Entity):
else:
allow_create = "false"
- # Profile stuff, should be configurable
- if nameid_format is None:
- name_id_policy = samlp.NameIDPolicy(
- allow_create=allow_create, format=NAMEID_FORMAT_TRANSIENT)
- elif nameid_format == "":
+ if nameid_format == "":
name_id_policy = None
else:
+ if nameid_format is None:
+ nameid_format = self.config.getattr("name_id_format", "sp")
+
+ if nameid_format is None:
+ nameid_format = NAMEID_FORMAT_TRANSIENT
+ elif isinstance(nameid_format, list):
+ # NameIDPolicy can only have one format specified
+ nameid_format = nameid_format[0]
+
+
name_id_policy = samlp.NameIDPolicy(allow_create=allow_create,
format=nameid_format)
diff --git a/src/saml2/request.py b/src/saml2/request.py
index 3497db1..17f3edf 100644
--- a/src/saml2/request.py
+++ b/src/saml2/request.py
@@ -127,6 +127,10 @@ class LogoutRequest(Request):
attribute_converters, timeslack)
self.signature_check = self.sec.correctly_signed_logout_request
+ @property
+ def issuer(self):
+ return self.message.issuer
+
class AttributeQuery(Request):
msgtype = "attribute_query"
diff --git a/src/saml2/samlp.py b/src/saml2/samlp.py
index 8951d34..06b4e4c 100644
--- a/src/saml2/samlp.py
+++ b/src/saml2/samlp.py
@@ -1833,4 +1833,4 @@ def any_response_from_string(xmlstr):
if not resp:
raise Exception("Unknown response type")
- return resp
\ No newline at end of file
+ return resp