When managing sessions in web applications the key to the castle is a sessionid. Most often this sessionid is passed to and from the client by means of a cookie. In this article we explore the tools available in Python to deal with cookies.
Reading and writing cookies
If we are extending the BaseHTTPRequestHandler
class from Python's http.server
module we have access to the request headers by means of the headers
attribute. It provides a convient method get_all()
to retrieve headers by name (line 12 in the code sample below):
from http.cookies import SimpleCookie as cookie ... class ApplicationRequestHandler(BaseHTTPRequestHandler): sessioncookies = {} def __init__(self,*args,**kwargs): self.sessionidmorsel = None super().__init__(*args,**kwargs) def _session_cookie(self,forcenew=False): cookiestring = "\n".join(self.headers.get_all('Cookie',failobj=[])) c = cookie() c.load(cookiestring) try: if forcenew or self.sessioncookies[c['session_id'].value]-time() > 3600: raise ValueError('new cookie needed') except: c['session_id']=uuid().hex for m in c: if m=='session_id': self.sessioncookies[c[m].value] = time() c[m]["httponly"] = True c[m]["max-age"] = 3600 c[m]["expires"] = self.date_time_string(time()+3600) self.sessionidmorsel = c[m] break
The way we call get_all()
provides us with an empty list if there are no cookies in the headers. Either way we end up with a (possibly empty) string that contains the cookies the client sent us. This cookie (or cookies) can be converted to a SimpleCookie
object from Python's http.cookie
module (line 14).
Cookies are basically key/value pairs with some extra attributes. The whole ensemble is called a morsel. The SimpleCookie
object acts as a dictionary that indexes those morsels by key. The whole excersize is aimed at maintaining a session id so we check if our cookie object holds a session_id
morsel and use its value (a GUID) as an index into the sessioncookies
class variable. This class variabele maintains a dictionary indexed by the GUIDs of the session id cookies we produced. The corresponding values are their timestamps. Line 17 will therefore raise an exception if
- no session_id cookie was provided by the client,
- the session_id is unknown to us,
- the session_id is expired, i.e. older than one hour, or
- when we explicitely indicated we want a new cookie, no matter what.
At this point we are guaranteed to have a SimpleCookie
object that contains a session_id. Our final tasks are to store the timestamp of this cookie and to update or set some additional attributes on this morsel. The client might have sent more than one cookie so we iterate over all morsel and stop at the first session_id morsel we find. We set its httponly
attribute to signal to the browset that this cookie should not be manipulated by any client side JavaScript and set both its expires
attribute and its max-age
before we store this specific morsel in an instance variabele. This way we can add this cookie to the response headers one we have processed the request. An outline is sketched in the snipper below:
... def do_GET(self): ... self._session_cookie() ... if not (self.sessionidmorsel is None): self.send_header('Set-Cookie',self.sessionidmorsel.OutputString()) ...
Security considerations
At this point we have a tool to manage a session id. Such a session id can be used as a key to access other session information, a topic we cover in a later article. Before we even start thinking of using this we should consider the security issues.
Pythonsecurity.org has a handy checklist that will walk through:
- Are session IDs exposed in the URL?
- No, we use cookies and those are part of the HTTP headers.
- Do session IDs timeout and can users log out?
- Our session IDs certainly timeout but providing a log out option should be part of the web application.
- When a user logs out or times out, is the session invalidated?
- At this point we only dealing with session ids.
- Are session IDs rotated after successful login?
- That is why we provide the
forcenew
paramter. After a successful login the web application should call_session_cookie()
again withforcenew=True
- Are session IDs only sent over TLS/SSL?
- That should be implemented in the HTTP server, here we only look at the request handling part.
- Are session IDs completely randomly generated?
- We use a variant 4 uuid from Python's
uuid
module which should be completely random. The documentation makes no claims about the quality of the random generator used but a quick look at the code of theuuid
module (in Python 3.2) reveals that is uses either system provided functions or the built-inrandom()
function. In other words, we don't know what we get and that is bad! It is probably better useos.urandom()
directly which will raise a NotImplentedError if a source of randomness couldn't be found. Generating a a string of 32 hex digits might be done as follows:"%02x"*16%tuple(os.urandom(16))