As a proud web developer, I definitely know about cookies and sessions.

  • Cookie is used to pass data between browser and server
  • Session is the data stored on server side

That sounds about right. So when I logged into a Django site, pass the authentication then Django will have something remembered so I don’t have to login for every request.

But how exactly that worked?

Django authentication system

Django authentication system is based on session.

In Django authentication middleware 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def get_user(request):
if not hasattr(request, '_cached_user'):
request._cached_user = auth.get_user(request)
return request._cached_user


class AuthenticationMiddleware(object):
def process_request(self, request):
assert hasattr(request, 'session'), (
"The Django authentication middleware requires session middleware "
"to be installed. Edit your MIDDLEWARE_CLASSES setting to insert "
"'django.contrib.sessions.middleware.SessionMiddleware' before "
"'django.contrib.auth.middleware.AuthenticationMiddleware'."
)
request.user = SimpleLazyObject(lambda: get_user(request))

The middleware first assert there’s session avaible on request object, but what actually uses sessions is in the get_user function in auth module:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def get_user(request):
"""
Returns the user model instance associated with the given request session.
If no user is retrieved an instance of `AnonymousUser` is returned.
"""

from .models import AnonymousUser
user = None
try:
user_id = _get_user_session_key(request)
backend_path = request.session[BACKEND_SESSION_KEY]
except KeyError:
pass
else:
... # Acutally use user_id and backend_path to load user info from session

Session

As I already know, the session is what Django useing on server side, basic HTTP protocal is stateless, most web server frameworks implemnt the session mechanism to make the request infomation persistable. From the authentication code above, all magic happend in request.session. It’s an object of SessionStore, which is the class in django to manage how the session data is saved, based on different settings, Django support save the session data in different places. For the sake of focusing on Django session and browser, I’ll view it as a blackbox 2. The session object is added to request in Django’s SessionMiddleware

1
2
3
4
5
6
7
8
class SessionMiddleware(object):
def __init__(self):
engine = import_module(settings.SESSION_ENGINE)
self.SessionStore = engine.SessionStore

def process_request(self, request):
session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
request.session = self.SessionStore(session_key)

And look at the above code closer, the session object is actually looked up by session key, getting from cookies. Problem solved! Django will put the key of session into cookies, then it can be reused across requests.

Cookies

In order to put the session key into cookies, in the process_response part of Django SessionMiddleware, after some cleanup work, it finally set the cookie in the end.

1
2
3
4
5
6
7
8
9
10
# Save the session data and refresh the client cookie.
# Skip session save for 500 responses, refs #3881.
if response.status_code != 500:
request.session.save()
response.set_cookie(settings.SESSION_COOKIE_NAME,
request.session.session_key, max_age=max_age,
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
path=settings.SESSION_COOKIE_PATH,
secure=settings.SESSION_COOKIE_SECURE or None,
httponly=settings.SESSION_COOKIE_HTTPONLY or None)

Unlike session, cookies is a real HTTP standard, more details can be found in RFC 6265. Here we can go over the arguments of the set_cookie function and find out what can be controlled in Django’s session.

SESSION_COOKIE_NAME
Cookie is key-value pairs, this is the name part of the session key in cookie

sesson_key
This is the actual value stored in cookie

max_age and expires
These two arguments are closely related, in cookie, they are “Max-Age” and “Expires” attributes. Both attributes control how long the data can stay in cookie, Max-Age is the seconds of the cookie liftime, Expires is the specific date when the cookie will expire.

Max-Age has a higher priority over exipres, if either of them exists, the cookie will be kept in browser until the end of its liftime, even after browser closed. If none of them are set, the cookie will be cleared after the session (this is the session of connection, not the server side session) closed, normally that means browser closed. As you might notice the two arguments are not read from settings directly, there’s a small part of logic to set them.

1
2
3
4
5
6
7
if request.session.get_expire_at_browser_close():
max_age = None
expires = None
else:
max_age = request.session.get_expiry_age()
expires_time = time.time() + max_age
expires = cookie_date(expires_time)

And the get_expire_at_browser_close method is basically controlled by settings.SESSION_EXPIRE_AT_BROWSER_CLOSE

1
2
3
4
5
6
7
8
9
10
def get_expire_at_browser_close(self):
"""
Returns ``True`` if the session is set to expire when the browser
closes, and ``False`` if there's an expiry date. Use
``get_expiry_date()`` or ``get_expiry_age()`` to find the actual expiry
date/age, if there is one.
"""

if self.get('_session_expiry') is None:
return settings.SESSION_EXPIRE_AT_BROWSER_CLOSE
return self.get('_session_expiry') == 0

domain
This is the Domain attribute, it controls if the cookie can be used for the request.

When using cookie, the request host needs to match the domain attribute, or the domain attribe is suffix of the request host. For example, if the domain ise set to .x.com, the request from a.x.com will be considered match as well.

When set the cookie, this attribute also needs to match request host, for security reason cookie specified for unmatched domain will be rejected. Which means you can’t set cookie from your site to google.com.

Also, event not violate the match logic, most agent will reject domain set on public suffix like “com” or “co.uk”

path
This is the Path attribute, like the domain attribute, it limits the scope of the cookie can be used. Only cookie with matched path is valid.

secure
This is the Secure attribute, if set to true, cookies will only be used in secure connection (normally means HTTPS). It’s default as False in Django settings, but it’s recommended to be set if you site has https.

httponly
This is HttpOnly Attribute, when set to true, non http api (javascript in most case) can’t access the cookie data, which is more secure.

One last thing

Now I feel even better knowledgeable about session and cookie, and how they worked together in Django. Django session is completely cookie based, but as session is implemented independenly by server framework, it’s possible to use session in other way. As user can set to use cookies or not in browser, if cookie is not usable, some web framework like PHP can fall back to use url argument to pass session key, but Django is designed to avoid that due to security consideration.

1 All code taken from django source code on github (tag 1.9.5)
2 There’s a session implementation called cookie based backend, which not only pass session key in cookies but also put all session data in cookies