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