Try to return a redirect instead a 400 for "not logged in" state
[cascardo/ipsilon.git] / ipsilon / providers / saml2 / logout.py
1 # Copyright (C) 2015  Rob Crittenden <rcritten@redhat.com>
2 #
3 # see file 'COPYING' for use and warranty information
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 from ipsilon.providers.common import ProviderPageBase
19 from ipsilon.providers.common import InvalidRequest
20 from ipsilon.providers.saml2.sessions import SAMLSessionsContainer
21 from ipsilon.providers.saml2.auth import UnknownProvider
22 from ipsilon.util.user import UserSession
23 import cherrypy
24 import lasso
25
26
27 class LogoutRequest(ProviderPageBase):
28     """
29     SP-initiated logout.
30
31     The sequence is:
32       - On each logout a new session is created to represent that
33         provider
34       - Initial logout request is verified and stored in the login
35         session
36       - If there are other sessions then one is chosen that is not
37         the current provider and a logoutRequest is sent
38       - When a logoutResponse is received the session is removed
39       - When all other sessions but the initial one have been
40         logged out then it a final logoutResponse is sent and the
41         session removed. At this point the cherrypy session is
42         deleted.
43     """
44
45     def __init__(self, *args, **kwargs):
46         super(LogoutRequest, self).__init__(*args, **kwargs)
47
48     def _handle_logout_request(self, us, logout, saml_sessions, message):
49         self.debug('Logout request')
50
51         try:
52             logout.processRequestMsg(message)
53         except (lasso.ServerProviderNotFoundError,
54                 lasso.ProfileUnknownProviderError) as e:
55             msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId,
56                                                  e, message)
57             self.error(msg)
58             raise UnknownProvider(msg)
59         except (lasso.ProfileInvalidProtocolprofileError,
60                 lasso.DsError), e:
61             msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request,
62                                                           e, message)
63             self.error(msg)
64             raise InvalidRequest(msg)
65         except lasso.Error, e:
66             self.error('SLO unknown error: %s' % message)
67             raise cherrypy.HTTPError(400, 'Invalid logout request')
68
69         # TODO: verify that the session index is in the request
70         session_indexes = logout.request.sessionIndexes
71         self.debug('SLO from %s with %s sessions' %
72                    (logout.remoteProviderId, session_indexes))
73
74         session = saml_sessions.find_session_by_provider(
75             logout.remoteProviderId)
76         if session:
77             try:
78                 logout.setSessionFromDump(session.session.dump())
79             except lasso.ProfileBadSessionDumpError as e:
80                 self.error('loading session failed: %s' % e)
81                 raise cherrypy.HTTPError(400, 'Invalid logout session')
82         else:
83             return self._not_logged_in(logout, message)
84
85         try:
86             logout.validateRequest()
87         except lasso.ProfileSessionNotFoundError, e:
88             self.error('Logout failed. No sessions for %s' %
89                        logout.remoteProviderId)
90             return self._not_logged_in(logout, message)
91         except lasso.LogoutUnsupportedProfileError:
92             self.error('Logout failed. Unsupported profile %s' %
93                        logout.remoteProviderId)
94             raise cherrypy.HTTPError(400, 'Profile does not support logout')
95         except lasso.Error, e:
96             self.error('SLO validation failed: %s' % e)
97             raise cherrypy.HTTPError(400, 'Failed to validate logout request')
98
99         try:
100             logout.buildResponseMsg()
101         except lasso.ProfileUnsupportedProfileError:
102             self.error('Unsupported profile for %s' % logout.remoteProviderId)
103             raise cherrypy.HTTPError(400, 'Profile does not support logout')
104         except lasso.Error, e:
105             self.error('SLO failed to build logout response: %s' % e)
106
107         session.set_logoutstate(logout.msgUrl, logout.request.id,
108                                 message)
109         saml_sessions.start_logout(session)
110
111         us.save_provider_data('saml2', saml_sessions)
112
113         return
114
115     def _handle_logout_response(self, us, logout, saml_sessions, message,
116                                 samlresponse):
117
118         self.debug('Logout response')
119
120         try:
121             logout.processResponseMsg(message)
122         except getattr(lasso, 'ProfileRequestDeniedError',
123                        lasso.LogoutRequestDeniedError):
124             self.error('Logout request denied by %s' %
125                        logout.remoteProviderId)
126             # Fall through to next provider
127         except (lasso.ProfileInvalidMsgError,
128                 lasso.LogoutPartialLogoutError) as e:
129             self.error('Logout request from %s failed: %s' %
130                        (logout.remoteProviderId, e))
131         else:
132             self.debug('Processing SLO Response from %s' %
133                        logout.remoteProviderId)
134
135             self.debug('SLO response to request id %s' %
136                        logout.response.inResponseTo)
137
138             saml_sessions = us.get_provider_data('saml2')
139             if saml_sessions is None:
140                 # TODO: return logged out instead
141                 saml_sessions = SAMLSessionsContainer()
142
143             # TODO: need to log out each SessionIndex?
144             session = saml_sessions.find_session_by_provider(
145                 logout.remoteProviderId)
146
147             if session is not None:
148                 self.debug('Logout response session logout id is: %s' %
149                            session.session_id)
150                 saml_sessions.remove_session_by_provider(
151                     logout.remoteProviderId)
152                 us.save_provider_data('saml2', saml_sessions)
153                 user = us.get_user()
154                 self._audit('Logged out user: %s [%s] from %s' %
155                             (user.name, user.fullname,
156                              logout.remoteProviderId))
157             else:
158                 return self._not_logged_in(logout, message)
159
160         return
161
162     def _not_logged_in(self, logout, message):
163         """
164         The user requested a logout but isn't logged in, or we can't
165         find a session for the user. Try to be nice and redirect them
166         back to the RelayState in the logout request.
167
168         We are only nice in the case of a valid logout request. If the
169         request is invalid (not signed, unknown SP, etc) then an
170         exception is raised.
171         """
172         self.error('Logout attempt without being logged in.')
173
174         if logout.msgRelayState is not None:
175             raise cherrypy.HTTPRedirect(logout.msgRelayState)
176
177         try:
178             logout.processRequestMsg(message)
179         except (lasso.ServerProviderNotFoundError,
180                 lasso.ProfileUnknownProviderError) as e:
181             msg = 'Invalid SP [%s] (%r [%r])' % (logout.remoteProviderId,
182                                                  e, message)
183             self.error(msg)
184             raise UnknownProvider(msg)
185         except (lasso.ProfileInvalidProtocolprofileError,
186                 lasso.DsError), e:
187             msg = 'Invalid SAML Request: %r (%r [%r])' % (logout.request,
188                                                           e, message)
189             self.error(msg)
190             raise InvalidRequest(msg)
191         except lasso.Error, e:
192             self.error('SLO unknown error: %s' % message)
193             raise cherrypy.HTTPError(400, 'Invalid logout request')
194
195         if logout.msgRelayState:
196             raise cherrypy.HTTPRedirect(logout.msgRelayState)
197         else:
198             raise cherrypy.HTTPError(400, 'Not logged in')
199
200     def logout(self, message, relaystate=None, samlresponse=None):
201         """
202         Handle HTTP Redirect logout. This is an asynchronous logout
203         request process that relies on the HTTP agent to forward
204         logout requests to any other SP's that are also logged in.
205
206         The basic process is this:
207          1. A logout request is received. It is processed and the response
208             cached.
209          2. If any other SP's have also logged in as this user then the
210             first such session is popped off and a logout request is
211             generated and forwarded to the SP.
212          3. If a logout response is received then the user is marked
213             as logged out from that SP.
214          Repeat steps 2-3 until only the initial logout request is
215          left unhandled, at which time the pre-generated response is sent
216          back to the SP that originated the logout request.
217         """
218         logout = self.cfg.idp.get_logout_handler()
219
220         us = UserSession()
221
222         saml_sessions = us.get_provider_data('saml2')
223         if saml_sessions is None:
224             # No sessions means nothing to log out
225             return self._not_logged_in(logout, message)
226
227         self.debug('%d sessions loaded' % saml_sessions.count())
228         saml_sessions.dump()
229
230         if lasso.SAML2_FIELD_REQUEST in message:
231             self._handle_logout_request(us, logout, saml_sessions, message)
232         elif samlresponse:
233             self._handle_logout_response(us, logout, saml_sessions, message,
234                                          samlresponse)
235         else:
236             raise cherrypy.HTTPRedirect(400, 'Bad Request. Not a logout ' +
237                                         'request or response.')
238
239         # Fall through to handle any remaining sessions.
240
241         # Find the next SP to logout and send a LogoutRequest
242         saml_sessions = us.get_provider_data('saml2')
243         session = saml_sessions.get_next_logout()
244         if session:
245             self.debug('Going to log out %s' % session.provider_id)
246
247             try:
248                 logout.setSessionFromDump(session.session.dump())
249             except lasso.ProfileBadSessionDumpError as e:
250                 self.error('Failed to load session: %s' % e)
251                 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
252                                             % e)
253
254             logout.initRequest(session.provider_id, lasso.HTTP_METHOD_REDIRECT)
255
256             try:
257                 logout.buildRequestMsg()
258             except lasso.Error, e:
259                 self.error('failure to build logout request msg: %s' % e)
260                 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
261                                             % e)
262
263             # Now set the full list of session indexes to log out
264             req = logout.get_request()
265             req.setSessionIndexes(tuple(set(session.session_indexes)))
266
267             session.set_logoutstate(logout.msgUrl, logout.request.id, None)
268             us.save_provider_data('saml2', saml_sessions)
269
270             self.debug('Request logout ID %s for session ID %s' %
271                        (logout.request.id, session.session_id))
272             self.debug('Redirecting to another SP to logout on %s at %s' %
273                        (logout.remoteProviderId, logout.msgUrl))
274
275             raise cherrypy.HTTPRedirect(logout.msgUrl)
276
277         # Otherwise we're done, respond to the original request using the
278         # response we cached earlier.
279
280         saml_sessions = us.get_provider_data('saml2')
281         if saml_sessions is None or saml_sessions.count() == 0:
282             return self._not_logged_in(logout, message)
283
284         try:
285             session = saml_sessions.get_last_session()
286         except ValueError:
287             self.debug('SLO get_last_session() unable to find last session')
288             raise cherrypy.HTTPError(400, 'Unable to determine logout state')
289
290         redirect = session.logoutstate.get('relaystate')
291         if not redirect:
292             redirect = self.basepath
293
294         # Log out of cherrypy session
295         user = us.get_user()
296         self._audit('Logged out user: %s [%s] from %s' %
297                     (user.name, user.fullname,
298                      session.provider_id))
299         us.logout(user)
300
301         self.debug('SLO redirect to %s' % redirect)
302
303         raise cherrypy.HTTPRedirect(redirect)