1 # Copyright (C) 2015 Rob Crittenden <rcritten@redhat.com>
3 # see file 'COPYING' for use and warranty information
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 from ipsilon.providers.common import ProviderPageBase
19 from ipsilon.providers.common import InvalidRequest
20 from ipsilon.providers.saml2.sessions import SAMLSessionsContainer
21 from ipsilon.providers.saml2.auth import UnknownProvider
22 from ipsilon.util.user import UserSession
27 class LogoutRequest(ProviderPageBase):
32 - On each logout a new session is created to represent that
34 - Initial logout request is verified and stored in the login
36 - If there are other sessions then one is chosen that is not
37 the current provider and a logoutRequest is sent
38 - When a logoutResponse is received the session is removed
39 - When all other sessions but the initial one have been
40 logged out then it a final logoutResponse is sent and the
41 session removed. At this point the cherrypy session is
45 def __init__(self, *args, **kwargs):
46 super(LogoutRequest, self).__init__(*args, **kwargs)
48 def _handle_logout_request(self, us, logout, saml_sessions, message):
49 self.debug('Logout request')
52 logout.processRequestMsg(message)
53 except (lasso.ServerProviderNotFoundError,
54 lasso.ProfileUnknownProviderError) as e:
55 msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId,
58 raise UnknownProvider(msg)
59 except (lasso.ProfileInvalidProtocolprofileError,
61 msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request,
64 raise InvalidRequest(msg)
65 except lasso.Error, e:
66 self.error('SLO unknown error: %s' % message)
67 raise cherrypy.HTTPError(400, 'Invalid logout request')
69 # TODO: verify that the session index is in the request
70 session_indexes = logout.request.sessionIndexes
71 self.debug('SLO from %s with %s sessions' %
72 (logout.remoteProviderId, session_indexes))
74 session = saml_sessions.find_session_by_provider(
75 logout.remoteProviderId)
78 logout.setSessionFromDump(session.session.dump())
79 except lasso.ProfileBadSessionDumpError as e:
80 self.error('loading session failed: %s' % e)
81 raise cherrypy.HTTPError(400, 'Invalid logout session')
83 self.error('Logout attempt without being loggged in.')
84 raise cherrypy.HTTPError(400, 'Not logged in')
87 logout.validateRequest()
88 except lasso.ProfileSessionNotFoundError, e:
89 self.error('Logout failed. No sessions for %s' %
90 logout.remoteProviderId)
91 raise cherrypy.HTTPError(400, 'Not logged in')
92 except lasso.LogoutUnsupportedProfileError:
93 self.error('Logout failed. Unsupported profile %s' %
94 logout.remoteProviderId)
95 raise cherrypy.HTTPError(400, 'Profile does not support logout')
96 except lasso.Error, e:
97 self.error('SLO validation failed: %s' % e)
98 raise cherrypy.HTTPError(400, 'Failed to validate logout request')
101 logout.buildResponseMsg()
102 except lasso.ProfileUnsupportedProfileError:
103 self.error('Unsupported profile for %s' % logout.remoteProviderId)
104 raise cherrypy.HTTPError(400, 'Profile does not support logout')
105 except lasso.Error, e:
106 self.error('SLO failed to build logout response: %s' % e)
108 session.set_logoutstate(logout.msgUrl, logout.request.id,
110 saml_sessions.start_logout(session)
112 us.save_provider_data('saml2', saml_sessions)
116 def _handle_logout_response(self, us, logout, saml_sessions, message,
119 self.debug('Logout response')
122 logout.processResponseMsg(message)
123 except getattr(lasso, 'ProfileRequestDeniedError',
124 lasso.LogoutRequestDeniedError):
125 self.error('Logout request denied by %s' %
126 logout.remoteProviderId)
127 # Fall through to next provider
128 except (lasso.ProfileInvalidMsgError,
129 lasso.LogoutPartialLogoutError) as e:
130 self.error('Logout request from %s failed: %s' %
131 (logout.remoteProviderId, e))
133 self.debug('Processing SLO Response from %s' %
134 logout.remoteProviderId)
136 self.debug('SLO response to request id %s' %
137 logout.response.inResponseTo)
139 saml_sessions = us.get_provider_data('saml2')
140 if saml_sessions is None:
141 # TODO: return logged out instead
142 saml_sessions = SAMLSessionsContainer()
144 # TODO: need to log out each SessionIndex?
145 session = saml_sessions.find_session_by_provider(
146 logout.remoteProviderId)
148 if session is not None:
149 self.debug('Logout response session logout id is: %s' %
151 saml_sessions.remove_session_by_provider(
152 logout.remoteProviderId)
153 us.save_provider_data('saml2', saml_sessions)
155 self._audit('Logged out user: %s [%s] from %s' %
156 (user.name, user.fullname,
157 logout.remoteProviderId))
159 self.error('Logout attempt without being loggged in.')
160 raise cherrypy.HTTPError(400, 'Not logged in')
164 def logout(self, message, relaystate=None, samlresponse=None):
166 Handle HTTP Redirect logout. This is an asynchronous logout
167 request process that relies on the HTTP agent to forward
168 logout requests to any other SP's that are also logged in.
170 The basic process is this:
171 1. A logout request is received. It is processed and the response
173 2. If any other SP's have also logged in as this user then the
174 first such session is popped off and a logout request is
175 generated and forwarded to the SP.
176 3. If a logout response is received then the user is marked
177 as logged out from that SP.
178 Repeat steps 2-3 until only the initial logout request is
179 left unhandled, at which time the pre-generated response is sent
180 back to the SP that originated the logout request.
182 logout = self.cfg.idp.get_logout_handler()
186 saml_sessions = us.get_provider_data('saml2')
187 if saml_sessions is None:
188 # No sessions means nothing to log out
189 self.error('Logout attempt without being loggged in.')
190 raise cherrypy.HTTPError(400, 'Not logged in')
192 self.debug('%d sessions loaded' % saml_sessions.count())
195 if lasso.SAML2_FIELD_REQUEST in message:
196 self._handle_logout_request(us, logout, saml_sessions, message)
198 self._handle_logout_response(us, logout, saml_sessions, message,
201 raise cherrypy.HTTPRedirect(400, 'Bad Request. Not a logout ' +
202 'request or response.')
204 # Fall through to handle any remaining sessions.
206 # Find the next SP to logout and send a LogoutRequest
207 saml_sessions = us.get_provider_data('saml2')
208 session = saml_sessions.get_next_logout()
210 self.debug('Going to log out %s' % session.provider_id)
213 logout.setSessionFromDump(session.session.dump())
214 except lasso.ProfileBadSessionDumpError as e:
215 self.error('Failed to load session: %s' % e)
216 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
219 logout.initRequest(session.provider_id, lasso.HTTP_METHOD_REDIRECT)
222 logout.buildRequestMsg()
223 except lasso.Error, e:
224 self.error('failure to build logout request msg: %s' % e)
225 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
228 session.set_logoutstate(logout.msgUrl, logout.request.id, None)
229 us.save_provider_data('saml2', saml_sessions)
231 self.debug('Request logout ID %s for session ID %s' %
232 (logout.request.id, session.session_id))
233 self.debug('Redirecting to another SP to logout on %s at %s' %
234 (logout.remoteProviderId, logout.msgUrl))
236 raise cherrypy.HTTPRedirect(logout.msgUrl)
238 # Otherwise we're done, respond to the original request using the
239 # response we cached earlier.
241 saml_sessions = us.get_provider_data('saml2')
242 if saml_sessions is None or saml_sessions.count() == 0:
243 self.error('Logout attempt without being loggged in.')
244 raise cherrypy.HTTPError(400, 'Not logged in')
247 session = saml_sessions.get_last_session()
249 self.debug('SLO get_last_session() unable to find last session')
250 raise cherrypy.HTTPError(400, 'Unable to determine logout state')
252 redirect = session.logoutstate.get('relaystate')
254 redirect = self.basepath
256 # Log out of cherrypy session
258 self._audit('Logged out user: %s [%s] from %s' %
259 (user.name, user.fullname,
260 session.provider_id))
263 self.debug('SLO redirect to %s' % redirect)
265 raise cherrypy.HTTPRedirect(redirect)