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
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:
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.
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'
]
# …