PageView

Subscribe to the Simple is Better Than Complex YouTube Channel!


How to Implement Case-Insensitive Username

How to Implement Case-Insensitive Username (Picture: https://stocksnap.io/photo/HX6YSHGXRB)

Inspired by a discussion in the How to Extend Django User Model comments, I decided to compile a few options on how to implement a case insensitive authentication using the built in Django User. Thanks to Paul Spiteri for bringing up the question and also to provide a possible solution!


Option 1: Extending The User Model

This is the best option if your project is in the beginning and you can afford swapping the authentication User. Actually any of the strategies described in the How to Extend Django User Model will work (except for the Proxy model, because the AUTH_USER_MODEL can’t be a Proxy).

Below, the most basic way to implement it, without messing around with anything:

models.py

from django.contrib.auth.models import AbstractUser, UserManager

class CustomUserManager(UserManager):
    def get_by_natural_key(self, username):
        case_insensitive_username_field = '{}__iexact'.format(self.model.USERNAME_FIELD)
        return self.get(**{case_insensitive_username_field: username})

class CustomUser(AbstractUser):
    objects = CustomUserManager()

Now, it’s just a matter of swapping the AUTH_USER_MODEL in the settings.py module:

settings.py

AUTH_USER_MODEL = 'core.CustomUser'

And that’s it!


Option 2: Custom Authentication Backend

Please note that there is a small difference in the implementation between Django 1.10 and 1.11, as from 1.11 the authenticate method receives a request object.

Django Version 1.10.x Or Below

If your application is already up and running and you can’t afford to customize the Django User model, this is the less intrusive way.

Create a python module named backends.py anywhere in your project and add the following snippet:

backends.py

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend


class CaseInsensitiveModelBackend(ModelBackend):
    def authenticate(self, username=None, password=None, **kwargs):
        UserModel = get_user_model()
        if username is None:
            username = kwargs.get(UserModel.USERNAME_FIELD)
        try:
            case_insensitive_username_field = '{}__iexact'.format(UserModel.USERNAME_FIELD)
            user = UserModel._default_manager.get(**{case_insensitive_username_field: username})
        except UserModel.DoesNotExist:
            # Run the default password hasher once to reduce the timing
            # difference between an existing and a non-existing user (#20760).
            UserModel().set_password(password)
        else:
            if user.check_password(password) and self.user_can_authenticate(user):
                return user

Now switch the authentication backend in the settings.py module:

settings.py

AUTHENTICATION_BACKENDS = ('mysite.core.backends.CaseInsensitiveModelBackend', )

Please note that 'mysite.core.backends.CaseInsensitiveModelBackend' must be changed to the valid path, where you created the backends.py module.

Django Version 1.11.x Or Higher

If your application is already up and running and you can’t afford to customize the Django User model, this is the less intrusive way.

Create a python module named backends.py anywhere in your project and add the following snippet:

backends.py

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend


class CaseInsensitiveModelBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        UserModel = get_user_model()
        if username is None:
            username = kwargs.get(UserModel.USERNAME_FIELD)
        try:
            case_insensitive_username_field = '{}__iexact'.format(UserModel.USERNAME_FIELD)
            user = UserModel._default_manager.get(**{case_insensitive_username_field: username})
        except UserModel.DoesNotExist:
            # Run the default password hasher once to reduce the timing
            # difference between an existing and a non-existing user (#20760).
            UserModel().set_password(password)
        else:
            if user.check_password(password) and self.user_can_authenticate(user):
                return user

Now switch the authentication backend in the settings.py module:

settings.py

AUTHENTICATION_BACKENDS = ('mysite.core.backends.CaseInsensitiveModelBackend', )

Please note that 'mysite.core.backends.CaseInsensitiveModelBackend' must be changed to the valid path, where you created the backends.py module.

Drawbacks

This change only affects the authentication process, it doesn’t enforce the uniqueness of the case insensitive username. Meaning that you can still have a username saved as John and another one as john and another one as JOHN. Actually if that’s the case, the application will crash when John or JOHN try to authenticate in the system, because UserModel._default_manager.get() will return more than one row, and will eventually throw an exception.

First thing you want to do, before you change the authentication backends, is to make sure there is no conflicting usernames in the database (same username with mixed case).

If you are good to go, make sure every input where the end user can set the username field is properly validated.

For example, if you are using the built-in UserCreationForm from django.contrib.auth.forms, extend it and enforce the validation. Below, a very straightforward example:

forms.py

from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User


class CaseInsensitiveUserCreationForm(UserCreationForm):
    def clean(self):
        cleaned_data = super(CaseInsensitiveUserCreationForm, self).clean()
        username = cleaned_data.get('username')
        if username and User.objects.filter(username__iexact=username).exists():
            self.add_error('username', 'A user with that username already exists.')
        return cleaned_data

The whole idea behind this strategy is to take care of all the possible ways the end user has to set the username, either by creating a new user or by editing it. Use it with caution.


I hope this tutorial could help you somehow!

Know a better way to solve this issue? Any questions? Comments? Leave a message in the comments below!