1 # Copyright (C) 2015 Ipsilon project Contributors, for license see COPYING
3 from ipsilon.providers.common import ProviderPageBase
4 from ipsilon.providers.common import InvalidRequest
5 from ipsilon.providers.saml2.auth import UnknownProvider
6 from ipsilon.util.user import UserSession
7 from ipsilon.util.constants import SOAP_MEDIA_TYPE
13 class LogoutRequest(ProviderPageBase):
18 - On each logout a new session is created to represent that
20 - Initial logout request is verified and stored in the login
22 - If there are other sessions then one is chosen that is not
23 the current provider and a logoutRequest is sent
24 - When a logoutResponse is received the session is removed
25 - When all other sessions but the initial one have been
26 logged out then it a final logoutResponse is sent and the
27 session removed. At this point the cherrypy session is
31 def __init__(self, site, provider, *args, **kwargs):
32 super(LogoutRequest, self).__init__(site, provider)
34 def _handle_logout_request(self, us, logout, saml_sessions, message):
35 self.debug('Logout request')
38 logout.processRequestMsg(message)
39 except (lasso.ServerProviderNotFoundError,
40 lasso.ProfileUnknownProviderError) as e:
41 msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId,
44 raise UnknownProvider(msg)
45 except (lasso.ProfileInvalidProtocolprofileError,
47 msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request,
50 raise InvalidRequest(msg)
51 except lasso.Error, e:
52 self.error('SLO unknown error: %s' % message)
53 raise cherrypy.HTTPError(400, 'Invalid logout request')
55 session_indexes = logout.request.sessionIndexes
56 self.debug('SLO from %s with %s sessions' %
57 (logout.remoteProviderId, session_indexes))
59 # Find the first session being asked to log out. Later we loop over
60 # all the session indexes and mark them as logging out but only one
61 # is needed to handle the request.
62 if len(session_indexes) < 1:
63 self.error('SLO empty session Indexes')
64 raise cherrypy.HTTPError(400, 'Invalid logout request')
65 session = saml_sessions.get_session_by_id(session_indexes[0])
68 logout.setSessionFromDump(session.login_session)
69 except lasso.ProfileBadSessionDumpError as e:
70 self.error('loading session failed: %s' % e)
71 raise cherrypy.HTTPError(400, 'Invalid logout session')
73 return self._not_logged_in(logout, message)
76 logout.validateRequest()
77 except lasso.ProfileSessionNotFoundError, e:
78 self.error('Logout failed. No sessions for %s' %
79 logout.remoteProviderId)
80 return self._not_logged_in(logout, message)
81 except lasso.LogoutUnsupportedProfileError:
82 self.error('Logout failed. Unsupported profile %s' %
83 logout.remoteProviderId)
84 raise cherrypy.HTTPError(400, 'Profile does not support logout')
85 except lasso.Error, e:
86 self.error('SLO validation failed: %s' % e)
87 raise cherrypy.HTTPError(400, 'Failed to validate logout request')
90 logout.buildResponseMsg()
91 except lasso.ProfileUnsupportedProfileError:
92 self.error('Unsupported profile for %s' % logout.remoteProviderId)
93 raise cherrypy.HTTPError(400, 'Profile does not support logout')
94 except lasso.Error, e:
95 self.error('SLO failed to build logout response: %s' % e)
97 for ind in session_indexes:
98 session = saml_sessions.get_session_by_id(ind)
100 session.set_logoutstate(relaystate=logout.msgUrl,
102 saml_sessions.start_logout(session)
104 self.error('SLO request to log out non-existent session: %s' %
109 def _handle_logout_response(self, us, logout, saml_sessions, message,
112 self.debug('Logout response')
115 logout.processResponseMsg(message)
116 except getattr(lasso, 'ProfileRequestDeniedError',
117 lasso.LogoutRequestDeniedError):
118 self.error('Logout request denied by %s' %
119 logout.remoteProviderId)
120 # Fall through to next provider
121 except (lasso.ProfileInvalidMsgError,
122 lasso.LogoutPartialLogoutError) as e:
123 self.error('Logout request from %s failed: %s' %
124 (logout.remoteProviderId, e))
126 self.debug('Processing SLO Response from %s' %
127 logout.remoteProviderId)
129 self.debug('SLO response to request id %s' %
130 logout.response.inResponseTo)
132 session = saml_sessions.get_session_by_request_id(
133 logout.response.inResponseTo)
135 if session is not None:
136 self.debug('Logout response session logout id is: %s' %
138 saml_sessions.remove_session(session)
140 self._audit('Logged out user: %s [%s] from %s' %
141 (user.name, user.fullname,
142 logout.remoteProviderId))
144 return self._not_logged_in(logout, message)
148 def _not_logged_in(self, logout, message):
150 The user requested a logout but isn't logged in, or we can't
151 find a session for the user. Try to be nice and redirect them
152 back to the RelayState in the logout request.
154 We are only nice in the case of a valid logout request. If the
155 request is invalid (not signed, unknown SP, etc) then an
158 self.error('Logout attempt without being logged in.')
160 if logout.msgRelayState is not None:
161 raise cherrypy.HTTPRedirect(logout.msgRelayState)
164 logout.processRequestMsg(message)
165 except (lasso.ServerProviderNotFoundError,
166 lasso.ProfileUnknownProviderError) as e:
167 msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId,
170 raise UnknownProvider(msg)
171 except (lasso.ProfileInvalidProtocolprofileError,
173 msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request,
176 raise InvalidRequest(msg)
177 except lasso.Error, e:
178 self.error('SLO unknown error: %s' % message)
179 raise cherrypy.HTTPError(400, 'Invalid logout request')
181 if logout.msgRelayState:
182 raise cherrypy.HTTPRedirect(logout.msgRelayState)
184 raise cherrypy.HTTPError(400, 'Not logged in')
186 def _soap_logout(self, logout):
188 Send a SOAP logout request over HTTP and return the result.
190 headers = {'Content-Type': SOAP_MEDIA_TYPE}
192 response = requests.post(logout.msgUrl, data=logout.msgBody,
194 except Exception as e: # pylint: disable=broad-except
195 self.error('SOAP HTTP request failed: (%s) (on %s)' %
199 if response.status_code != 200:
200 self.error('SOAP error (%s) (on %s)' %
201 (response.status_code, logout.msgUrl))
202 raise InvalidRequest('SOAP HTTP error code %s' %
203 response.status_code)
205 if not response.text:
206 self.error('Empty SOAP response')
207 raise InvalidRequest('No content in SOAP response')
211 def logout(self, message, relaystate=None, samlresponse=None):
213 Handle HTTP logout. The supported logout methods are stored
214 in each session. First all the SOAP sessions are logged out
215 then the HTTP Redirect method is used for any remaining
218 The basic process is this:
219 1. A logout request is received. It is processed and the response
221 2. If any other SP's have also logged in as this user then the
222 first such session is popped off and a logout request is
223 generated and forwarded to the SP.
224 3. If a logout response is received then the user is marked
225 as logged out from that SP.
226 Repeat steps 2-3 until only the initial logout request is
227 left unhandled, at which time the pre-generated response is sent
228 back to the SP that originated the logout request.
230 The final logout response is always a redirect.
232 logout = self.cfg.idp.get_logout_handler()
236 saml_sessions = self.cfg.idp.sessionfactory
238 if lasso.SAML2_FIELD_REQUEST in message:
239 self._handle_logout_request(us, logout, saml_sessions, message)
241 self._handle_logout_response(us, logout, saml_sessions, message,
244 raise cherrypy.HTTPRedirect(400, 'Bad Request. Not a logout ' +
245 'request or response.')
247 # Fall through to handle any remaining sessions.
249 # Find the next SP to logout and send a LogoutRequest
251 lasso.SAML2_METADATA_BINDING_SOAP,
252 lasso.SAML2_METADATA_BINDING_REDIRECT,
254 (logout_mech, session) = saml_sessions.get_next_logout(
255 logout_mechs=logout_order)
257 self.debug('Going to log out %s' % session.provider_id)
260 logout.setSessionFromDump(session.login_session)
261 except lasso.ProfileBadSessionDumpError as e:
262 self.error('Failed to load session: %s' % e)
263 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
265 if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT:
266 logout.initRequest(session.provider_id,
267 lasso.HTTP_METHOD_REDIRECT)
269 logout.initRequest(session.provider_id,
270 lasso.HTTP_METHOD_SOAP)
273 logout.buildRequestMsg()
274 except lasso.Error, e:
275 self.error('failure to build logout request msg: %s' % e)
276 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
279 # Set the full list of session indexes for this provider to
281 self.debug('logging out provider id %s' % session.provider_id)
282 indexes = saml_sessions.get_session_id_by_provider_id(
285 self.debug('Requesting logout for sessions %s' % (indexes,))
286 req = logout.get_request()
287 req.setSessionIndexes(indexes)
289 session.set_logoutstate(relaystate=logout.msgUrl,
290 request_id=logout.request.id)
291 saml_sessions.start_logout(session, initial=False)
293 self.debug('Request logout ID %s for session ID %s' %
294 (logout.request.id, session.session_id))
296 if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT:
297 self.debug('Redirecting to another SP to logout on %s at %s' %
298 (logout.remoteProviderId, logout.msgUrl))
299 raise cherrypy.HTTPRedirect(logout.msgUrl)
301 self.debug('SOAP request to another SP to logout on %s at %s' %
302 (logout.remoteProviderId, logout.msgUrl))
304 message = self._soap_logout(logout)
306 self._handle_logout_response(us,
311 except Exception as e: # pylint: disable=broad-except
312 self.error('SOAP SLO failed %s' % e)
314 self.error('Provider does not support SOAP')
316 (logout_mech, session) = saml_sessions.get_next_logout(
317 logout_mechs=logout_order)
321 # All sessions should be logged out now. Respond to the
322 # original request using the response we cached earlier.
325 session = saml_sessions.get_initial_logout()
327 self.debug('SLO get_last_session() unable to find last session')
328 raise cherrypy.HTTPError(400, 'Unable to determine logout state')
330 redirect = session.relaystate
332 redirect = self.basepath
334 saml_sessions.remove_session(session)
336 # Log out of cherrypy session
338 self._audit('Logged out user: %s [%s] from %s' %
339 (user.name, user.fullname,
340 session.provider_id))
343 self.debug('SLO redirect to %s' % redirect)
345 raise cherrypy.HTTPRedirect(redirect)