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.sessions import SAMLSessionFactory
6 from ipsilon.providers.saml2.auth import UnknownProvider
7 from ipsilon.util.user import UserSession
12 class LogoutRequest(ProviderPageBase):
17 - On each logout a new session is created to represent that
19 - Initial logout request is verified and stored in the login
21 - If there are other sessions then one is chosen that is not
22 the current provider and a logoutRequest is sent
23 - When a logoutResponse is received the session is removed
24 - When all other sessions but the initial one have been
25 logged out then it a final logoutResponse is sent and the
26 session removed. At this point the cherrypy session is
30 def __init__(self, *args, **kwargs):
31 super(LogoutRequest, self).__init__(*args, **kwargs)
33 def _handle_logout_request(self, us, logout, saml_sessions, message):
34 self.debug('Logout request')
37 logout.processRequestMsg(message)
38 except (lasso.ServerProviderNotFoundError,
39 lasso.ProfileUnknownProviderError) as e:
40 msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId,
43 raise UnknownProvider(msg)
44 except (lasso.ProfileInvalidProtocolprofileError,
46 msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request,
49 raise InvalidRequest(msg)
50 except lasso.Error, e:
51 self.error('SLO unknown error: %s' % message)
52 raise cherrypy.HTTPError(400, 'Invalid logout request')
54 session_indexes = logout.request.sessionIndexes
55 self.debug('SLO from %s with %s sessions' %
56 (logout.remoteProviderId, session_indexes))
58 # Find the first session being asked to log out. Later we loop over
59 # all the session indexes and mark them as logging out but only one
60 # is needed to handle the request.
61 if len(session_indexes) < 1:
62 self.error('SLO empty session Indexes: %s')
63 raise cherrypy.HTTPError(400, 'Invalid logout request')
64 session = saml_sessions.get_session_by_id(session_indexes[0])
67 logout.setSessionFromDump(session.login_session)
68 except lasso.ProfileBadSessionDumpError as e:
69 self.error('loading session failed: %s' % e)
70 raise cherrypy.HTTPError(400, 'Invalid logout session')
72 return self._not_logged_in(logout, message)
75 logout.validateRequest()
76 except lasso.ProfileSessionNotFoundError, e:
77 self.error('Logout failed. No sessions for %s' %
78 logout.remoteProviderId)
79 return self._not_logged_in(logout, message)
80 except lasso.LogoutUnsupportedProfileError:
81 self.error('Logout failed. Unsupported profile %s' %
82 logout.remoteProviderId)
83 raise cherrypy.HTTPError(400, 'Profile does not support logout')
84 except lasso.Error, e:
85 self.error('SLO validation failed: %s' % e)
86 raise cherrypy.HTTPError(400, 'Failed to validate logout request')
89 logout.buildResponseMsg()
90 except lasso.ProfileUnsupportedProfileError:
91 self.error('Unsupported profile for %s' % logout.remoteProviderId)
92 raise cherrypy.HTTPError(400, 'Profile does not support logout')
93 except lasso.Error, e:
94 self.error('SLO failed to build logout response: %s' % e)
96 for ind in session_indexes:
97 session = saml_sessions.get_session_by_id(ind)
99 session.set_logoutstate(relaystate=logout.msgUrl,
101 saml_sessions.start_logout(session)
103 self.error('SLO request to log out non-existent session: %s' %
108 def _handle_logout_response(self, us, logout, saml_sessions, message,
111 self.debug('Logout response')
114 logout.processResponseMsg(message)
115 except getattr(lasso, 'ProfileRequestDeniedError',
116 lasso.LogoutRequestDeniedError):
117 self.error('Logout request denied by %s' %
118 logout.remoteProviderId)
119 # Fall through to next provider
120 except (lasso.ProfileInvalidMsgError,
121 lasso.LogoutPartialLogoutError) as e:
122 self.error('Logout request from %s failed: %s' %
123 (logout.remoteProviderId, e))
125 self.debug('Processing SLO Response from %s' %
126 logout.remoteProviderId)
128 self.debug('SLO response to request id %s' %
129 logout.response.inResponseTo)
131 session = saml_sessions.get_session_by_request_id(
132 logout.response.inResponseTo)
134 if session is not None:
135 self.debug('Logout response session logout id is: %s' %
137 saml_sessions.remove_session(session)
139 self._audit('Logged out user: %s [%s] from %s' %
140 (user.name, user.fullname,
141 logout.remoteProviderId))
143 return self._not_logged_in(logout, message)
147 def _not_logged_in(self, logout, message):
149 The user requested a logout but isn't logged in, or we can't
150 find a session for the user. Try to be nice and redirect them
151 back to the RelayState in the logout request.
153 We are only nice in the case of a valid logout request. If the
154 request is invalid (not signed, unknown SP, etc) then an
157 self.error('Logout attempt without being logged in.')
159 if logout.msgRelayState is not None:
160 raise cherrypy.HTTPRedirect(logout.msgRelayState)
163 logout.processRequestMsg(message)
164 except (lasso.ServerProviderNotFoundError,
165 lasso.ProfileUnknownProviderError) as e:
166 msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId,
169 raise UnknownProvider(msg)
170 except (lasso.ProfileInvalidProtocolprofileError,
172 msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request,
175 raise InvalidRequest(msg)
176 except lasso.Error, e:
177 self.error('SLO unknown error: %s' % message)
178 raise cherrypy.HTTPError(400, 'Invalid logout request')
180 if logout.msgRelayState:
181 raise cherrypy.HTTPRedirect(logout.msgRelayState)
183 raise cherrypy.HTTPError(400, 'Not logged in')
185 def logout(self, message, relaystate=None, samlresponse=None):
187 Handle HTTP Redirect logout. This is an asynchronous logout
188 request process that relies on the HTTP agent to forward
189 logout requests to any other SP's that are also logged in.
191 The basic process is this:
192 1. A logout request is received. It is processed and the response
194 2. If any other SP's have also logged in as this user then the
195 first such session is popped off and a logout request is
196 generated and forwarded to the SP.
197 3. If a logout response is received then the user is marked
198 as logged out from that SP.
199 Repeat steps 2-3 until only the initial logout request is
200 left unhandled, at which time the pre-generated response is sent
201 back to the SP that originated the logout request.
203 logout = self.cfg.idp.get_logout_handler()
207 saml_sessions = SAMLSessionFactory()
209 if lasso.SAML2_FIELD_REQUEST in message:
210 self._handle_logout_request(us, logout, saml_sessions, message)
212 self._handle_logout_response(us, logout, saml_sessions, message,
215 raise cherrypy.HTTPRedirect(400, 'Bad Request. Not a logout ' +
216 'request or response.')
218 # Fall through to handle any remaining sessions.
220 # Find the next SP to logout and send a LogoutRequest
221 session = saml_sessions.get_next_logout()
223 self.debug('Going to log out %s' % session.provider_id)
226 logout.setSessionFromDump(session.login_session)
227 except lasso.ProfileBadSessionDumpError as e:
228 self.error('Failed to load session: %s' % e)
229 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
232 logout.initRequest(session.provider_id, lasso.HTTP_METHOD_REDIRECT)
235 logout.buildRequestMsg()
236 except lasso.Error, e:
237 self.error('failure to build logout request msg: %s' % e)
238 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
241 # Set the full list of session indexes for this provider to
243 self.debug('logging out provider id %s' % session.provider_id)
244 indexes = saml_sessions.get_session_id_by_provider_id(
247 self.debug('Requesting logout for sessions %s' % indexes)
248 req = logout.get_request()
249 req.setSessionIndexes(indexes)
251 session.set_logoutstate(relaystate=logout.msgUrl,
252 request_id=logout.request.id)
253 saml_sessions.start_logout(session, initial=False)
255 self.debug('Request logout ID %s for session ID %s' %
256 (logout.request.id, session.session_id))
257 self.debug('Redirecting to another SP to logout on %s at %s' %
258 (logout.remoteProviderId, logout.msgUrl))
260 raise cherrypy.HTTPRedirect(logout.msgUrl)
262 # Otherwise we're done, respond to the original request using the
263 # response we cached earlier.
266 session = saml_sessions.get_initial_logout()
268 self.debug('SLO get_last_session() unable to find last session')
269 raise cherrypy.HTTPError(400, 'Unable to determine logout state')
271 redirect = session.relaystate
273 redirect = self.basepath
275 saml_sessions.remove_session(session)
277 # Log out of cherrypy session
279 self._audit('Logged out user: %s [%s] from %s' %
280 (user.name, user.fullname,
281 session.provider_id))
284 self.debug('SLO redirect to %s' % redirect)
286 raise cherrypy.HTTPRedirect(redirect)