IdP-initiated logout for current user
authorRob Crittenden <rcritten@redhat.com>
Mon, 30 Mar 2015 15:42:10 +0000 (11:42 -0400)
committerRob Crittenden <rcritten@redhat.com>
Thu, 2 Apr 2015 02:53:55 +0000 (22:53 -0400)
Perform Single Logout for the current user when a logout is initiated
in the IdP.

A fake initial session is created. In the current logout code the
initial logout requestor holds the final redirect URL. In this case
it redirects back to the root IdP page.

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

Signed-off-by: Rob Crittenden <rcritten@redhat.com>
Reviewed-by: Nathan Kinder <nkinder@redhat.com>
ipsilon/login/common.py
ipsilon/providers/saml2/sessions.py
ipsilon/providers/saml2idp.py
tests/testlogout.py

index 9beb741..d616882 100644 (file)
@@ -273,11 +273,28 @@ class Login(Page):
 
 
 class Logout(Page):
+    def __init__(self, *args, **kwargs):
+        super(Logout, self).__init__(*args, **kwargs)
+        self.handlers = {}
 
     def root(self, *args, **kwargs):
-        UserSession().logout(self.user)
+        us = UserSession()
+
+        for provider in self.handlers:
+            self.debug("Calling logout for provider %s" % provider)
+            obj = self.handlers[provider]
+            obj()
+
+        us.logout(self.user)
         return self._template('logout.html', title='Logout')
 
+    def add_handler(self, provider, handler):
+        """
+        Providers can register a logout handler here that is called
+        when the IdP logout link is accessed.
+        """
+        self.handlers[provider] = handler
+
 
 class Cancel(Page):
 
index fb1f646..5931734 100644 (file)
@@ -140,12 +140,16 @@ class SAMLSessionsContainer(Log):
 
         self.sessions_logging_out[session.provider_id] = session
 
-    def get_next_logout(self):
+    def get_next_logout(self, remove=True):
         """
         Get the next session in the logged-in state and move
         it to the logging_out state.  Return the session that is
         found.
 
+        :param remove: 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 login state.
         """
         try:
@@ -153,7 +157,10 @@ class SAMLSessionsContainer(Log):
         except IndexError:
             return None
 
-        session = self.sessions.pop(provider_id)
+        if remove:
+            session = self.sessions.pop(provider_id)
+        else:
+            session = self.sessions.itervalues().next()
 
         if provider_id in self.sessions_logging_out:
             self.sessions_logging_out.pop(provider_id)
index 8ff512c..9bc75b3 100644 (file)
@@ -298,6 +298,8 @@ Provides SAML 2.0 authentication infrastructure. """
             self._debug('Failed to init SAML2 provider: %r' % e)
             return None
 
+        self._root.logout.add_handler(self.name, self.idp_initiated_logout)
+
         # Import all known applications
         data = self.get_data()
         for idval in data:
@@ -320,6 +322,45 @@ Provides SAML 2.0 authentication infrastructure. """
             if self.admin:
                 self.admin.add_sps()
 
+    def idp_initiated_logout(self):
+        """
+        Logout all SP sessions when the logout comes from the IdP.
+
+        For the current user only.
+        """
+        self._debug("IdP-initiated SAML2 logout")
+        us = UserSession()
+
+        saml_sessions = us.get_provider_data('saml2')
+        if saml_sessions is None:
+            self._debug("No SAML2 sessions to logout")
+            return
+        session = saml_sessions.get_next_logout(remove=False)
+        if session is None:
+            return
+
+        # Add a fake session to indicate where the user should
+        # be redirected to when all SP's are logged out.
+        idpurl = self._root.instance_base_url()
+        saml_sessions.add_session("_idp_initiated_logout",
+                                  idpurl,
+                                  "")
+        init_session = saml_sessions.find_session_by_provider(idpurl)
+        init_session.set_logoutstate(idpurl, "idp_initiated_logout", None)
+        saml_sessions.start_logout(init_session)
+
+        logout = self.idp.get_logout_handler()
+        logout.setSessionFromDump(session.session.dump())
+        logout.initRequest(session.provider_id)
+        try:
+            logout.buildRequestMsg()
+        except lasso.Error, e:
+            self.error('failure to build logout request msg: %s' % e)
+            raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
+                                        % e)
+
+        raise cherrypy.HTTPRedirect(logout.msgUrl)
+
 
 class IdpMetadataGenerator(object):
 
index b192739..5018066 100755 (executable)
@@ -291,3 +291,80 @@ if __name__ == '__main__':
         print >> sys.stderr, " ERROR: %s" % repr(e)
         sys.exit(1)
     print " SUCCESS"
+
+    # Test IdP-initiated logout
+    print "testlogout: Access SP Protected Area of SP1...",
+    try:
+        page = sess.fetch_page(idpname, 'http://127.0.0.11:45081/sp/')
+        page.expected_value('text()', 'WORKS!')
+    except ValueError, e:
+        print >> sys.stderr, " ERROR: %s" % repr(e)
+        sys.exit(1)
+    print " SUCCESS"
+
+    print "testlogout: Access SP Protected Area of SP2...",
+    try:
+        page = sess.fetch_page(idpname, 'http://127.0.0.10:45082/sp/')
+        page.expected_value('text()', 'WORKS!')
+    except ValueError, e:
+        print >> sys.stderr, " ERROR: %s" % repr(e)
+        sys.exit(1)
+    print " SUCCESS"
+
+    print "testlogout: Access the IdP...",
+    try:
+        page = sess.fetch_page(idpname, 'http://127.0.0.10:45080/%s' % idpname)
+        page.expected_value('//div[@id="welcome"]/p/text()',
+                            'Welcome %s!' % user)
+    except ValueError, e:
+        print >> sys.stderr, " ERROR: %s" % repr(e)
+        sys.exit(1)
+    print " SUCCESS"
+
+    print "testlogout: IdP-initiated logout ...",
+    try:
+        page = sess.fetch_page(idpname,
+                               'http://127.0.0.10:45080/%s/logout' % idpname)
+        page.expected_value('//div[@id="content"]/p/a/text()', 'Log In')
+    except ValueError, e:
+        print >> sys.stderr, " ERROR: %s" % repr(e)
+        sys.exit(1)
+    print " SUCCESS"
+
+    print "testlogout: Ensure logout of SP1 ...",
+    try:
+        ensure_logout(sess, idpname, 'http://127.0.0.11:45081/sp/')
+    except ValueError, e:
+        print >> sys.stderr, " ERROR: %s" % repr(e)
+        sys.exit(1)
+    print " SUCCESS"
+
+    print "testlogout: Ensure logout of SP2 ...",
+    try:
+        ensure_logout(sess, idpname, 'http://127.0.0.10:45082/sp/')
+    except ValueError, e:
+        print >> sys.stderr, " ERROR: %s" % repr(e)
+        sys.exit(1)
+    print " SUCCESS"
+
+    print "testlogout: Access the IdP...",
+    try:
+        page = sess.fetch_page(idpname,
+                               'http://127.0.0.10:45080/%s/login' % idpname)
+        page.expected_value('//div[@id="welcome"]/p/text()',
+                            'Welcome %s!' % user)
+    except ValueError, e:
+        print >> sys.stderr, " ERROR: %s" % repr(e)
+        sys.exit(1)
+    print " SUCCESS"
+
+    print "testlogout: IdP-initiated logout with no SP sessions...",
+    try:
+        page = sess.fetch_page(idpname,
+                               'http://127.0.0.10:45080/%s/logout' % idpname)
+        page.expected_value('//div[@id="logout"]/p//text()',
+                            'Successfully logged out.')
+    except ValueError, e:
+        print >> sys.stderr, " ERROR: %s" % repr(e)
+        sys.exit(1)
+    print " SUCCESS"