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.DsInvalidSigalgError as e:
46 msg = 'Invalid SAML Request: missing or invalid signature ' \
49 raise InvalidRequest(msg)
50 except (lasso.ProfileInvalidProtocolprofileError,
52 msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request,
55 raise InvalidRequest(msg)
56 except lasso.Error as e:
57 self.error('SLO unknown error: %s' % message)
58 raise cherrypy.HTTPError(400, 'Invalid logout request')
60 session_indexes = logout.request.sessionIndexes
61 self.debug('SLO from %s with %s sessions' %
62 (logout.remoteProviderId, session_indexes))
64 # Find the first session being asked to log out. Later we loop over
65 # all the session indexes and mark them as logging out but only one
66 # is needed to handle the request.
67 if len(session_indexes) < 1:
68 self.error('SLO empty session Indexes')
69 raise cherrypy.HTTPError(400, 'Invalid logout request')
70 session = saml_sessions.get_session_by_id(session_indexes[0])
73 logout.setSessionFromDump(session.login_session)
74 except lasso.ProfileBadSessionDumpError as e:
75 self.error('loading session failed: %s' % e)
76 raise cherrypy.HTTPError(400, 'Invalid logout session')
78 return self._not_logged_in(logout, message)
81 logout.validateRequest()
82 except lasso.ProfileSessionNotFoundError, e:
83 self.error('Logout failed. No sessions for %s' %
84 logout.remoteProviderId)
85 return self._not_logged_in(logout, message)
86 except lasso.LogoutUnsupportedProfileError:
87 self.error('Logout failed. Unsupported profile %s' %
88 logout.remoteProviderId)
89 raise cherrypy.HTTPError(400, 'Profile does not support logout')
90 except lasso.Error, e:
91 self.error('SLO validation failed: %s' % e)
92 raise cherrypy.HTTPError(400, 'Failed to validate logout request')
95 logout.buildResponseMsg()
96 except lasso.ProfileUnsupportedProfileError:
97 self.error('Unsupported profile for %s' % logout.remoteProviderId)
98 raise cherrypy.HTTPError(400, 'Profile does not support logout')
99 except lasso.Error, e:
100 self.error('SLO failed to build logout response: %s' % e)
102 for ind in session_indexes:
103 session = saml_sessions.get_session_by_id(ind)
105 session.set_logoutstate(relaystate=logout.msgUrl,
107 saml_sessions.start_logout(session)
109 self.error('SLO request to log out non-existent session: %s' %
114 def _handle_logout_response(self, us, logout, saml_sessions, message,
117 self.debug('Logout response')
120 logout.processResponseMsg(message)
121 except getattr(lasso, 'ProfileRequestDeniedError',
122 lasso.LogoutRequestDeniedError):
123 self.error('Logout request denied by %s' %
124 logout.remoteProviderId)
125 # Fall through to next provider
126 except (lasso.ProfileInvalidMsgError,
127 lasso.LogoutPartialLogoutError) as e:
128 self.error('Logout request from %s failed: %s' %
129 (logout.remoteProviderId, e))
131 self.debug('Processing SLO Response from %s' %
132 logout.remoteProviderId)
134 self.debug('SLO response to request id %s' %
135 logout.response.inResponseTo)
137 session = saml_sessions.get_session_by_request_id(
138 logout.response.inResponseTo)
140 if session is not None:
141 self.debug('Logout response session logout id is: %s' %
143 saml_sessions.remove_session(session)
145 self._audit('Logged out user: %s [%s] from %s' %
146 (user.name, user.fullname,
147 logout.remoteProviderId))
149 return self._not_logged_in(logout, message)
153 def _not_logged_in(self, logout, message):
155 The user requested a logout but isn't logged in, or we can't
156 find a session for the user. Try to be nice and redirect them
157 back to the RelayState in the logout request.
159 We are only nice in the case of a valid logout request. If the
160 request is invalid (not signed, unknown SP, etc) then an
163 self.error('Logout attempt without being logged in.')
165 if logout.msgRelayState is not None:
166 raise cherrypy.HTTPRedirect(logout.msgRelayState)
169 logout.processRequestMsg(message)
170 except (lasso.ServerProviderNotFoundError,
171 lasso.ProfileUnknownProviderError) as e:
172 msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId,
175 raise UnknownProvider(msg)
176 except (lasso.ProfileInvalidProtocolprofileError,
178 msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request,
181 raise InvalidRequest(msg)
182 except lasso.Error, e:
183 self.error('SLO unknown error: %s' % message)
184 raise cherrypy.HTTPError(400, 'Invalid logout request')
186 if logout.msgRelayState:
187 raise cherrypy.HTTPRedirect(logout.msgRelayState)
189 raise cherrypy.HTTPError(400, 'Not logged in')
191 def _soap_logout(self, logout):
193 Send a SOAP logout request over HTTP and return the result.
195 headers = {'Content-Type': SOAP_MEDIA_TYPE}
197 response = requests.post(logout.msgUrl, data=logout.msgBody,
199 except Exception as e: # pylint: disable=broad-except
200 self.error('SOAP HTTP request failed: (%s) (on %s)' %
204 if response.status_code != 200:
205 self.error('SOAP error (%s) (on %s)' %
206 (response.status_code, logout.msgUrl))
207 raise InvalidRequest('SOAP HTTP error code %s' %
208 response.status_code)
210 if not response.text:
211 self.error('Empty SOAP response')
212 raise InvalidRequest('No content in SOAP response')
216 def logout(self, message, relaystate=None, samlresponse=None):
218 Handle HTTP logout. The supported logout methods are stored
219 in each session. First all the SOAP sessions are logged out
220 then the HTTP Redirect method is used for any remaining
223 The basic process is this:
224 1. A logout request is received. It is processed and the response
226 2. If any other SP's have also logged in as this user then the
227 first such session is popped off and a logout request is
228 generated and forwarded to the SP.
229 3. If a logout response is received then the user is marked
230 as logged out from that SP.
231 Repeat steps 2-3 until only the initial logout request is
232 left unhandled, at which time the pre-generated response is sent
233 back to the SP that originated the logout request.
235 The final logout response is always a redirect.
237 logout = self.cfg.idp.get_logout_handler()
241 saml_sessions = self.cfg.idp.sessionfactory
244 if lasso.SAML2_FIELD_REQUEST in message:
245 self._handle_logout_request(us, logout, saml_sessions,
248 self._handle_logout_response(us, logout, saml_sessions,
249 message, samlresponse)
251 raise cherrypy.HTTPError(400, 'Bad Request. Not a ' +
252 'logout request or response.')
253 except InvalidRequest as e:
254 raise cherrypy.HTTPError(400, 'Bad Request. %s' % e)
256 # Fall through to handle any remaining sessions.
258 # Find the next SP to logout and send a LogoutRequest
260 lasso.SAML2_METADATA_BINDING_SOAP,
261 lasso.SAML2_METADATA_BINDING_REDIRECT,
263 (logout_mech, session) = saml_sessions.get_next_logout(
264 logout_mechs=logout_order)
266 self.debug('Going to log out %s' % session.provider_id)
269 logout.setSessionFromDump(session.login_session)
270 except lasso.ProfileBadSessionDumpError as e:
271 self.error('Failed to load session: %s' % e)
272 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
274 if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT:
275 logout.initRequest(session.provider_id,
276 lasso.HTTP_METHOD_REDIRECT)
278 logout.initRequest(session.provider_id,
279 lasso.HTTP_METHOD_SOAP)
282 logout.buildRequestMsg()
283 except lasso.Error, e:
284 self.error('failure to build logout request msg: %s' % e)
285 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
288 # Set the full list of session indexes for this provider to
290 self.debug('logging out provider id %s' % session.provider_id)
291 indexes = saml_sessions.get_session_id_by_provider_id(
294 self.debug('Requesting logout for sessions %s' % (indexes,))
295 req = logout.get_request()
296 req.setSessionIndexes(indexes)
298 session.set_logoutstate(relaystate=logout.msgUrl,
299 request_id=logout.request.id)
300 saml_sessions.start_logout(session, initial=False)
302 self.debug('Request logout ID %s for session ID %s' %
303 (logout.request.id, session.session_id))
305 if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT:
306 self.debug('Redirecting to another SP to logout on %s at %s' %
307 (logout.remoteProviderId, logout.msgUrl))
308 raise cherrypy.HTTPRedirect(logout.msgUrl)
310 self.debug('SOAP request to another SP to logout on %s at %s' %
311 (logout.remoteProviderId, logout.msgUrl))
313 message = self._soap_logout(logout)
315 self._handle_logout_response(us,
320 except Exception as e: # pylint: disable=broad-except
321 self.error('SOAP SLO failed %s' % e)
323 self.error('Provider does not support SOAP')
325 (logout_mech, session) = saml_sessions.get_next_logout(
326 logout_mechs=logout_order)
330 # All sessions should be logged out now. Respond to the
331 # original request using the response we cached earlier.
334 session = saml_sessions.get_initial_logout()
336 self.debug('SLO get_last_session() unable to find last session')
337 raise cherrypy.HTTPError(400, 'Unable to determine logout state')
339 redirect = session.relaystate
341 redirect = self.basepath
343 saml_sessions.remove_session(session)
345 # Log out of cherrypy session
347 self._audit('Logged out user: %s [%s] from %s' %
348 (user.name, user.fullname,
349 session.provider_id))
352 self.debug('SLO redirect to %s' % redirect)
354 raise cherrypy.HTTPRedirect(redirect)