Django : Case insensitive matching of username from auth user?
Asked Answered
I

7

24

Django by-default implements username as case sensitive, now for authentication I have written my own Authentication Backend to handle case insensitive usernames while authentication.

As shown in : http://blog.shopfiber.com/?p=220

Now, the problem is :

I have various views and util methods which compares username to some stings.

i.e.

request.user.username == username_from_some_other_system_as_str

Now, if username is yugal then:

request.user.username == 'Yugal' # Returns False

Now, it should return True [ What I wanted to achieve ]

For that I remember from C++ days, Operator Overloading. But I don't think simply doing that for django's auth user would be a good idea, since auth user is tightly bound with django. Also, overloading == will make it case-insensitive for the whole class not just for the username field.

So, how should I go about this username case-insensitivity even when compared throughout.

Note:

  • Creating a get_username method that returns lower-case username always is not possible, since it would require all code to be re-factored to use it. You can do it for your code for once, but not possible if you are using 3rd party django apps.

  • I know user.username.lower() = something.lower() is possible but is bug prone and not the write solution for something so often used in a multi-developer setup.

  • I have used SomeModel.objects.filter(username__iexact=username), wherever possible. But that still leaves the system vulnerable to a mistake by any of un-aware developer.

======================================

Figured out the solution conceptually, but could not make it work ( Help ) :

####### Custom CharField for username case-insensitivity #######
from django.db.models.fields import CharField
class iUnicode:
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        if isinstance(other, str) or isinstance(other, unicode):
            return self.value.lower() == other.lower()
        if isinstance(other, self.__class__):
            return other == self.value

    def __unicode__(self):
        return unicode(self.value)
    def __str__(self):
        return self.__unicode__()


class UsernameCharField(CharField):
    def to_python(self, value):  # Its not getting called
        unicode_val = super(CharField, self).to_python(value)
        return iUnicode(unicode_val)

if User._meta.local_fields[1].name == 'username':
    User._meta.local_fields[1] = UsernameCharField(max_length=30)
    User._meta.local_fields[1].model = User
################################################################

I assume to_python is used to convert the value received from database to unicode in python. But, I guess my to_python is not getting called.

This will also ensure case-insensitivity in 3rd party apps and would not require any re-factoring. It will patch the User at its core. I will be adding this to __init__.py of my first INSTALLED_APP

What am I doing wrong ?

Inexpressive answered 2/11, 2012 at 7:1 Comment(13)
Create seperate method on user class for comparing names and refactor all existing code.Byers
Refactoring all the code is not possible when using 3rd party django apps that might use it.Inexpressive
Let me add it to the question !Inexpressive
You can add the middleware, which wraps user.username in smth like this: class S(unicode): def __eq__(self, other): return self.lower() == other.lower()Auster
Not solving the root problem. I can but what if someone uses it directly from the database. [ Not request ].Inexpressive
@YugalJindle Then always save lower case only ( and you may keep "real" username in seperate field ). The number of problems raises really fast. You should consider changing the design.Byers
Try what freakish said. Still the best solution will be implementing a method returning the lowercased username and refactoring the code to use it everywhere.Factor
Well, refactoring for a separate method is out of question since 3rd party apps use my method. Now, saving username in lowercase again can be done in my custom Form. But again, that won't work for 3rd party app. Solving it at the User class level was a solution but we can't edit that.Inexpressive
@YugalJindle So basically you are saying that you can't modify anything and yet you want solve this problem? :)Byers
could you create a wrapper for your method, which accepts the same arguments, but can temper with the data before/after passing it to the actual method?Stereopticon
Well, this seems like that. But what I mean is - I am looking for a clever monkey patch to trick the username. I have some implementation - Adding it.Inexpressive
@Byers here you go.. I am almost there. Working out the clever way. Please help debug.Inexpressive
Feel free to ask for more clarifications :-|Inexpressive
I
4

Finally got it :

With so much experimenting and minimum effect on User model, finally achieved it. [ Thanks to Mr. @freakish for a different thought ]

Here it is :

############ username case-insensitivity ############
class iunicode(unicode):
    def __init__(self, value):
        super(iunicode, self).__init__(value)
        self.value = value

    def __eq__(self, other):
        if isinstance(other, str) or isinstance(other, unicode):
            return self.value.lower() == other.lower()
        if isinstance(other, self.__class__):
            return other == self.value


def custom_getattribute(self, name):
    val = object.__getattribute__(self, name)
    if name == "username":
        val = iunicode(val)
    return val

def auth_user_save(self, *args, **kwargs): # Ensures lowercase usernames
    username = self.username
    if username and type(username) in [unicode, str, iunicode]:
        self.username = username.lower()   # Only lower case allowed
    super(User, self).save(*args, **kwargs)

User.__getattribute__ = custom_getattribute
User.save = MethodType(auth_user_save, None, User)
#####################################################

I tested it and it worked as expected. :D

So, here are the testcases :

from django.test.testcases import TestCase

def create_user(data='testuser'):
    email = '%s@%s.com' % (data, data)
    user = G(User, username=data, email=email, is_active=True)
    user.set_password(data)
    user.save()
    return user

class UsernameCaseInsensitiveTests(TestCase):

    def test_user_create(self):
        testuser = 'testuser'
        user = create_user(testuser)
        # Lowercase
        self.assertEqual(testuser, user.username)
        # Uppercase
        user.username = testuser.upper()
        user.save()
        self.assertEqual(testuser, user.username)

def test_username_eq(self):
    testuser = 'testuser'
    user = create_user(testuser)
    self.assertTrue(isinstance(user.username, iunicode))
    self.assertEqual(user.username, testuser)
    self.assertEqual(user.username, testuser.upper())
    self.assertTrue(user.username == testuser.upper())
    self.assertTrue(testuser.upper() == user.username)
    self.assertTrue(user.username == iunicode(testuser.upper()))
Implicit Case-insensitive queries for database
###################### QuerySet #############################
def _filter_or_exclude(self, negate, *args, **kwargs):
    if 'username' in kwargs:
        kwargs['username__iexact'] = kwargs['username']
        del kwargs['username']
    if args or kwargs:
        assert self.query.can_filter(),\
        "Cannot filter a query once a slice has been taken."
    from django.db.models import Q
    clone = self._clone()
    if negate:
        clone.query.add_q(~Q(*args, **kwargs))
    else:
        clone.query.add_q(Q(*args, **kwargs))
    return clone

from django.db.models.query import QuerySet
QuerySet._filter_or_exclude = _filter_or_exclude
#############################################################

This will allow, User.objects.get(username='yugal') & User.objects.get(username='YUGAl') yield the same user.

Inexpressive answered 3/11, 2012 at 9:21 Comment(0)
B
44

As of Django 1.5, making usernames case insensitive is straightforward:

class MyUserManager(BaseUserManager):
    def get_by_natural_key(self, username):
        return self.get(username__iexact=username)

Sources: 1, 2

Ballance answered 31/10, 2015 at 20:29 Comment(3)
I think this method is hands down the simplest way of achieving this. Couple this with an overridden UserManager.create_user method where you can lower case the username before saving to the DB and you're golden.Saturn
Or, if you want to observe the custom USERNAME_FIELD, change the last line to: return self.get(**{self.model.USERNAME_FIELD + '__iexact': username})Silicate
pytest fixtures for django expect the create_user and create_superuser methods. I expect other django stuff expects it (for example, the management command createsuperuser). I extended UserManager as in @jitesh2796 's answer, rather than implementing my own versions of those methods.Stanleystanly
F
7

I modified few lines in my registration and login process that seems to work for me. With my solution usernames will still be displayed like the user wrote them when registering, but it will not allow others to use the same username written differently. It also allows users to login without worrying about writing the case sensitive username.

I modified the registration form to search for case insensitive usernames.

This is line from my validation of username, it searches for user with this username.

User._default_manager.get(username__iexact=username)

Then I needed to allow users to login with case insensitive usernames.

From my login view:

username = request.POST['username']
password = request.POST['password']
caseSensitiveUsername = username
try:
  findUser = User._default_manager.get(username__iexact=username)
except User.DoesNotExist:
  findUser = None
if findUser is not None:
  caseSensitiveUsername = findUser.get_username
user = auth.authenticate(username=caseSensitiveUsername, password=password)
Footpoundsecond answered 22/2, 2015 at 14:3 Comment(1)
It worked fine for me, but I had to use findUser instead of findUser.get_username in the last but one line.Astounding
I
4

Finally got it :

With so much experimenting and minimum effect on User model, finally achieved it. [ Thanks to Mr. @freakish for a different thought ]

Here it is :

############ username case-insensitivity ############
class iunicode(unicode):
    def __init__(self, value):
        super(iunicode, self).__init__(value)
        self.value = value

    def __eq__(self, other):
        if isinstance(other, str) or isinstance(other, unicode):
            return self.value.lower() == other.lower()
        if isinstance(other, self.__class__):
            return other == self.value


def custom_getattribute(self, name):
    val = object.__getattribute__(self, name)
    if name == "username":
        val = iunicode(val)
    return val

def auth_user_save(self, *args, **kwargs): # Ensures lowercase usernames
    username = self.username
    if username and type(username) in [unicode, str, iunicode]:
        self.username = username.lower()   # Only lower case allowed
    super(User, self).save(*args, **kwargs)

User.__getattribute__ = custom_getattribute
User.save = MethodType(auth_user_save, None, User)
#####################################################

I tested it and it worked as expected. :D

So, here are the testcases :

from django.test.testcases import TestCase

def create_user(data='testuser'):
    email = '%s@%s.com' % (data, data)
    user = G(User, username=data, email=email, is_active=True)
    user.set_password(data)
    user.save()
    return user

class UsernameCaseInsensitiveTests(TestCase):

    def test_user_create(self):
        testuser = 'testuser'
        user = create_user(testuser)
        # Lowercase
        self.assertEqual(testuser, user.username)
        # Uppercase
        user.username = testuser.upper()
        user.save()
        self.assertEqual(testuser, user.username)

def test_username_eq(self):
    testuser = 'testuser'
    user = create_user(testuser)
    self.assertTrue(isinstance(user.username, iunicode))
    self.assertEqual(user.username, testuser)
    self.assertEqual(user.username, testuser.upper())
    self.assertTrue(user.username == testuser.upper())
    self.assertTrue(testuser.upper() == user.username)
    self.assertTrue(user.username == iunicode(testuser.upper()))
Implicit Case-insensitive queries for database
###################### QuerySet #############################
def _filter_or_exclude(self, negate, *args, **kwargs):
    if 'username' in kwargs:
        kwargs['username__iexact'] = kwargs['username']
        del kwargs['username']
    if args or kwargs:
        assert self.query.can_filter(),\
        "Cannot filter a query once a slice has been taken."
    from django.db.models import Q
    clone = self._clone()
    if negate:
        clone.query.add_q(~Q(*args, **kwargs))
    else:
        clone.query.add_q(Q(*args, **kwargs))
    return clone

from django.db.models.query import QuerySet
QuerySet._filter_or_exclude = _filter_or_exclude
#############################################################

This will allow, User.objects.get(username='yugal') & User.objects.get(username='YUGAl') yield the same user.

Inexpressive answered 3/11, 2012 at 9:21 Comment(0)
U
3

The simplest way to use case insensitive username is to inherit from default ModelBackend and override authenticate method.

Please note that inside the except block we are executing UserModel().set_password(password), by doing this we decreasing hasher work time. Fixed bug report

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

from users.models import User

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:
            d = {'%s__iexact'%UserModel.USERNAME_FIELD: username}
            user = UserModel.objects.get(**d)
            if user.check_password(password):
                return user
        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)

        return None

And add this backend to AUTHENTICATION_BACKENDS in settings.py

AUTHENTICATION_BACKENDS = (
    'sdcpy.backends.CaseInsensitiveModelBackend', # inherits from 'django.contrib.auth.backends.ModelBackend'
)
Urania answered 2/2, 2015 at 13:59 Comment(0)
M
1

Using UserManager is one of the easiest way of achieving case-insensitive usernames without messing with other things.

Sample Code (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()

And you are good to go! Source

Malraux answered 19/9, 2020 at 10:22 Comment(2)
where would you place this? In home/app/models.py or /home/models.py?Obedient
former one! @ObedientMalraux
B
0

This monkey patching looks like a bad idea. You will definetly hit some problems in future ( Django does a lot of stuff behind the scene ). I highly recommend redesigning your app.

However here's what you can try ( using your iUnicode class ):

def new_getattribute( self, name ):
    val = object.__getattribute__( self, name )
    if name == "username":
        val = iUnicode( val )
    return val

User.__getattribute__ = new_getattr

Now, I'm not 100% that this will work, and it is a bit hacky, so use it with caution. :)

Byers answered 2/11, 2012 at 15:55 Comment(1)
Well, your solution is similar - keeping all 3rd party apps independent. If this works it will be great.Inexpressive
O
0

There is a relatively clean way to do this:

# Case-insensitive django authentication, modified from
# http://justcramer.com/2008/08/23/logging-in-with-email-addresses-in-django/
# See also https://github.com/dabapps/django-email-as-username
# And https://docs.djangoproject.com/en/dev/topics/auth/customizing/#auth-custom-user
from django.contrib.auth.models     import User

class EmailOrUsernameModelBackend(object):
    def authenticate(self, username=None, password=None):
        username = username.lower()     # Force all usernames & email to all lower case
        if '@' in username:
            kwargs = {'email': username}
        else:
            kwargs = {'username': username}
        try:
            user = User.objects.get(**kwargs)
            if user.check_password(password):
                return user
        except User.DoesNotExist:
            return None

    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

def my_password_reset(request, **kwargs):
    # Override django.contrib.auth.views.password_reset not needed because django does
    # SELECT FROM "auth_user" WHERE UPPER("auth_user"."email"::text) = UPPER(E'[email protected]')
    # But note you may want to manually create an UPPER index in the database for speed.
    return password_reset(request, **kwargs)

Then set

AUTHENTICATION_BACKENDS = (
    'obviously.backends.EmailOrUsernameModelBackend',
    'django.contrib.auth.backends.ModelBackend',
)

You'll also have to force usernames to lower case in your registration workflow

This all works OK, but does not preserve the case given by the user, nor is it efficient in looking up in the database. The default django behavior is by design, see https://code.djangoproject.com/ticket/2273

Orifice answered 11/9, 2013 at 2:39 Comment(3)
Its not only about authentication. Its also about querying with username on anything. We can always do to_lower but its all about bugs which get in when people in large teams miss it at some point.Inexpressive
As I write above see code.djangoproject.com/ticket/2273 this is a design decision by djano... and none of the workarounds are pretty. I recommend saving all lower case for django's auth, and keeping the mixed case username in a private field.Orifice
Thanks for linking with an official verdict. They left it upto the developers in case it matters to their projects. So.. we have to use dirty solutions if we must.Inexpressive

© 2022 - 2024 — McMap. All rights reserved.