Django middleware, sessions and users
A brief walkthrough of the Django session and authentication middleware.
tl;dr If you already know how Django sessions and auth work, read no further - you won't learn anything new.
Coming from a .NET 'enterprise' background, one of the things that confused me at first about Django was the phrase 'middleware'. In my old world this meant a messaging system - think Tibco, MQSeries, BizTalk, Mulesoft - big, complicated, expensive products, installed and configured by big, complicated, expensive consultancies.
This is not what is meant in the Djangoverse - to quote from the source:
Middleware is a framework of hooks into Django’s request/response processing. It’s a light, low-level "plugin" system for globally altering Django’s input or output.
When creating a new Django project, two specific pieces of middleware are nearly always included. Django wouldn't really work without them - they are the SessionMiddleware and AuthenticationMiddleware. They add session support and user authentication to Django apps - but how do they work?
This article is an aide-memoire to myself on how these work together, and how that influenced our ProfileMiddleware.
First off, ordering is important. Middleware is invoked in order of declaration, and as you'll see that has consequences. You should begin with the SessionMiddleware.
A brief diversion into HTTP.
Before discussing what the middleware does to an HTTP request object, it's worth noting what the default HttpRequest object is. By default it represents a basic HTTP request - which means that it understands cookies, headers, methods, paths etc., but critically has no understanding of sessions or users - as these are application-specific concepts, hence the use of middleware.
The second thing to note is that, this being Python, adding attributes to objects is as easy as … adding the attribute to the object. This is the key to request-enrichment (and thereby sessions, users, profiles etc.)
SessionMiddleware
The SessionMiddleware adds session support to requests. This means that from within your app you have access to the request.session
dictionary (or more specifically a 'dict-like object').
HTTP has no concept of sessions (being stateless), and so a session is simply a serializable data blob that is stored server-side, and rehydrated on each request. Identifying which session blob belongs to a request is done via cookies - which the default HttpRequest object does understand.
It's this simple:
session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)
request.session = self.SessionStore(session_key)
If the cookie exists, and the SessionStore can find a matching session, it is rehydrated; if it can't be found, a new session blob is created, and the session key response cookie is updated.
By the time your request has passed through the session middleware, it will have a valid session property. It may not have anything in it, but it will exist.
AuthenticationMiddleware
Now that we have a session attached to our request, we can start to manage state. And the first thing most applications will want to do is identify the requestor. Our request object may have a session property now, but it doesn't have a user property. This is what the AuthenticationMiddleware does, and it depends on the session that we have just created.
In fact, it will blow up if this hasn't been set:
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'."
From then on, it's a case of looking for an existing user_id in the session dictionary, and then looking up the user record if it exists, and finally attaching it to the request object.
user_id = request.session['_auth_user_id']
request.user = backend.get_user(user_id) or AnonymousUser()
In order to ensure that every request has a user property, the AnonymousUser
is a special implementation of User
that is applied in those cases when the user_id doesn't exist. It looks like a User object, but is fixed to return is_authenticated = False
.
Now that we have pushed the request through session and authentication middleware we have a request object that is guaranteed to have session and user properties.
And now for something … else.
One of the long-standing gripes with Django's authentication feature has been the restrictive User model and way in which extended user profiles work. this has been largely addressed in v1.5, however this still doesn't work for our use case, where we have more than one kind of user profile (specifically one for employers, and another for employees). We had a couple of utility functions for fetching the appropriate profile, but our codebase was littered with profile-related lookups and validation (is this person an employer or employee?). I decided to clean this up, and took the existing middleware pattern as my guide.
We now have our own ProfileMiddleware, which runs after the AuthenticationMiddleware (remember that ordering is important - we need the request.user
object to exist). This works in much the same way as the other middleware, extending the user property, so that it now has a profile property (request.user.profile
), along with some helper shortcuts (request.user.is_employer
):
profile = get_user_profile(request.user)
request.user.profile = profile
This has greatly simplified our codebase, along with reducing the number of profile-related lookups.
Making Freelance Work
Posted in: django