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 return self._not_logged_in(logout, message)
86 logout.validateRequest()
87 except lasso.ProfileSessionNotFoundError, e:
88 self.error('Logout failed. No sessions for %s' %
89 logout.remoteProviderId)
90 return self._not_logged_in(logout, message)
91 except lasso.LogoutUnsupportedProfileError:
92 self.error('Logout failed. Unsupported profile %s' %
93 logout.remoteProviderId)
94 raise cherrypy.HTTPError(400, 'Profile does not support logout')
95 except lasso.Error, e:
96 self.error('SLO validation failed: %s' % e)
97 raise cherrypy.HTTPError(400, 'Failed to validate logout request')
100 logout.buildResponseMsg()
101 except lasso.ProfileUnsupportedProfileError:
102 self.error('Unsupported profile for %s' % logout.remoteProviderId)
103 raise cherrypy.HTTPError(400, 'Profile does not support logout')
104 except lasso.Error, e:
105 self.error('SLO failed to build logout response: %s' % e)
107 session.set_logoutstate(logout.msgUrl, logout.request.id,
109 saml_sessions.start_logout(session)
111 us.save_provider_data('saml2', saml_sessions)
115 def _handle_logout_response(self, us, logout, saml_sessions, message,
118 self.debug('Logout response')
121 logout.processResponseMsg(message)
122 except getattr(lasso, 'ProfileRequestDeniedError',
123 lasso.LogoutRequestDeniedError):
124 self.error('Logout request denied by %s' %
125 logout.remoteProviderId)
126 # Fall through to next provider
127 except (lasso.ProfileInvalidMsgError,
128 lasso.LogoutPartialLogoutError) as e:
129 self.error('Logout request from %s failed: %s' %
130 (logout.remoteProviderId, e))
132 self.debug('Processing SLO Response from %s' %
133 logout.remoteProviderId)
135 self.debug('SLO response to request id %s' %
136 logout.response.inResponseTo)
138 saml_sessions = us.get_provider_data('saml2')
139 if saml_sessions is None:
140 # TODO: return logged out instead
141 saml_sessions = SAMLSessionsContainer()
143 # TODO: need to log out each SessionIndex?
144 session = saml_sessions.find_session_by_provider(
145 logout.remoteProviderId)
147 if session is not None:
148 self.debug('Logout response session logout id is: %s' %
150 saml_sessions.remove_session_by_provider(
151 logout.remoteProviderId)
152 us.save_provider_data('saml2', saml_sessions)
154 self._audit('Logged out user: %s [%s] from %s' %
155 (user.name, user.fullname,
156 logout.remoteProviderId))
158 return self._not_logged_in(logout, message)
162 def _not_logged_in(self, logout, message):
164 The user requested a logout but isn't logged in, or we can't
165 find a session for the user. Try to be nice and redirect them
166 back to the RelayState in the logout request.
168 We are only nice in the case of a valid logout request. If the
169 request is invalid (not signed, unknown SP, etc) then an
172 self.error('Logout attempt without being logged in.')
174 if logout.msgRelayState is not None:
175 raise cherrypy.HTTPRedirect(logout.msgRelayState)
178 logout.processRequestMsg(message)
179 except (lasso.ServerProviderNotFoundError,
180 lasso.ProfileUnknownProviderError) as e:
181 msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId,
184 raise UnknownProvider(msg)
185 except (lasso.ProfileInvalidProtocolprofileError,
187 msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request,
190 raise InvalidRequest(msg)
191 except lasso.Error, e:
192 self.error('SLO unknown error: %s' % message)
193 raise cherrypy.HTTPError(400, 'Invalid logout request')
195 if logout.msgRelayState:
196 raise cherrypy.HTTPRedirect(logout.msgRelayState)
198 raise cherrypy.HTTPError(400, 'Not logged in')
200 def logout(self, message, relaystate=None, samlresponse=None):
202 Handle HTTP Redirect logout. This is an asynchronous logout
203 request process that relies on the HTTP agent to forward
204 logout requests to any other SP's that are also logged in.
206 The basic process is this:
207 1. A logout request is received. It is processed and the response
209 2. If any other SP's have also logged in as this user then the
210 first such session is popped off and a logout request is
211 generated and forwarded to the SP.
212 3. If a logout response is received then the user is marked
213 as logged out from that SP.
214 Repeat steps 2-3 until only the initial logout request is
215 left unhandled, at which time the pre-generated response is sent
216 back to the SP that originated the logout request.
218 logout = self.cfg.idp.get_logout_handler()
222 saml_sessions = us.get_provider_data('saml2')
223 if saml_sessions is None:
224 # No sessions means nothing to log out
225 return self._not_logged_in(logout, message)
227 self.debug('%d sessions loaded' % saml_sessions.count())
230 if lasso.SAML2_FIELD_REQUEST in message:
231 self._handle_logout_request(us, logout, saml_sessions, message)
233 self._handle_logout_response(us, logout, saml_sessions, message,
236 raise cherrypy.HTTPRedirect(400, 'Bad Request. Not a logout ' +
237 'request or response.')
239 # Fall through to handle any remaining sessions.
241 # Find the next SP to logout and send a LogoutRequest
242 saml_sessions = us.get_provider_data('saml2')
243 session = saml_sessions.get_next_logout()
245 self.debug('Going to log out %s' % session.provider_id)
248 logout.setSessionFromDump(session.session.dump())
249 except lasso.ProfileBadSessionDumpError as e:
250 self.error('Failed to load session: %s' % e)
251 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
254 logout.initRequest(session.provider_id, lasso.HTTP_METHOD_REDIRECT)
257 logout.buildRequestMsg()
258 except lasso.Error, e:
259 self.error('failure to build logout request msg: %s' % e)
260 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
263 # Now set the full list of session indexes to log out
264 req = logout.get_request()
265 req.setSessionIndexes(tuple(set(session.session_indexes)))
267 session.set_logoutstate(logout.msgUrl, logout.request.id, None)
268 us.save_provider_data('saml2', saml_sessions)
270 self.debug('Request logout ID %s for session ID %s' %
271 (logout.request.id, session.session_id))
272 self.debug('Redirecting to another SP to logout on %s at %s' %
273 (logout.remoteProviderId, logout.msgUrl))
275 raise cherrypy.HTTPRedirect(logout.msgUrl)
277 # Otherwise we're done, respond to the original request using the
278 # response we cached earlier.
280 saml_sessions = us.get_provider_data('saml2')
281 if saml_sessions is None or saml_sessions.count() == 0:
282 return self._not_logged_in(logout, message)
285 session = saml_sessions.get_last_session()
287 self.debug('SLO get_last_session() unable to find last session')
288 raise cherrypy.HTTPError(400, 'Unable to determine logout state')
290 redirect = session.logoutstate.get('relaystate')
292 redirect = self.basepath
294 # Log out of cherrypy session
296 self._audit('Logged out user: %s [%s] from %s' %
297 (user.name, user.fullname,
298 session.provider_id))
301 self.debug('SLO redirect to %s' % redirect)
303 raise cherrypy.HTTPRedirect(redirect)