diff options
-rw-r--r-- | ipsilon/login/common.py | 19 | ||||
-rw-r--r-- | ipsilon/providers/saml2/sessions.py | 11 | ||||
-rw-r--r-- | ipsilon/providers/saml2idp.py | 41 | ||||
-rwxr-xr-x | tests/testlogout.py | 77 |
4 files changed, 145 insertions, 3 deletions
diff --git a/ipsilon/login/common.py b/ipsilon/login/common.py index 9beb741..d616882 100644 --- a/ipsilon/login/common.py +++ b/ipsilon/login/common.py @@ -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): diff --git a/ipsilon/providers/saml2/sessions.py b/ipsilon/providers/saml2/sessions.py index fb1f646..5931734 100644 --- a/ipsilon/providers/saml2/sessions.py +++ b/ipsilon/providers/saml2/sessions.py @@ -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) diff --git a/ipsilon/providers/saml2idp.py b/ipsilon/providers/saml2idp.py index 8ff512c..9bc75b3 100644 --- a/ipsilon/providers/saml2idp.py +++ b/ipsilon/providers/saml2idp.py @@ -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): diff --git a/tests/testlogout.py b/tests/testlogout.py index b192739..5018066 100755 --- a/tests/testlogout.py +++ b/tests/testlogout.py @@ -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" |