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 SAMLSessionsContainer
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 # TODO: verify that the session index is in the request
55 session_indexes = logout.request.sessionIndexes
56 self.debug('SLO from %s with %s sessions' %
57 (logout.remoteProviderId, session_indexes))
59 session = saml_sessions.find_session_by_provider(
60 logout.remoteProviderId)
63 logout.setSessionFromDump(session.session.dump())
64 except lasso.ProfileBadSessionDumpError as e:
65 self.error('loading session failed: %s' % e)
66 raise cherrypy.HTTPError(400, 'Invalid logout session')
68 return self._not_logged_in(logout, message)
71 logout.validateRequest()
72 except lasso.ProfileSessionNotFoundError, e:
73 self.error('Logout failed. No sessions for %s' %
74 logout.remoteProviderId)
75 return self._not_logged_in(logout, message)
76 except lasso.LogoutUnsupportedProfileError:
77 self.error('Logout failed. Unsupported profile %s' %
78 logout.remoteProviderId)
79 raise cherrypy.HTTPError(400, 'Profile does not support logout')
80 except lasso.Error, e:
81 self.error('SLO validation failed: %s' % e)
82 raise cherrypy.HTTPError(400, 'Failed to validate logout request')
85 logout.buildResponseMsg()
86 except lasso.ProfileUnsupportedProfileError:
87 self.error('Unsupported profile for %s' % logout.remoteProviderId)
88 raise cherrypy.HTTPError(400, 'Profile does not support logout')
89 except lasso.Error, e:
90 self.error('SLO failed to build logout response: %s' % e)
92 session.set_logoutstate(logout.msgUrl, logout.request.id,
94 saml_sessions.start_logout(session)
96 us.save_provider_data('saml2', saml_sessions)
100 def _handle_logout_response(self, us, logout, saml_sessions, message,
103 self.debug('Logout response')
106 logout.processResponseMsg(message)
107 except getattr(lasso, 'ProfileRequestDeniedError',
108 lasso.LogoutRequestDeniedError):
109 self.error('Logout request denied by %s' %
110 logout.remoteProviderId)
111 # Fall through to next provider
112 except (lasso.ProfileInvalidMsgError,
113 lasso.LogoutPartialLogoutError) as e:
114 self.error('Logout request from %s failed: %s' %
115 (logout.remoteProviderId, e))
117 self.debug('Processing SLO Response from %s' %
118 logout.remoteProviderId)
120 self.debug('SLO response to request id %s' %
121 logout.response.inResponseTo)
123 saml_sessions = us.get_provider_data('saml2')
124 if saml_sessions is None:
125 # TODO: return logged out instead
126 saml_sessions = SAMLSessionsContainer()
128 # TODO: need to log out each SessionIndex?
129 session = saml_sessions.find_session_by_provider(
130 logout.remoteProviderId)
132 if session is not None:
133 self.debug('Logout response session logout id is: %s' %
135 saml_sessions.remove_session_by_provider(
136 logout.remoteProviderId)
137 us.save_provider_data('saml2', saml_sessions)
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 = us.get_provider_data('saml2')
208 if saml_sessions is None:
209 # No sessions means nothing to log out
210 return self._not_logged_in(logout, message)
212 self.debug('%d sessions loaded' % saml_sessions.count())
215 if lasso.SAML2_FIELD_REQUEST in message:
216 self._handle_logout_request(us, logout, saml_sessions, message)
218 self._handle_logout_response(us, logout, saml_sessions, message,
221 raise cherrypy.HTTPRedirect(400, 'Bad Request. Not a logout ' +
222 'request or response.')
224 # Fall through to handle any remaining sessions.
226 # Find the next SP to logout and send a LogoutRequest
227 saml_sessions = us.get_provider_data('saml2')
228 session = saml_sessions.get_next_logout()
230 self.debug('Going to log out %s' % session.provider_id)
233 logout.setSessionFromDump(session.session.dump())
234 except lasso.ProfileBadSessionDumpError as e:
235 self.error('Failed to load session: %s' % e)
236 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
239 logout.initRequest(session.provider_id, lasso.HTTP_METHOD_REDIRECT)
242 logout.buildRequestMsg()
243 except lasso.Error, e:
244 self.error('failure to build logout request msg: %s' % e)
245 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
248 # Now set the full list of session indexes to log out
249 req = logout.get_request()
250 req.setSessionIndexes(tuple(set(session.session_indexes)))
252 session.set_logoutstate(logout.msgUrl, logout.request.id, None)
253 us.save_provider_data('saml2', saml_sessions)
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.
265 saml_sessions = us.get_provider_data('saml2')
266 if saml_sessions is None or saml_sessions.count() == 0:
267 return self._not_logged_in(logout, message)
270 session = saml_sessions.get_last_session()
272 self.debug('SLO get_last_session() unable to find last session')
273 raise cherrypy.HTTPError(400, 'Unable to determine logout state')
275 redirect = session.logoutstate.get('relaystate')
277 redirect = self.basepath
279 # Log out of cherrypy session
281 self._audit('Logged out user: %s [%s] from %s' %
282 (user.name, user.fullname,
283 session.provider_id))
286 self.debug('SLO redirect to %s' % redirect)
288 raise cherrypy.HTTPRedirect(redirect)