Changing the session key to a different user model field

I have my own user model. Django derives the session key from the primary key in this model. However, I wish to use a different field as the primary key / session key. The problem with changing the primary key is that it will break existing logged in sessions since the Django session middleware code tries to convert the session key to the type of the primary key defined in the model. I wish to change from username to a uuid. I thought I could work around this by having different auth backends since the session stores the backend but this type conversion occurs before Django calls the get_user method in my custom backends. Is there a way to uses a new field as the primary key?

How does Django finds the user for a given session

The details are, as probably in most web frameworks, a bit messy, and requires some digging. The request.user is set in the AuthenticationMiddleware [Django-doc] with a SimpleLazyObject [GitHub]:

class AuthenticationMiddleware(MiddlewareMixin):
    def process_request(self, request):
        if not hasattr(request, "session"):
            raise ImproperlyConfigured(
                "The Django authentication middleware requires session "
                "middleware to be installed. Edit your MIDDLEWARE setting to "
                "insert "
                "'django.contrib.sessions.middleware.SessionMiddleware' before "
                "'django.contrib.auth.middleware.AuthenticationMiddleware'."
            )
        request.user = SimpleLazyObject(lambda: get_user(request))
        request.auser = partial(auser, request)

The get_user(…) [GitHub] itself does not much more than checking if the user is somehow already cached, and if not fetch it:

def get_user(request):
    if not hasattr(request, "_cached_user"):
        request._cached_user = auth.get_user(request)
    return request._cached_user

Now the interesting part is the auth.get_user(…) [GitHub], which looks if the session data contains the user session key, and the backend to use:

def get_user(request):
    # …
    user_id = _get_user_session_key(request)
    backend_path = request.session[BACKEND_SESSION_KEY]
    # …

It is thus important to note that the session does not only keep the user_id, but also the backend that was used for that id, to prevent having to enumerate over the backends again.

But even if that was not the case, it is not easy to just change the backend. Indeed, if we look at _get_user_session_key(…) [GitHub], we see:

def _get_user_session_key(request):
    # This value in the session is always serialized to a string, so we need
    # to convert it back to Python whenever we access it.
    return get_user_model()._meta.pk.to_python(request.session[SESSION_KEY])

It thus uses the primary key field to "deserialize" the session key, even if you would have used a different field first.

This is the same when we log in: in the login(…) function [GitHub], it sets the primary key of the user:

def login(request, user, backend=None):
    # …
    request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)
    # …

The two are thus much tailored towards the primary key. The (default) ModelBackend, does not do much, except fetching the item with the primary key [GitHub]:

def get_user(self, user_id):
    try:
        user = UserModel._default_manager.get(pk=user_id)
    except UserModel.DoesNotExist:
        return None
    return user if self.user_can_authenticate(user) else None

Work (temporary) with two primary keys

However, if I change the primary key on my user model to the id field, I will break active sessions that are using the user name as the session key.

You coud monkey-patch the .get_user(…) method of the ModelBackend, for example in the .ready() method [Django-doc] of any of the AppConfigs in your project:

# app_name/apps.py
from django.apps import AppConfig


class MyAppConfig(AppConfig):
    def ready(self):
        from django.contrib.auth import get_user_model

        UserModel = get_user_model()

        def get_user(self, user_id):
            try:
                user = UserModel._default_manager.get(pk=user_id)
            except UserModel.DoesNotExist:
                try:
                    user = UserModel._default_manager.get(username=user_id)
                except UserModel.DoesNotExist:
                    return None
            return user if self.user_can_authenticate(user) else None

        from django.contrib.auth.backends import ModelBackend

        ModelBackend.get_user = get_user  # 🖘 monkey-patch

But that being said, I don't think that people getting logged out is that much of a big deal: eventually people will get logged out, because the cookie expires holding the session, or because of other reasons.

I think that using two natural keys imposes a security risk: if I somehow know the UUID of a certain user, I could try to rename the username to that UUID, essentially stealing the session.

Conclusion

What is perhaps more important is that the primary key of a model should typically be seen as a blackbox item. Sure, the primary key of many models is in fact an integer, but that is a technical detail. It makes not much sense to add two primary keys together for example. In database design, typically they teach to make the primary key a natural key. It is more a "Django-ism" to always use a black-box object, such as an AutoField, or a UUIDField.

While one can use monkey patch, it only introduces security risks, so I would advise not to do that, and let the sessions just expire: people then can login again. Changing the primary key imposes also the same risks.

Back to Top