Search code examples
djangohashpasswordsbcryptdjango-authentication

Django hash integrate with legacy database


I am working with django on a mysql legacy database. I have integrated everything but passwords. The legacy database is storing the passwords this way $2a$10$Pdg3h8AVZ6Vl3X1mMKgQDuMriv8iysnValEa5YZO3j9pEboLrOBUK and django reads only if the same hash has the bcrypt$ prefix bcrypt$$2a$10$Pdg3h8AVZ6Vl3X1mMKgQDuMriv8iysnValEa5YZO3j9pEboLrOBUK . How do I make django authenticate users by reading first example hash?

And why django is adding the prefix to the password?

UPDATE I have added the model backend

from django.contrib.auth.backends import ModelBackend
from users.models import TbUser
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.db.models import Exists, OuterRef, Q

UserModel = get_user_model()


class BaseBackend:
    def authenticate(self, request, **kwargs):
        return None

    def get_user(self, user_id):
        return None

    def get_user_permissions(self, user_obj, obj=None):
        return set()

    def get_group_permissions(self, user_obj, obj=None):
        return set()

    def get_all_permissions(self, user_obj, obj=None):
        return {
            *self.get_user_permissions(user_obj, obj=obj),
            *self.get_group_permissions(user_obj, obj=obj),
        }

    def has_perm(self, user_obj, perm, obj=None):
        return perm in self.get_all_permissions(user_obj, obj=obj)


class ModelBackend(BaseBackend):
    """
    Authenticates against settings.AUTH_USER_MODEL.
    """

    def authenticate(self, request, username=None, password=None, **kwargs):
        print(">>>>>>>>>>>>>>>>>> Authentication start")
        print("User: ", username, " Password: ", password)
        if username is None:
            username = kwargs.get(UserModel.USERNAME_FIELD)
        if username is None or password is None:
            return
        try:
            user = UserModel._default_manager.get_by_natural_key(username)
        except UserModel.DoesNotExist:
            # Run the default password hasher once to reduce the timing
            # difference between an existing and a nonexistent user (#20760).
            UserModel().set_password(password)
        else:
            if user.password.startswith('bcrypt'):
                u = user
            else:
                u = UserModel(password=f'bcrypt${user.password}')
                print(">>>>>>>>>>>>>>>>>> Authentication prefix adding...")
                print(">>>>>>>>>>>>>>>>>> NO-PREFIX: ", user.password)
                print(">>>>>>>>>>>>>>>>>> WITH-PREFIX: ",
                      f'bcrypt${user.password}')
                print(">>>>>>>>>>>>>>>>>> CHECKING PASSWORD NO-PREFIX: ",
                      user.check_password(password))
                print(">>>>>>>>>>>>>>>>>> CHECKING PASSWORD WITH-PREFIX: ",
                      user.check_password(f'bcrypt${user.password}'))
                #user = UserModel(password=f'bcrypt${user.password}')
            if u.check_password(password) and self.user_can_authenticate(user):
                return user

    def user_can_authenticate(self, user):
        """
        Reject users with is_active=False. Custom user models that don't have
        that attribute are allowed.
        """
        is_active = getattr(user, 'is_active', None)
        return is_active or is_active is None

the settings.py is using this authentication backend

AUTHENTICATION_BACKENDS = [
    'users.backends.ModelBackend',
    'django.contrib.auth.backends.ModelBackend'
]

and when django checks the password it throws the following error

File "\site-packages\django\contrib\auth\__init__.py", line 73, in authenticate
    user = backend.authenticate(request, **credentials)
  File "\auth\users\backends.py", line 74, in authenticate
    if user.check_password(password) and self.user_can_authenticate(user):
  File "\site-packages\django\contrib\auth\base_user.py", line 112, in check_password
    return check_password(raw_password, self.password, setter)
  File "\site-packages\django\contrib\auth\hashers.py", line 49, in check_password
    must_update = hasher_changed or preferred.must_update(encoded)
  File "\site-packages\django\contrib\auth\hashers.py", line 443, in must_update
    return int(rounds) != self.rounds
ValueError: invalid literal for int() with base 10: '2b'

I have used a part of this implementation https://github.com/django/django/blob/main/django/contrib/auth/backends.py

TESTING

>>> from users.models import TbUser
>>> from django.contrib.auth import check_password
>>> user = TbUser.objects.get(username="Gastro")
>>> user
<TbUser: Gastro>
>>> ps = user.password
>>> ps_bc = "bcrypt$" + ps
>>> ps_bc
'bcrypt$$2a$10$rA59QU2GsWR4v6hugdYhruxY0bgZYVLv6ncxRe3BiDJMEpK0A0huW'
>>> check_password("111111", ps_bc)
True
>>> check_password("111111", ps)
False
>>> user.check_password("111111") 
False

Solution

  • Django first specifies the hashing algorithm that is used, since it can for example use different hashing algorithms for each user.

    There are basically two ways to solve this:

    1. changing the passwords in the database; or
    2. make a (slightly) different authentication backend that will prepend it with the password.

    Option 1: update the password

    You can update the records in bulk with:

    from django.db.models import F, Value
    from django.db.models.functions import Concat
    
    MyModel.objects.update(
        password=Concat(Value('bcrypt$'), F('password'))
    )

    This will thus update the password field (if the passwords are stored in another field, you use that field name) by prepending 'bcrypt$' to the hashes.

    Option 2: make a custom authentication method

    We can subclass the ModelBackend and slightly rewrite this to:

    # app_name/backends.py
    
    django.contrib.auth.backends import ModelBackend
    
    class MyModelBackend(ModelBackend):
    
        def authenticate(self, request, username=None, password=None, **kwargs):
            if username is None:
                username = kwargs.get(UserModel.USERNAME_FIELD)
            if username is None or password is None:
                return
            try:
                user = UserModel._default_manager.get_by_natural_key(username)
            except UserModel.DoesNotExist:
                # Run the default password hasher once to reduce the timing
                # difference between an existing and a nonexistent user (#20760).
                UserModel().set_password(password)
            else:
                if user.password.startswith('bcrypt'):
                    u = user
                else:
                    u = UserModel(password=f'bcrypt_sha256${user.password}')
                if u.check_password(password) and self.user_can_authenticate(user):
                    return user

    We can then register this backend in the AUTHENTICATION_BACKENDS [Django-doc]:

    # settings.py
    
    # …
    
    AUTHENTICATION_BACKENDS = [
        'app_name.backends.MyModelBackend',
        'django.contrib.auth.backends.ModelBackend'
    ]
    
    # …