diff options
Diffstat (limited to 'ipsilon/providers/saml2/logout.py')
-rw-r--r-- | ipsilon/providers/saml2/logout.py | 265 |
1 files changed, 265 insertions, 0 deletions
diff --git a/ipsilon/providers/saml2/logout.py b/ipsilon/providers/saml2/logout.py new file mode 100644 index 0000000..46aea6e --- /dev/null +++ b/ipsilon/providers/saml2/logout.py @@ -0,0 +1,265 @@ +# Copyright (C) 2015 Rob Crittenden <rcritten@redhat.com> +# +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from ipsilon.providers.common import ProviderPageBase +from ipsilon.providers.common import InvalidRequest +from ipsilon.providers.saml2.sessions import SAMLSessionsContainer +from ipsilon.providers.saml2.auth import UnknownProvider +from ipsilon.util.user import UserSession +import cherrypy +import lasso + + +class LogoutRequest(ProviderPageBase): + """ + SP-initiated logout. + + The sequence is: + - On each logout a new session is created to represent that + provider + - Initial logout request is verified and stored in the login + session + - If there are other sessions then one is chosen that is not + the current provider and a logoutRequest is sent + - When a logoutResponse is received the session is removed + - When all other sessions but the initial one have been + logged out then it a final logoutResponse is sent and the + session removed. At this point the cherrypy session is + deleted. + """ + + def __init__(self, *args, **kwargs): + super(LogoutRequest, self).__init__(*args, **kwargs) + + def _handle_logout_request(self, us, logout, saml_sessions, message): + self.debug('Logout request') + + try: + logout.processRequestMsg(message) + except (lasso.ServerProviderNotFoundError, + lasso.ProfileUnknownProviderError) as e: + msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId, + e, message) + self.error(msg) + raise UnknownProvider(msg) + except (lasso.ProfileInvalidProtocolprofileError, + lasso.DsError), e: + msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request, + e, message) + self.error(msg) + raise InvalidRequest(msg) + except lasso.Error, e: + self.error('SLO unknown error: %s' % message) + raise cherrypy.HTTPError(400, 'Invalid logout request') + + # TODO: verify that the session index is in the request + session_indexes = logout.request.sessionIndexes + self.debug('SLO from %s with %s sessions' % + (logout.remoteProviderId, session_indexes)) + + session = saml_sessions.find_session_by_provider( + logout.remoteProviderId) + if session: + try: + logout.setSessionFromDump(session.session.dump()) + except lasso.ProfileBadSessionDumpError as e: + self.error('loading session failed: %s' % e) + raise cherrypy.HTTPError(400, 'Invalid logout session') + else: + self.error('Logout attempt without being loggged in.') + raise cherrypy.HTTPError(400, 'Not logged in') + + try: + logout.validateRequest() + except lasso.ProfileSessionNotFoundError, e: + self.error('Logout failed. No sessions for %s' % + logout.remoteProviderId) + raise cherrypy.HTTPError(400, 'Not logged in') + except lasso.LogoutUnsupportedProfileError: + self.error('Logout failed. Unsupported profile %s' % + logout.remoteProviderId) + raise cherrypy.HTTPError(400, 'Profile does not support logout') + except lasso.Error, e: + self.error('SLO validation failed: %s' % e) + raise cherrypy.HTTPError(400, 'Failed to validate logout request') + + try: + logout.buildResponseMsg() + except lasso.ProfileUnsupportedProfileError: + self.error('Unsupported profile for %s' % logout.remoteProviderId) + raise cherrypy.HTTPError(400, 'Profile does not support logout') + except lasso.Error, e: + self.error('SLO failed to build logout response: %s' % e) + + session.set_logoutstate(logout.msgUrl, logout.request.id, + message) + saml_sessions.start_logout(session) + + us.save_provider_data('saml2', saml_sessions) + + return + + def _handle_logout_response(self, us, logout, saml_sessions, message, + samlresponse): + + self.debug('Logout response') + + try: + logout.processResponseMsg(message) + except getattr(lasso, 'ProfileRequestDeniedError', + lasso.LogoutRequestDeniedError): + self.error('Logout request denied by %s' % + logout.remoteProviderId) + # Fall through to next provider + except (lasso.ProfileInvalidMsgError, + lasso.LogoutPartialLogoutError) as e: + self.error('Logout request from %s failed: %s' % + (logout.remoteProviderId, e)) + else: + self.debug('Processing SLO Response from %s' % + logout.remoteProviderId) + + self.debug('SLO response to request id %s' % + logout.response.inResponseTo) + + saml_sessions = us.get_provider_data('saml2') + if saml_sessions is None: + # TODO: return logged out instead + saml_sessions = SAMLSessionsContainer() + + # TODO: need to log out each SessionIndex? + session = saml_sessions.find_session_by_provider( + logout.remoteProviderId) + + if session is not None: + self.debug('Logout response session logout id is: %s' % + session.session_id) + saml_sessions.remove_session_by_provider( + logout.remoteProviderId) + us.save_provider_data('saml2', saml_sessions) + user = us.get_user() + self._audit('Logged out user: %s [%s] from %s' % + (user.name, user.fullname, + logout.remoteProviderId)) + else: + self.error('Logout attempt without being loggged in.') + raise cherrypy.HTTPError(400, 'Not logged in') + + return + + 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. + + The basic process is this: + 1. A logout request is received. It is processed and the response + cached. + 2. If any other SP's have also logged in as this user then the + first such session is popped off and a logout request is + generated and forwarded to the SP. + 3. If a logout response is received then the user is marked + as logged out from that SP. + 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. + """ + logout = self.cfg.idp.get_logout_handler() + + us = UserSession() + + saml_sessions = us.get_provider_data('saml2') + if saml_sessions is None: + # No sessions means nothing to log out + self.error('Logout attempt without being loggged in.') + raise cherrypy.HTTPError(400, 'Not logged in') + + self.debug('%d sessions loaded' % saml_sessions.count()) + saml_sessions.dump() + + if lasso.SAML2_FIELD_REQUEST in message: + self._handle_logout_request(us, logout, saml_sessions, message) + elif samlresponse: + self._handle_logout_response(us, logout, saml_sessions, message, + samlresponse) + else: + raise cherrypy.HTTPRedirect(400, 'Bad Request. Not a logout ' + + 'request or response.') + + # Fall through to handle any remaining sessions. + + # Find the next SP to logout and send a LogoutRequest + saml_sessions = us.get_provider_data('saml2') + session = saml_sessions.get_next_logout() + if session: + self.debug('Going to log out %s' % session.provider_id) + + try: + logout.setSessionFromDump(session.session.dump()) + except lasso.ProfileBadSessionDumpError as 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) + + 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) + + session.set_logoutstate(logout.msgUrl, logout.request.id, None) + us.save_provider_data('saml2', saml_sessions) + + 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. + + saml_sessions = us.get_provider_data('saml2') + if saml_sessions is None or saml_sessions.count() == 0: + self.error('Logout attempt without being loggged in.') + raise cherrypy.HTTPError(400, 'Not logged in') + + try: + session = saml_sessions.get_last_session() + except ValueError: + self.debug('SLO get_last_session() unable to find last session') + raise cherrypy.HTTPError(400, 'Unable to determine logout state') + + redirect = session.logoutstate.get('relaystate') + if not redirect: + redirect = self.basepath + + # Log out of cherrypy session + user = us.get_user() + self._audit('Logged out user: %s [%s] from %s' % + (user.name, user.fullname, + session.provider_id)) + us.logout(user) + + self.debug('SLO redirect to %s' % redirect) + + raise cherrypy.HTTPRedirect(redirect) |