Jacob Kaplan-Moss

Django internals: authentication

I wrote this post in 2009, more than 15 years ago. It may be very out of date, partially or totally incorrect. I may even no longer agree with this, or might approach things differently if I wrote this post today. I rarely edit posts after writing them, but if I have there'll be a note at the bottom about what I changed and why. If something in this post is actively harmful or dangerous please get in touch and I'll fix it.

Django’s session and authentication frameworks are designed to Just Work™, and can seem pretty magical. Like the rest of Django, though, these parts aren’t magic — just Python. So let’s take a look at the internals of sessions and authentication and see how the whole thing works.

Our journey begins with the session middleware. You’ll see that the process_request method is pretty simple: it looks up the session engine setting, looks for a session key in the request’s cookies, and then sets request.session to a SessionStore instance.

I’ll skip over the details of SessionStore (see the implementations if you want to get more details) other than to note that SessionStore` just behaves like a dict, and that the data you store there is automatically persisted for you. All that the user receives is a session cookie, an opaque key identifying the session. You can read more about the technical details of the session framework in Chapter 14 of The Django Book.

I’ll assume from here on out that you’re using the database session backend since that’s the easiest to poke at.

So now we hop over to the auth middleware. The auth middleware is similarly simple: it sets request.user to a LazyUser [1]. Like request.session, request.user is lazy: the user won’t be loaded until request.user is accessed.

When request.user is accessed – often by something like the @login_required decorator – LazyUser calls django.contrib.auth.get_user(), passing in the request; get_user() pulls info out of the session and, if the user is authenticated, returns the appropriate User instance.

So what’s the data that’s stored in the session? Let’s take a look:

>>> from django.contrib.sessions.models import Session
>>> s = Session.objects.get(pk='d638d3e640d2133c8cc0b73d0e88c6b3')
>>> s.get_decoded()
{'_auth_user_backend': 'django.contrib.auth.backends.ModelBackend',
 '_auth_user_id': 1}

So you can see that a properly authenticated user gets two pieces of information stored in the session: (a) which backend was used to authenticate that user and (b) what the user’s ID is.

So how does that information get there?

Most of the time, a user is going to get logged in from some view. Django ships with such a view; it’s django.contrib.auth.views.login. There’s a fair bit going on there that’s related to processing the view and request itself; the important details are the steps you need to take to log in a user:

  1. Given a dict of credentials, call django.contrib.auth.authenticate(**credentials) (i.e. authenticate(username='jacob', password='mypass')). authenticate() returns the User object, if successful, or None if the credentials are incorrect.
  2. Given a properly authenticated user, call django.contrib.auth.login(request, user). This is what stores the backend and user ID info in the session.

The final step in the authentication dance happens back in the session middleware; SessionMiddleware.process_response() will notice that request.session has been modified (in this case by login()) and save the altered data in the session.

[1]Technically, the auth middleware sets request.__class__.user to LazyUser(), which is a descriptor – you’ll recall that descriptors may only be class attributes.