cc9b7771c609ffe53a35cefec0e65b259166f733
[cascardo/ipsilon.git] / ipsilon / providers / saml2 / logout.py
1 # Copyright (C) 2015 Ipsilon project Contributors, for license see COPYING
2
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 import cherrypy
8 import lasso
9
10
11 class LogoutRequest(ProviderPageBase):
12     """
13     SP-initiated logout.
14
15     The sequence is:
16       - On each logout a new session is created to represent that
17         provider
18       - Initial logout request is verified and stored in the login
19         session
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
26         deleted.
27     """
28
29     def __init__(self, *args, **kwargs):
30         super(LogoutRequest, self).__init__(*args, **kwargs)
31
32     def _handle_logout_request(self, us, logout, saml_sessions, message):
33         self.debug('Logout request')
34
35         try:
36             logout.processRequestMsg(message)
37         except (lasso.ServerProviderNotFoundError,
38                 lasso.ProfileUnknownProviderError) as e:
39             msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId,
40                                                  e, message)
41             self.error(msg)
42             raise UnknownProvider(msg)
43         except (lasso.ProfileInvalidProtocolprofileError,
44                 lasso.DsError), e:
45             msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request,
46                                                           e, message)
47             self.error(msg)
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')
52
53         session_indexes = logout.request.sessionIndexes
54         self.debug('SLO from %s with %s sessions' %
55                    (logout.remoteProviderId, session_indexes))
56
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])
64         if session:
65             try:
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')
70         else:
71             return self._not_logged_in(logout, message)
72
73         try:
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')
86
87         try:
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)
94
95         for ind in session_indexes:
96             session = saml_sessions.get_session_by_id(ind)
97             if session:
98                 session.set_logoutstate(relaystate=logout.msgUrl,
99                                         request=message)
100                 saml_sessions.start_logout(session)
101             else:
102                 self.error('SLO request to log out non-existent session: %s' %
103                            ind)
104
105         return
106
107     def _handle_logout_response(self, us, logout, saml_sessions, message,
108                                 samlresponse):
109
110         self.debug('Logout response')
111
112         try:
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))
123         else:
124             self.debug('Processing SLO Response from %s' %
125                        logout.remoteProviderId)
126
127             self.debug('SLO response to request id %s' %
128                        logout.response.inResponseTo)
129
130             session = saml_sessions.get_session_by_request_id(
131                 logout.response.inResponseTo)
132
133             if session is not None:
134                 self.debug('Logout response session logout id is: %s' %
135                            session.session_id)
136                 saml_sessions.remove_session(session)
137                 user = us.get_user()
138                 self._audit('Logged out user: %s [%s] from %s' %
139                             (user.name, user.fullname,
140                              logout.remoteProviderId))
141             else:
142                 return self._not_logged_in(logout, message)
143
144         return
145
146     def _not_logged_in(self, logout, message):
147         """
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.
151
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
154         exception is raised.
155         """
156         self.error('Logout attempt without being logged in.')
157
158         if logout.msgRelayState is not None:
159             raise cherrypy.HTTPRedirect(logout.msgRelayState)
160
161         try:
162             logout.processRequestMsg(message)
163         except (lasso.ServerProviderNotFoundError,
164                 lasso.ProfileUnknownProviderError) as e:
165             msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId,
166                                                  e, message)
167             self.error(msg)
168             raise UnknownProvider(msg)
169         except (lasso.ProfileInvalidProtocolprofileError,
170                 lasso.DsError), e:
171             msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request,
172                                                           e, message)
173             self.error(msg)
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')
178
179         if logout.msgRelayState:
180             raise cherrypy.HTTPRedirect(logout.msgRelayState)
181         else:
182             raise cherrypy.HTTPError(400, 'Not logged in')
183
184     def logout(self, message, relaystate=None, samlresponse=None):
185         """
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.
189
190         The basic process is this:
191          1. A logout request is received. It is processed and the response
192             cached.
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.
201         """
202         logout = self.cfg.idp.get_logout_handler()
203
204         us = UserSession()
205
206         saml_sessions = self.cfg.idp.sessionfactory
207
208         if lasso.SAML2_FIELD_REQUEST in message:
209             self._handle_logout_request(us, logout, saml_sessions, message)
210         elif samlresponse:
211             self._handle_logout_response(us, logout, saml_sessions, message,
212                                          samlresponse)
213         else:
214             raise cherrypy.HTTPRedirect(400, 'Bad Request. Not a logout ' +
215                                         'request or response.')
216
217         # Fall through to handle any remaining sessions.
218
219         # Find the next SP to logout and send a LogoutRequest
220         session = saml_sessions.get_next_logout()
221         if session:
222             self.debug('Going to log out %s' % session.provider_id)
223
224             try:
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 '
229                                             % e)
230
231             logout.initRequest(session.provider_id, lasso.HTTP_METHOD_REDIRECT)
232
233             try:
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 '
238                                             % e)
239
240             # Set the full list of session indexes for this provider to
241             # log out
242             self.debug('logging out provider id %s' % session.provider_id)
243             indexes = saml_sessions.get_session_id_by_provider_id(
244                 session.provider_id
245             )
246             self.debug('Requesting logout for sessions %s' % indexes)
247             req = logout.get_request()
248             req.setSessionIndexes(indexes)
249
250             session.set_logoutstate(relaystate=logout.msgUrl,
251                                     request_id=logout.request.id)
252             saml_sessions.start_logout(session, initial=False)
253
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))
258
259             raise cherrypy.HTTPRedirect(logout.msgUrl)
260
261         # Otherwise we're done, respond to the original request using the
262         # response we cached earlier.
263
264         try:
265             session = saml_sessions.get_initial_logout()
266         except ValueError:
267             self.debug('SLO get_last_session() unable to find last session')
268             raise cherrypy.HTTPError(400, 'Unable to determine logout state')
269
270         redirect = session.relaystate
271         if not redirect:
272             redirect = self.basepath
273
274         saml_sessions.remove_session(session)
275
276         # Log out of cherrypy session
277         user = us.get_user()
278         self._audit('Logged out user: %s [%s] from %s' %
279                     (user.name, user.fullname,
280                      session.provider_id))
281         us.logout(user)
282
283         self.debug('SLO redirect to %s' % redirect)
284
285         raise cherrypy.HTTPRedirect(redirect)