Add support for logout over SOAP
authorRob Crittenden <rcritten@redhat.com>
Thu, 25 Jun 2015 15:00:59 +0000 (11:00 -0400)
committerPatrick Uiterwijk <puiterwijk@redhat.com>
Thu, 16 Jul 2015 13:04:36 +0000 (15:04 +0200)
As each login session comes in, store the supported logout
mechanisms in the SP metadata.

Upon a logout request, loop through all of those SP's that
support SOAP and log those out first, then log out any
remaining sessions using HTTP Redirect.

https://fedorahosted.org/ipsilon/ticket/59

Signed-off-by: Rob Crittenden <rcritten@redhat.com>
Reviewed-by: Patrick Uiterwijk <puiterwijk@redhat.com>
ipsilon/install/ipsilon-client-install
ipsilon/providers/saml2/auth.py
ipsilon/providers/saml2/logout.py
ipsilon/providers/saml2/provider.py
ipsilon/providers/saml2/sessions.py
ipsilon/providers/saml2idp.py
ipsilon/tools/saml2metadata.py
ipsilon/util/data.py

index 49d9e78..d8a310c 100755 (executable)
@@ -97,6 +97,7 @@ def saml2():
     m.set_entity_id(url_sp)
     m.add_certs(c)
     m.add_service(SAML2_SERVICE_MAP['logout-redirect'], url_logout)
     m.set_entity_id(url_sp)
     m.add_certs(c)
     m.add_service(SAML2_SERVICE_MAP['logout-redirect'], url_logout)
+    m.add_service(SAML2_SERVICE_MAP['slo-soap'], url_logout)
     m.add_service(SAML2_SERVICE_MAP['response-post'], url_post, index="0")
     m.add_allowed_name_format(SAML2_NAMEID_MAP[args['saml_nameid']])
     sp_metafile = os.path.join(path, 'metadata.xml')
     m.add_service(SAML2_SERVICE_MAP['response-post'], url_post, index="0")
     m.add_allowed_name_format(SAML2_NAMEID_MAP[args['saml_nameid']])
     sp_metafile = os.path.join(path, 'metadata.xml')
index c46d604..d856220 100644 (file)
@@ -278,10 +278,13 @@ class AuthenticateRequest(ProviderPageBase):
 
         lasso_session = lasso.Session()
         lasso_session.addAssertion(login.remoteProviderId, login.assertion)
 
         lasso_session = lasso.Session()
         lasso_session.addAssertion(login.remoteProviderId, login.assertion)
+        provider = ServiceProvider(self.cfg, login.remoteProviderId)
         saml_sessions.add_session(login.assertion.id,
                                   login.remoteProviderId,
                                   user.name,
         saml_sessions.add_session(login.assertion.id,
                                   login.remoteProviderId,
                                   user.name,
-                                  lasso_session.dump())
+                                  lasso_session.dump(),
+                                  None,
+                                  provider.logout_mechs)
 
     def saml2error(self, login, code, message):
         status = lasso.Samlp2Status()
 
     def saml2error(self, login, code, message):
         status = lasso.Samlp2Status()
index cc9b777..374e885 100644 (file)
@@ -4,8 +4,10 @@ from ipsilon.providers.common import ProviderPageBase
 from ipsilon.providers.common import InvalidRequest
 from ipsilon.providers.saml2.auth import UnknownProvider
 from ipsilon.util.user import UserSession
 from ipsilon.providers.common import InvalidRequest
 from ipsilon.providers.saml2.auth import UnknownProvider
 from ipsilon.util.user import UserSession
+from ipsilon.util.constants import SOAP_MEDIA_TYPE
 import cherrypy
 import lasso
 import cherrypy
 import lasso
+import requests
 
 
 class LogoutRequest(ProviderPageBase):
 
 
 class LogoutRequest(ProviderPageBase):
@@ -58,7 +60,7 @@ class LogoutRequest(ProviderPageBase):
         # all the session indexes and mark them as logging out but only one
         # is needed to handle the request.
         if len(session_indexes) < 1:
         # all the session indexes and mark them as logging out but only one
         # is needed to handle the request.
         if len(session_indexes) < 1:
-            self.error('SLO empty session Indexes: %s')
+            self.error('SLO empty session Indexes')
             raise cherrypy.HTTPError(400, 'Invalid logout request')
         session = saml_sessions.get_session_by_id(session_indexes[0])
         if session:
             raise cherrypy.HTTPError(400, 'Invalid logout request')
         session = saml_sessions.get_session_by_id(session_indexes[0])
         if session:
@@ -181,11 +183,36 @@ class LogoutRequest(ProviderPageBase):
         else:
             raise cherrypy.HTTPError(400, 'Not logged in')
 
         else:
             raise cherrypy.HTTPError(400, 'Not logged in')
 
+    def _soap_logout(self, logout):
+        """
+        Send a SOAP logout request over HTTP and return the result.
+        """
+        headers = {'Content-Type': SOAP_MEDIA_TYPE}
+        try:
+            response = requests.post(logout.msgUrl, data=logout.msgBody,
+                                     headers=headers)
+        except Exception as e:  # pylint: disable=broad-except
+            self.error('SOAP HTTP request failed: (%s) (on %s)' %
+                       (e, logout.msgUrl))
+            raise
+
+        if response.status_code != 200:
+            self.error('SOAP error (%s) (on %s)' %
+                       (response.status, logout.msgUrl))
+            raise InvalidRequest('SOAP HTTP error code', response.status_code)
+
+        if not response.text:
+            self.error('Empty SOAP response')
+            raise InvalidRequest('No content in SOAP response')
+
+        return response.text
+
     def logout(self, message, relaystate=None, samlresponse=None):
         """
     def logout(self, message, relaystate=None, samlresponse=None):
         """
-        Handle HTTP Redirect logout. This is an asynchronous logout
-        request process that relies on the HTTP agent to forward
-        logout requests to any other SP's that are also logged in.
+        Handle HTTP logout. The supported logout methods are stored
+        in each session. First all the SOAP sessions are logged out
+        then the HTTP Redirect method is used for any remaining
+        sessions.
 
         The basic process is this:
          1. A logout request is received. It is processed and the response
 
         The basic process is this:
          1. A logout request is received. It is processed and the response
@@ -198,6 +225,8 @@ class LogoutRequest(ProviderPageBase):
          Repeat steps 2-3 until only the initial logout request is
          left unhandled, at which time the pre-generated response is sent
          back to the SP that originated the logout request.
          Repeat steps 2-3 until only the initial logout request is
          left unhandled, at which time the pre-generated response is sent
          back to the SP that originated the logout request.
+
+        The final logout response is always a redirect.
         """
         logout = self.cfg.idp.get_logout_handler()
 
         """
         logout = self.cfg.idp.get_logout_handler()
 
@@ -217,8 +246,13 @@ class LogoutRequest(ProviderPageBase):
         # Fall through to handle any remaining sessions.
 
         # Find the next SP to logout and send a LogoutRequest
         # Fall through to handle any remaining sessions.
 
         # Find the next SP to logout and send a LogoutRequest
-        session = saml_sessions.get_next_logout()
-        if session:
+        logout_order = [
+            lasso.SAML2_METADATA_BINDING_SOAP,
+            lasso.SAML2_METADATA_BINDING_REDIRECT,
+        ]
+        (logout_mech, session) = saml_sessions.get_next_logout(
+            logout_mechs=logout_order)
+        while session:
             self.debug('Going to log out %s' % session.provider_id)
 
             try:
             self.debug('Going to log out %s' % session.provider_id)
 
             try:
@@ -227,8 +261,12 @@ class LogoutRequest(ProviderPageBase):
                 self.error('Failed to load session: %s' % e)
                 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
                                             % e)
                 self.error('Failed to load session: %s' % e)
                 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
                                             % e)
-
-            logout.initRequest(session.provider_id, lasso.HTTP_METHOD_REDIRECT)
+            if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT:
+                logout.initRequest(session.provider_id,
+                                   lasso.HTTP_METHOD_REDIRECT)
+            else:
+                logout.initRequest(session.provider_id,
+                                   lasso.HTTP_METHOD_SOAP)
 
             try:
                 logout.buildRequestMsg()
 
             try:
                 logout.buildRequestMsg()
@@ -243,7 +281,7 @@ class LogoutRequest(ProviderPageBase):
             indexes = saml_sessions.get_session_id_by_provider_id(
                 session.provider_id
             )
             indexes = saml_sessions.get_session_id_by_provider_id(
                 session.provider_id
             )
-            self.debug('Requesting logout for sessions %s' % indexes)
+            self.debug('Requesting logout for sessions %s' % (indexes,))
             req = logout.get_request()
             req.setSessionIndexes(indexes)
 
             req = logout.get_request()
             req.setSessionIndexes(indexes)
 
@@ -253,13 +291,34 @@ class LogoutRequest(ProviderPageBase):
 
             self.debug('Request logout ID %s for session ID %s' %
                        (logout.request.id, session.session_id))
 
             self.debug('Request logout ID %s for session ID %s' %
                        (logout.request.id, session.session_id))
-            self.debug('Redirecting to another SP to logout on %s at %s' %
-                       (logout.remoteProviderId, logout.msgUrl))
-
-            raise cherrypy.HTTPRedirect(logout.msgUrl)
 
 
-        # Otherwise we're done, respond to the original request using the
-        # response we cached earlier.
+            if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT:
+                self.debug('Redirecting to another SP to logout on %s at %s' %
+                           (logout.remoteProviderId, logout.msgUrl))
+                raise cherrypy.HTTPRedirect(logout.msgUrl)
+            else:
+                self.debug('SOAP request to another SP to logout on %s at %s' %
+                           (logout.remoteProviderId, logout.msgUrl))
+                if logout.msgBody:
+                    message = self._soap_logout(logout)
+                    try:
+                        self._handle_logout_response(us,
+                                                     logout,
+                                                     saml_sessions,
+                                                     message,
+                                                     samlresponse)
+                    except Exception as e:  # pylint: disable=broad-except
+                        self.error('SOAP SLO failed %s' % e)
+                else:
+                    self.error('Provider does not support SOAP')
+
+            (logout_mech, session) = saml_sessions.get_next_logout(
+                logout_mechs=logout_order)
+
+        # done while
+
+        # All sessions should be logged out now. Respond to the
+        # original request using the response we cached earlier.
 
         try:
             session = saml_sessions.get_initial_logout()
 
         try:
             session = saml_sessions.get_initial_logout()
index 3dea631..b70582e 100644 (file)
@@ -3,8 +3,9 @@
 from ipsilon.providers.common import ProviderException
 from ipsilon.util import config as pconfig
 from ipsilon.util.config import ConfigHelper
 from ipsilon.providers.common import ProviderException
 from ipsilon.util import config as pconfig
 from ipsilon.util.config import ConfigHelper
-from ipsilon.tools.saml2metadata import SAML2_NAMEID_MAP
+from ipsilon.tools.saml2metadata import SAML2_NAMEID_MAP, NSMAP
 from ipsilon.util.log import Log
 from ipsilon.util.log import Log
+from lxml import etree
 import lasso
 import re
 
 import lasso
 import re
 
@@ -49,6 +50,14 @@ class ServiceProvider(ServiceProviderConfig):
         self._properties = data[idval]
         self._staging = dict()
         self.load_config()
         self._properties = data[idval]
         self._staging = dict()
         self.load_config()
+        self.logout_mechs = []
+        xmldoc = etree.XML(str(data[idval]['metadata']))
+        logout = xmldoc.xpath('//md:EntityDescriptor'
+                              '/md:SPSSODescriptor'
+                              '/md:SingleLogoutService',
+                              namespaces=NSMAP)
+        for service in logout:
+            self.logout_mechs.append(service.values()[0])
 
     def load_config(self):
         self.new_config(
 
     def load_config(self):
         self.new_config(
index 1000a87..d3ed7e2 100644 (file)
@@ -4,6 +4,10 @@ from cherrypy import config as cherrypy_config
 from ipsilon.util.log import Log
 from ipsilon.util.data import SAML2SessionStore
 import datetime
 from ipsilon.util.log import Log
 from ipsilon.util.data import SAML2SessionStore
 import datetime
+from lasso import (
+    SAML2_METADATA_BINDING_SOAP,
+    SAML2_METADATA_BINDING_REDIRECT,
+)
 
 LOGGED_IN = 1
 INIT_LOGOUT = 2
 
 LOGGED_IN = 1
 INIT_LOGOUT = 2
@@ -29,11 +33,13 @@ class SAMLSession(Log):
                     which matches this.
        logout_request - the Logout request object
        expiration_time - the time the login session expires
                     which matches this.
        logout_request - the Logout request object
        expiration_time - the time the login session expires
+       supported_logout_mechs - logout mechanisms supported by this session
     """
     def __init__(self, uuidval, session_id, provider_id, user,
                  login_session, logoutstate=None, relaystate=None,
                  logout_request=None, request_id=None,
     """
     def __init__(self, uuidval, session_id, provider_id, user,
                  login_session, logoutstate=None, relaystate=None,
                  logout_request=None, request_id=None,
-                 expiration_time=None):
+                 expiration_time=None,
+                 supported_logout_mechs=None):
 
         self.uuidval = uuidval
         self.session_id = session_id
 
         self.uuidval = uuidval
         self.session_id = session_id
@@ -45,6 +51,9 @@ class SAMLSession(Log):
         self.request_id = request_id
         self.logout_request = logout_request
         self.expiration_time = expiration_time
         self.request_id = request_id
         self.logout_request = logout_request
         self.expiration_time = expiration_time
+        if supported_logout_mechs is None:
+            supported_logout_mechs = []
+        self.supported_logout_mechs = supported_logout_mechs
 
     def set_logoutstate(self, relaystate=None, request=None, request_id=None):
         """
 
     def set_logoutstate(self, relaystate=None, request=None, request_id=None):
         """
@@ -66,6 +75,7 @@ class SAMLSession(Log):
         self.debug('provider_id %s' % self.provider_id)
         self.debug('login session %s' % self.login_session)
         self.debug('logoutstate %s' % self.logoutstate)
         self.debug('provider_id %s' % self.provider_id)
         self.debug('login session %s' % self.login_session)
         self.debug('logoutstate %s' % self.logoutstate)
+        self.debug('logout mech %s' % self.supported_logout_mechs)
 
     def convert(self):
         """
 
     def convert(self):
         """
@@ -118,12 +128,20 @@ class SAMLSessionFactory(Log):
                            data.get('relaystate'),
                            data.get('logout_request'),
                            data.get('request_id'),
                            data.get('relaystate'),
                            data.get('logout_request'),
                            data.get('request_id'),
-                           data.get('expiration_time'))
+                           data.get('expiration_time'),
+                           data.get('supported_logout_mechs'))
 
     def add_session(self, session_id, provider_id, user, login_session,
 
     def add_session(self, session_id, provider_id, user, login_session,
-                    request_id=None):
+                    request_id, supported_logout_mechs):
         """
         Add a new login session to the table.
         """
         Add a new login session to the table.
+
+        :param session_id: The login session ID
+        :param provider_id: The URL of the SP
+        :param user: The NameID username
+        :param login_session: The lasso Login session
+        :param request_id: The request ID of the Logout
+        :param supported_logout_mechs: A list of logout protocols supported
         """
         self.user = user
 
         """
         self.user = user
 
@@ -136,9 +154,9 @@ class SAMLSessionFactory(Log):
                 'user': user,
                 'login_session': login_session,
                 'logoutstate': LOGGED_IN,
                 'user': user,
                 'login_session': login_session,
                 'logoutstate': LOGGED_IN,
-                'expiration_time': expiration_time}
-        if request_id:
-            data['request_id'] = request_id
+                'expiration_time': expiration_time,
+                'request_id': request_id,
+                'supported_logout_mechs': supported_logout_mechs}
 
         uuidval = self._ss.new_session(data)
 
 
         uuidval = self._ss.new_session(data)
 
@@ -209,7 +227,8 @@ class SAMLSessionFactory(Log):
         datum = samlsession.convert()
         self._ss.update_session(datum)
 
         datum = samlsession.convert()
         self._ss.update_session(datum)
 
-    def get_next_logout(self, peek=False):
+    def get_next_logout(self, peek=False,
+                        logout_mechs=None):
         """
         Get the next session in the logged-in state and move
         it to the logging_out state.  Return the session that is
         """
         Get the next session in the logged-in state and move
         it to the logging_out state.  Return the session that is
@@ -218,24 +237,34 @@ class SAMLSessionFactory(Log):
         :param peek: for IdP-initiated logout we can't remove the
                      session otherwise when the request comes back
                      in the user won't be seen as being logged-on.
         :param peek: for IdP-initiated logout we can't remove the
                      session otherwise when the request comes back
                      in the user won't be seen as being logged-on.
-
-        Return None if no more sessions in LOGGED_IN state.
+        :param logout_mechs: An ordered list of logout mechanisms
+                     you're looking for. For each mechanism in order
+                     loop through all sessions. If If no sessions of
+                     this method are available then try the next mechanism
+                     until exhausted. In that case None is returned.
+
+        Returns a tuple of (mechanism, session) or
+        (None, None) if no more sessions in LOGGED_IN state.
         """
         candidates = self._ss.get_user_sessions(self.user)
         """
         candidates = self._ss.get_user_sessions(self.user)
-
-        for c in candidates:
-            key = c.keys()[0]
-            if int(c[key].get('logoutstate', 0)) == LOGGED_IN:
-                samlsession = self._data_to_samlsession(key, c[key])
-                self.start_logout(samlsession, initial=False)
-                return samlsession
-        return None
+        if logout_mechs is None:
+            logout_mechs = [SAML2_METADATA_BINDING_REDIRECT, ]
+
+        for mech in logout_mechs:
+            for c in candidates:
+                key = c.keys()[0]
+                if ((int(c[key].get('logoutstate', 0)) == LOGGED_IN) and
+                        (mech in c[key].get('supported_logout_mechs'))):
+                    samlsession = self._data_to_samlsession(key, c[key])
+                    self.start_logout(samlsession, initial=False)
+                    return (mech, samlsession)
+        return (None, None)
 
     def get_initial_logout(self):
         """
         Get the initial logout request.
 
 
     def get_initial_logout(self):
         """
         Get the initial logout request.
 
-        Return None if no sessions in INIT_LOGOUT state.
+        Raises ValueError if no sessions in INIT_LOGOUT state.
         """
         candidates = self._ss.get_user_sessions(self.user)
 
         """
         candidates = self._ss.get_user_sessions(self.user)
 
@@ -248,7 +277,7 @@ class SAMLSessionFactory(Log):
             if int(c[key].get('logoutstate', 0)) == INIT_LOGOUT:
                 samlsession = self._data_to_samlsession(key, c[key])
                 return samlsession
             if int(c[key].get('logoutstate', 0)) == INIT_LOGOUT:
                 samlsession = self._data_to_samlsession(key, c[key])
                 return samlsession
-        return None
+        raise ValueError()
 
     def wipe_data(self):
         self._ss.wipe_data()
 
     def wipe_data(self):
         self._ss.wipe_data()
@@ -276,14 +305,21 @@ if __name__ == '__main__':
     factory = SAMLSessionFactory('/tmp/saml2sessions.sqlite')
     factory.wipe_data()
 
     factory = SAMLSessionFactory('/tmp/saml2sessions.sqlite')
     factory.wipe_data()
 
-    sess1 = factory.add_session('_123456', provider1, "admin", "<Login/>")
-    sess2 = factory.add_session('_789012', provider2, "testuser", "<Login/>")
+    sess1 = factory.add_session('_123456', provider1, "admin",
+                                "<Login/>", '_1234',
+                                [SAML2_METADATA_BINDING_REDIRECT])
+    sess2 = factory.add_session('_789012', provider2, "testuser",
+                                "<Login/>", '_7890',
+                                [SAML2_METADATA_BINDING_SOAP,
+                                 SAML2_METADATA_BINDING_REDIRECT])
 
     # Test finding sessions by provider
     ids = factory.get_session_id_by_provider_id(provider2)
     assert(len(ids) == 1)
 
 
     # Test finding sessions by provider
     ids = factory.get_session_id_by_provider_id(provider2)
     assert(len(ids) == 1)
 
-    sess3 = factory.add_session('_345678', provider2, "testuser", "<Login/>")
+    sess3 = factory.add_session('_345678', provider2, "testuser",
+                                "<Login/>", '_3456',
+                                [SAML2_METADATA_BINDING_REDIRECT])
     ids = factory.get_session_id_by_provider_id(provider2)
     assert(len(ids) == 2)
 
     ids = factory.get_session_id_by_provider_id(provider2)
     assert(len(ids) == 2)
 
@@ -307,7 +343,7 @@ if __name__ == '__main__':
     test2 = factory.get_session_by_id('_789012')
     factory.start_logout(test2, initial=True)
 
     test2 = factory.get_session_by_id('_789012')
     factory.start_logout(test2, initial=True)
 
-    test3 = factory.get_next_logout()
+    (lmech, test3) = factory.get_next_logout()
     assert(test3.session_id == '_345678')
 
     test4 = factory.get_initial_logout()
     assert(test3.session_id == '_345678')
 
     test4 = factory.get_initial_logout()
index f771ef7..5ac83dd 100644 (file)
@@ -131,7 +131,7 @@ class Continue(AuthenticateRequest):
         return self.auth(login)
 
 
         return self.auth(login)
 
 
-class RedirectLogout(LogoutRequest):
+class Logout(LogoutRequest):
 
     def GET(self, *args, **kwargs):
         query = cherrypy.request.query_string
 
     def GET(self, *args, **kwargs):
         query = cherrypy.request.query_string
@@ -159,7 +159,7 @@ class SLO(ProviderPageBase):
     def __init__(self, *args, **kwargs):
         super(SLO, self).__init__(*args, **kwargs)
         self.debug('SLO init')
     def __init__(self, *args, **kwargs):
         super(SLO, self).__init__(*args, **kwargs)
         self.debug('SLO init')
-        self.Redirect = RedirectLogout(*args, **kwargs)
+        self.Redirect = Logout(*args, **kwargs)
 
 
 # one week
 
 
 # one week
@@ -394,13 +394,18 @@ Provides SAML 2.0 authentication infrastructure. """
         Logout all SP sessions when the logout comes from the IdP.
 
         For the current user only.
         Logout all SP sessions when the logout comes from the IdP.
 
         For the current user only.
+
+        Only use HTTP-Redirect to start the logout. This is guaranteed
+        to be supported in SAML 2.
         """
         self.debug("IdP-initiated SAML2 logout")
         us = UserSession()
         user = us.get_user()
 
         saml_sessions = self.sessionfactory
         """
         self.debug("IdP-initiated SAML2 logout")
         us = UserSession()
         user = us.get_user()
 
         saml_sessions = self.sessionfactory
-        session = saml_sessions.get_next_logout()
+        # pylint: disable=unused-variable
+        (mech, session) = saml_sessions.get_next_logout(
+            logout_mechs=[lasso.SAML2_METADATA_BINDING_REDIRECT])
         if session is None:
             return
 
         if session is None:
             return
 
@@ -418,7 +423,8 @@ Provides SAML 2.0 authentication infrastructure. """
         # be redirected to when all SP's are logged out.
         idpurl = self._root.instance_base_url()
         session_id = "_" + uuid.uuid4().hex.upper()
         # be redirected to when all SP's are logged out.
         idpurl = self._root.instance_base_url()
         session_id = "_" + uuid.uuid4().hex.upper()
-        saml_sessions.add_session(session_id, idpurl, user.name, "")
+        saml_sessions.add_session(session_id, idpurl, user.name, "", "",
+                                  [lasso.SAML2_METADATA_BINDING_REDIRECT])
         init_session = saml_sessions.get_session_by_id(session_id)
         saml_sessions.start_logout(init_session, relaystate=idpurl)
 
         init_session = saml_sessions.get_session_by_id(session_id)
         saml_sessions.start_logout(init_session, relaystate=idpurl)
 
index 99857bf..98e7c67 100755 (executable)
@@ -29,6 +29,8 @@ SAML2_SERVICE_MAP = {
                  lasso.SAML2_METADATA_BINDING_SOAP),
     'logout-redirect': ('SingleLogoutService',
                         lasso.SAML2_METADATA_BINDING_REDIRECT),
                  lasso.SAML2_METADATA_BINDING_SOAP),
     'logout-redirect': ('SingleLogoutService',
                         lasso.SAML2_METADATA_BINDING_REDIRECT),
+    'slo-soap': ('SingleLogoutService',
+                 lasso.SAML2_METADATA_BINDING_SOAP),
     'response-post': ('AssertionConsumerService',
                       lasso.SAML2_METADATA_BINDING_POST)
 }
     'response-post': ('AssertionConsumerService',
                       lasso.SAML2_METADATA_BINDING_POST)
 }
index 53a1756..e0cd6e1 100644 (file)
@@ -551,6 +551,10 @@ class SAML2SessionStore(Store):
         return self.get_unique_data(self.table, idval, name, value)
 
     def new_session(self, datum):
         return self.get_unique_data(self.table, idval, name, value)
 
     def new_session(self, datum):
+        if 'supported_logout_mechs' in datum:
+            datum['supported_logout_mechs'] = ','.join(
+                datum['supported_logout_mechs']
+            )
         return self.new_unique_data(self.table, datum)
 
     def get_session(self, session_id=None, request_id=None):
         return self.new_unique_data(self.table, datum)
 
     def get_session(self, session_id=None, request_id=None):
@@ -567,7 +571,7 @@ class SAML2SessionStore(Store):
 
     def get_user_sessions(self, user):
         """
 
     def get_user_sessions(self, user):
         """
-        Retrun a list of all sessions for a given user.
+        Return a list of all sessions for a given user.
         """
         rows = self.get_unique_data(self.table, name='user', value=user)
 
         """
         rows = self.get_unique_data(self.table, name='user', value=user)
 
@@ -575,6 +579,8 @@ class SAML2SessionStore(Store):
         logged_in = []
         for r in rows:
             data = self.get_unique_data(self.table, uuidval=r)
         logged_in = []
         for r in rows:
             data = self.get_unique_data(self.table, uuidval=r)
+            data[r]['supported_logout_mechs'] = data[r].get(
+                'supported_logout_mechs', '').split(',')
             logged_in.append(data)
 
         return logged_in
             logged_in.append(data)
 
         return logged_in