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
11 class LogoutRequest(ProviderPageBase):
16 - On each logout a new session is created to represent that
18 - Initial logout request is verified and stored in the login
20 - If there are other sessions then one is chosen that is not
21 the current provider and a logoutRequest is sent
22 - When a logoutResponse is received the session is removed
23 - When all other sessions but the initial one have been
24 logged out then it a final logoutResponse is sent and the
25 session removed. At this point the cherrypy session is
29 def __init__(self, *args, **kwargs):
30 super(LogoutRequest, self).__init__(*args, **kwargs)
32 def _handle_logout_request(self, us, logout, saml_sessions, message):
33 self.debug('Logout request')
36 logout.processRequestMsg(message)
37 except (lasso.ServerProviderNotFoundError,
38 lasso.ProfileUnknownProviderError) as e:
39 msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId,
42 raise UnknownProvider(msg)
43 except (lasso.ProfileInvalidProtocolprofileError,
45 msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request,
48 raise InvalidRequest(msg)
49 except lasso.Error, e:
50 self.error('SLO unknown error: %s' % message)
51 raise cherrypy.HTTPError(400, 'Invalid logout request')
53 session_indexes = logout.request.sessionIndexes
54 self.debug('SLO from %s with %s sessions' %
55 (logout.remoteProviderId, session_indexes))
57 # Find the first session being asked to log out. Later we loop over
58 # all the session indexes and mark them as logging out but only one
59 # is needed to handle the request.
60 if len(session_indexes) < 1:
61 self.error('SLO empty session Indexes: %s')
62 raise cherrypy.HTTPError(400, 'Invalid logout request')
63 session = saml_sessions.get_session_by_id(session_indexes[0])
66 logout.setSessionFromDump(session.login_session)
67 except lasso.ProfileBadSessionDumpError as e:
68 self.error('loading session failed: %s' % e)
69 raise cherrypy.HTTPError(400, 'Invalid logout session')
71 return self._not_logged_in(logout, message)
74 logout.validateRequest()
75 except lasso.ProfileSessionNotFoundError, e:
76 self.error('Logout failed. No sessions for %s' %
77 logout.remoteProviderId)
78 return self._not_logged_in(logout, message)
79 except lasso.LogoutUnsupportedProfileError:
80 self.error('Logout failed. Unsupported profile %s' %
81 logout.remoteProviderId)
82 raise cherrypy.HTTPError(400, 'Profile does not support logout')
83 except lasso.Error, e:
84 self.error('SLO validation failed: %s' % e)
85 raise cherrypy.HTTPError(400, 'Failed to validate logout request')
88 logout.buildResponseMsg()
89 except lasso.ProfileUnsupportedProfileError:
90 self.error('Unsupported profile for %s' % logout.remoteProviderId)
91 raise cherrypy.HTTPError(400, 'Profile does not support logout')
92 except lasso.Error, e:
93 self.error('SLO failed to build logout response: %s' % e)
95 for ind in session_indexes:
96 session = saml_sessions.get_session_by_id(ind)
98 session.set_logoutstate(relaystate=logout.msgUrl,
100 saml_sessions.start_logout(session)
102 self.error('SLO request to log out non-existent session: %s' %
107 def _handle_logout_response(self, us, logout, saml_sessions, message,
110 self.debug('Logout response')
113 logout.processResponseMsg(message)
114 except getattr(lasso, 'ProfileRequestDeniedError',
115 lasso.LogoutRequestDeniedError):
116 self.error('Logout request denied by %s' %
117 logout.remoteProviderId)
118 # Fall through to next provider
119 except (lasso.ProfileInvalidMsgError,
120 lasso.LogoutPartialLogoutError) as e:
121 self.error('Logout request from %s failed: %s' %
122 (logout.remoteProviderId, e))
124 self.debug('Processing SLO Response from %s' %
125 logout.remoteProviderId)
127 self.debug('SLO response to request id %s' %
128 logout.response.inResponseTo)
130 session = saml_sessions.get_session_by_request_id(
131 logout.response.inResponseTo)
133 if session is not None:
134 self.debug('Logout response session logout id is: %s' %
136 saml_sessions.remove_session(session)
138 self._audit('Logged out user: %s [%s] from %s' %
139 (user.name, user.fullname,
140 logout.remoteProviderId))
142 return self._not_logged_in(logout, message)
146 def _not_logged_in(self, logout, message):
148 The user requested a logout but isn't logged in, or we can't
149 find a session for the user. Try to be nice and redirect them
150 back to the RelayState in the logout request.
152 We are only nice in the case of a valid logout request. If the
153 request is invalid (not signed, unknown SP, etc) then an
156 self.error('Logout attempt without being logged in.')
158 if logout.msgRelayState is not None:
159 raise cherrypy.HTTPRedirect(logout.msgRelayState)
162 logout.processRequestMsg(message)
163 except (lasso.ServerProviderNotFoundError,
164 lasso.ProfileUnknownProviderError) as e:
165 msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId,
168 raise UnknownProvider(msg)
169 except (lasso.ProfileInvalidProtocolprofileError,
171 msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request,
174 raise InvalidRequest(msg)
175 except lasso.Error, e:
176 self.error('SLO unknown error: %s' % message)
177 raise cherrypy.HTTPError(400, 'Invalid logout request')
179 if logout.msgRelayState:
180 raise cherrypy.HTTPRedirect(logout.msgRelayState)
182 raise cherrypy.HTTPError(400, 'Not logged in')
184 def logout(self, message, relaystate=None, samlresponse=None):
186 Handle HTTP Redirect logout. This is an asynchronous logout
187 request process that relies on the HTTP agent to forward
188 logout requests to any other SP's that are also logged in.
190 The basic process is this:
191 1. A logout request is received. It is processed and the response
193 2. If any other SP's have also logged in as this user then the
194 first such session is popped off and a logout request is
195 generated and forwarded to the SP.
196 3. If a logout response is received then the user is marked
197 as logged out from that SP.
198 Repeat steps 2-3 until only the initial logout request is
199 left unhandled, at which time the pre-generated response is sent
200 back to the SP that originated the logout request.
202 logout = self.cfg.idp.get_logout_handler()
206 saml_sessions = self.cfg.idp.sessionfactory
208 if lasso.SAML2_FIELD_REQUEST in message:
209 self._handle_logout_request(us, logout, saml_sessions, message)
211 self._handle_logout_response(us, logout, saml_sessions, message,
214 raise cherrypy.HTTPRedirect(400, 'Bad Request. Not a logout ' +
215 'request or response.')
217 # Fall through to handle any remaining sessions.
219 # Find the next SP to logout and send a LogoutRequest
220 session = saml_sessions.get_next_logout()
222 self.debug('Going to log out %s' % session.provider_id)
225 logout.setSessionFromDump(session.login_session)
226 except lasso.ProfileBadSessionDumpError as e:
227 self.error('Failed to load session: %s' % e)
228 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
231 logout.initRequest(session.provider_id, lasso.HTTP_METHOD_REDIRECT)
234 logout.buildRequestMsg()
235 except lasso.Error, e:
236 self.error('failure to build logout request msg: %s' % e)
237 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
240 # Set the full list of session indexes for this provider to
242 self.debug('logging out provider id %s' % session.provider_id)
243 indexes = saml_sessions.get_session_id_by_provider_id(
246 self.debug('Requesting logout for sessions %s' % indexes)
247 req = logout.get_request()
248 req.setSessionIndexes(indexes)
250 session.set_logoutstate(relaystate=logout.msgUrl,
251 request_id=logout.request.id)
252 saml_sessions.start_logout(session, initial=False)
254 self.debug('Request logout ID %s for session ID %s' %
255 (logout.request.id, session.session_id))
256 self.debug('Redirecting to another SP to logout on %s at %s' %
257 (logout.remoteProviderId, logout.msgUrl))
259 raise cherrypy.HTTPRedirect(logout.msgUrl)
261 # Otherwise we're done, respond to the original request using the
262 # response we cached earlier.
265 session = saml_sessions.get_initial_logout()
267 self.debug('SLO get_last_session() unable to find last session')
268 raise cherrypy.HTTPError(400, 'Unable to determine logout state')
270 redirect = session.relaystate
272 redirect = self.basepath
274 saml_sessions.remove_session(session)
276 # Log out of cherrypy session
278 self._audit('Logged out user: %s [%s] from %s' %
279 (user.name, user.fullname,
280 session.provider_id))
283 self.debug('SLO redirect to %s' % redirect)
285 raise cherrypy.HTTPRedirect(redirect)