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, *args, **kwargs):
32 super(LogoutRequest, self).__init__(*args, **kwargs)
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, logout.msgUrl))
202 raise InvalidRequest('SOAP HTTP error code', response.status_code)
204 if not response.text:
205 self.error('Empty SOAP response')
206 raise InvalidRequest('No content in SOAP response')
210 def logout(self, message, relaystate=None, samlresponse=None):
212 Handle HTTP logout. The supported logout methods are stored
213 in each session. First all the SOAP sessions are logged out
214 then the HTTP Redirect method is used for any remaining
217 The basic process is this:
218 1. A logout request is received. It is processed and the response
220 2. If any other SP's have also logged in as this user then the
221 first such session is popped off and a logout request is
222 generated and forwarded to the SP.
223 3. If a logout response is received then the user is marked
224 as logged out from that SP.
225 Repeat steps 2-3 until only the initial logout request is
226 left unhandled, at which time the pre-generated response is sent
227 back to the SP that originated the logout request.
229 The final logout response is always a redirect.
231 logout = self.cfg.idp.get_logout_handler()
235 saml_sessions = self.cfg.idp.sessionfactory
237 if lasso.SAML2_FIELD_REQUEST in message:
238 self._handle_logout_request(us, logout, saml_sessions, message)
240 self._handle_logout_response(us, logout, saml_sessions, message,
243 raise cherrypy.HTTPRedirect(400, 'Bad Request. Not a logout ' +
244 'request or response.')
246 # Fall through to handle any remaining sessions.
248 # Find the next SP to logout and send a LogoutRequest
250 lasso.SAML2_METADATA_BINDING_SOAP,
251 lasso.SAML2_METADATA_BINDING_REDIRECT,
253 (logout_mech, session) = saml_sessions.get_next_logout(
254 logout_mechs=logout_order)
256 self.debug('Going to log out %s' % session.provider_id)
259 logout.setSessionFromDump(session.login_session)
260 except lasso.ProfileBadSessionDumpError as e:
261 self.error('Failed to load session: %s' % e)
262 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
264 if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT:
265 logout.initRequest(session.provider_id,
266 lasso.HTTP_METHOD_REDIRECT)
268 logout.initRequest(session.provider_id,
269 lasso.HTTP_METHOD_SOAP)
272 logout.buildRequestMsg()
273 except lasso.Error, e:
274 self.error('failure to build logout request msg: %s' % e)
275 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
278 # Set the full list of session indexes for this provider to
280 self.debug('logging out provider id %s' % session.provider_id)
281 indexes = saml_sessions.get_session_id_by_provider_id(
284 self.debug('Requesting logout for sessions %s' % (indexes,))
285 req = logout.get_request()
286 req.setSessionIndexes(indexes)
288 session.set_logoutstate(relaystate=logout.msgUrl,
289 request_id=logout.request.id)
290 saml_sessions.start_logout(session, initial=False)
292 self.debug('Request logout ID %s for session ID %s' %
293 (logout.request.id, session.session_id))
295 if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT:
296 self.debug('Redirecting to another SP to logout on %s at %s' %
297 (logout.remoteProviderId, logout.msgUrl))
298 raise cherrypy.HTTPRedirect(logout.msgUrl)
300 self.debug('SOAP request to another SP to logout on %s at %s' %
301 (logout.remoteProviderId, logout.msgUrl))
303 message = self._soap_logout(logout)
305 self._handle_logout_response(us,
310 except Exception as e: # pylint: disable=broad-except
311 self.error('SOAP SLO failed %s' % e)
313 self.error('Provider does not support SOAP')
315 (logout_mech, session) = saml_sessions.get_next_logout(
316 logout_mechs=logout_order)
320 # All sessions should be logged out now. Respond to the
321 # original request using the response we cached earlier.
324 session = saml_sessions.get_initial_logout()
326 self.debug('SLO get_last_session() unable to find last session')
327 raise cherrypy.HTTPError(400, 'Unable to determine logout state')
329 redirect = session.relaystate
331 redirect = self.basepath
333 saml_sessions.remove_session(session)
335 # Log out of cherrypy session
337 self._audit('Logged out user: %s [%s] from %s' %
338 (user.name, user.fullname,
339 session.provider_id))
342 self.debug('SLO redirect to %s' % redirect)
344 raise cherrypy.HTTPRedirect(redirect)