1 # Copyright (C) 2015 Ipsilon project Contributors, for license see COPYING
3 from cherrypy import config as cherrypy_config
4 from ipsilon.util.log import Log
5 from ipsilon.util.data import SAML2SessionStore
8 SAML2_METADATA_BINDING_SOAP,
9 SAML2_METADATA_BINDING_REDIRECT,
18 class SAMLSession(Log):
22 uuidval - Unique ID stored in the database
23 session_id - ID of the login session
24 provider_id - ID of the SP
25 user - the login name of the user that owns the session
26 login_session - the Login session object
27 logoutstate - an integer constant representing where in the
28 logout process this request is
29 relaystate - where the user will be redirected when logout is
31 request_id - the logout request ID if initiated from IdP. The
32 logout response will include an InResponseTo value
34 logout_request - the Logout request object
35 expiration_time - the time the login session expires
36 supported_logout_mechs - logout mechanisms supported by this session
38 def __init__(self, uuidval, session_id, provider_id, user,
39 login_session, logoutstate=None, relaystate=None,
40 logout_request=None, request_id=None,
42 supported_logout_mechs=None):
44 self.uuidval = uuidval
45 self.session_id = session_id
46 self.provider_id = provider_id
48 self.login_session = login_session
49 self.logoutstate = logoutstate
50 self.relaystate = relaystate
51 self.request_id = request_id
52 self.logout_request = logout_request
53 self.expiration_time = expiration_time
54 if supported_logout_mechs is None:
55 supported_logout_mechs = []
56 self.supported_logout_mechs = supported_logout_mechs
58 def set_logoutstate(self, relaystate=None, request=None, request_id=None):
60 Update attributes needed to determine the state of the session for
63 The database is not updated when these are set. It is expected that
64 this is called prior to start_logout()
67 self.relaystate = relaystate
69 self.logout_request = request
71 self.request_id = request_id
74 self.debug('session_id %s' % self.session_id)
75 self.debug('provider_id %s' % self.provider_id)
76 self.debug('login session %s' % self.login_session)
77 self.debug('logoutstate %s' % self.logoutstate)
78 self.debug('logout mech %s' % self.supported_logout_mechs)
82 Convert this object into something suitable to store in the
86 data['session_id'] = self.session_id
87 data['provider_id'] = self.provider_id
88 data['user'] = self.user
89 data['login_session'] = self.login_session
90 data['logoutstate'] = self.logoutstate
91 data['relaystate'] = self.relaystate
92 data['logout_request'] = self.logout_request
93 data['request_id'] = self.request_id
94 data['expiration_time'] = self.expiration_time
96 return {self.uuidval: data}
99 class SAMLSessionFactory(Log):
101 Access SAML session information.
103 The sessions are stored via the data backend.
105 When a user logs in, add_session() is called and a new SAMLSession
106 created and added to the table.
108 When a user logs out, the next login session is found and moved to
109 sessions_logging_out. remove_session() will look in both when trying
112 Returns a SAMLSession object representing the new session.
114 def __init__(self, database_url):
115 self._ss = SAML2SessionStore(database_url=database_url)
118 def _data_to_samlsession(self, uuidval, data):
120 Convert data from the data backend to a SAMLSession object.
122 return SAMLSession(uuidval,
123 data.get('session_id'),
124 data.get('provider_id'),
126 data.get('login_session'),
127 data.get('logoutstate'),
128 data.get('relaystate'),
129 data.get('logout_request'),
130 data.get('request_id'),
131 data.get('expiration_time'),
132 data.get('supported_logout_mechs'))
134 def add_session(self, session_id, provider_id, user, login_session,
135 request_id, supported_logout_mechs):
137 Add a new login session to the table.
139 :param session_id: The login session ID
140 :param provider_id: The URL of the SP
141 :param user: The NameID username
142 :param login_session: The lasso Login session
143 :param request_id: The request ID of the Logout
144 :param supported_logout_mechs: A list of logout protocols supported
148 timeout = cherrypy_config['tools.sessions.timeout']
149 t = datetime.timedelta(seconds=timeout * 60)
150 expiration_time = datetime.datetime.now() + t
152 data = {'session_id': session_id,
153 'provider_id': provider_id,
155 'login_session': login_session,
156 'logoutstate': LOGGED_IN,
157 'expiration_time': expiration_time,
158 'request_id': request_id,
159 'supported_logout_mechs': supported_logout_mechs}
161 uuidval = self._ss.new_session(data)
163 return SAMLSession(uuidval, session_id, provider_id, user,
164 login_session, LOGGED_IN,
165 request_id=request_id,
166 expiration_time=expiration_time)
168 def get_session_by_id(self, session_id):
170 Retrieve a session by session ID
172 uuidval, data = self._ss.get_session(session_id=session_id)
176 return self._data_to_samlsession(uuidval, data)
178 def get_session_id_by_provider_id(self, provider_id):
180 Return a tuple of logged-in session IDs by provider_id
182 candidates = self._ss.get_user_sessions(self.user)
187 if c[key].get('provider_id') == provider_id:
188 samlsession = self._data_to_samlsession(key, c[key])
189 session_ids.append(samlsession.session_id.encode('utf-8'))
191 return tuple(session_ids)
193 def get_session_by_request_id(self, request_id):
195 Retrieve a session by logout request ID
197 uuidval, data = self._ss.get_session(request_id=request_id)
201 return self._data_to_samlsession(uuidval, data)
203 def remove_session(self, samlsession):
204 return self._ss.remove_session(samlsession.uuidval)
206 def remove_session_by_session_id(self, session_id):
207 session = self.get_session_by_id(session_id)
208 return self._ss.remove_session(session.uuidval)
210 def start_logout(self, samlsession, relaystate=None, initial=True):
212 Move a session into the logging_out state
214 samlsession: the SAMLSession object to start logging out
215 relaystate: URL to redirect user to when logout is completed
216 initial: boolean to indicate if this session started logout.
217 Only the initial session's relaystate is used.
222 samlsession.logoutstate = INIT_LOGOUT
224 samlsession.logoutstate = LOGGING_OUT
226 samlsession.relaystate = relaystate
227 datum = samlsession.convert()
228 self._ss.update_session(datum)
230 def get_next_logout(self, peek=False,
233 Get the next session in the logged-in state and move
234 it to the logging_out state. Return the session that is
237 :param peek: for IdP-initiated logout we can't remove the
238 session otherwise when the request comes back
239 in the user won't be seen as being logged-on.
240 :param logout_mechs: An ordered list of logout mechanisms
241 you're looking for. For each mechanism in order
242 loop through all sessions. If If no sessions of
243 this method are available then try the next mechanism
244 until exhausted. In that case None is returned.
246 Returns a tuple of (mechanism, session) or
247 (None, None) if no more sessions in LOGGED_IN state.
249 candidates = self._ss.get_user_sessions(self.user)
250 if logout_mechs is None:
251 logout_mechs = [SAML2_METADATA_BINDING_REDIRECT, ]
253 for mech in logout_mechs:
256 if ((int(c[key].get('logoutstate', 0)) == LOGGED_IN) and
257 (mech in c[key].get('supported_logout_mechs'))):
258 samlsession = self._data_to_samlsession(key, c[key])
259 self.start_logout(samlsession, initial=False)
260 return (mech, samlsession)
263 def get_initial_logout(self):
265 Get the initial logout request.
267 Raises ValueError if no sessions in INIT_LOGOUT state.
269 candidates = self._ss.get_user_sessions(self.user)
271 # FIXME: what does it mean if there are multiple in init? We
272 # just return the first one for now. How do we know
273 # it's the "right" one if multiple logouts are started
274 # at the same time from different SPs?
277 if int(c[key].get('logoutstate', 0)) == INIT_LOGOUT:
278 samlsession = self._data_to_samlsession(key, c[key])
287 Dump all sessions to debug log
289 candidates = self._ss.get_user_sessions(self.user)
294 samlsession = self._data_to_samlsession(key, c[key])
295 self.debug('session %d: %s' % (count, samlsession.convert()))
298 if __name__ == '__main__':
299 provider1 = "http://127.0.0.10/saml2"
300 provider2 = "http://127.0.0.11/saml2"
302 # temporary values to simulate cherrypy
303 cherrypy_config['tools.sessions.timeout'] = 60
305 factory = SAMLSessionFactory('/tmp/saml2sessions.sqlite')
308 sess1 = factory.add_session('_123456', provider1, "admin",
310 [SAML2_METADATA_BINDING_REDIRECT])
311 sess2 = factory.add_session('_789012', provider2, "testuser",
313 [SAML2_METADATA_BINDING_SOAP,
314 SAML2_METADATA_BINDING_REDIRECT])
316 # Test finding sessions by provider
317 ids = factory.get_session_id_by_provider_id(provider2)
318 assert(len(ids) == 1)
320 sess3 = factory.add_session('_345678', provider2, "testuser",
322 [SAML2_METADATA_BINDING_REDIRECT])
323 ids = factory.get_session_id_by_provider_id(provider2)
324 assert(len(ids) == 2)
326 # Test finding sessions by session ID
327 test1 = factory.get_session_by_id('_123456')
328 assert(test1.user == 'admin')
329 assert(test1.provider_id == provider1)
331 # Log out and remove the first session
332 test1.set_logoutstate('http://www.example.com/idp')
333 factory.start_logout(test1, initial=True)
334 test1 = factory.get_session_by_id('_123456')
335 assert(test1.relaystate == 'http://www.example.com/idp')
337 factory.remove_session_by_session_id('_123456')
339 # Make sure it is gone from the db
340 test1 = factory.get_session_by_id('_123456')
341 assert(test1 is None)
343 test2 = factory.get_session_by_id('_789012')
344 factory.start_logout(test2, initial=True)
346 (lmech, test3) = factory.get_next_logout()
347 assert(test3.session_id == '_345678')
349 test4 = factory.get_initial_logout()
350 assert(test4.session_id == '_789012')
352 # Even though we've started logout, make sure we can still find
353 # all sessions for a provider.
354 ids = factory.get_session_id_by_provider_id(provider2)
355 assert(len(ids) == 2)