Search code examples
pythondjangodjango-authenticationpassword-hash

Django password hasher with username in hash


Maybe a dumb question, but if possible, what's the best way of writing a BasePasswordHasher subclass using username as a part of the salt? I'm rewriting a site from scratch and used this approach in php. The problem is accessing the username in a password hasher. I would be really happy to resolve this as a LOT of users would lose their passwords otherwise, so a big thanks in advance!

PHP Code:

function passHash($login, $pass)
{
    return md5(md5($pass).'salt'.$login);
}

Solution

  • As you noticed, this cannot be done in the password hasher alone. The password hasher does not have information about the user, only the password and hash. I think you have two options.

    First, and probably best, is to write a custom authentication backend. At the authentication backend level, we have access to the username and raw password. It would look like this

    # settings.py
    AUTHENTICATION_BACKENDS=(
        'myapp.backends.LegacyBackend',
        'django.contrib.auth.backends.ModelBackend',
    )
    
    # myapp.backends
    from django.contrib.auth.backends import ModelBackend
    from django.contrib.auth import get_user_model
    from django.utils.encoding import force_bytes
    import hashlib
    
    class LegacyBackend(ModelBackend):
    
        # We only need to override the authenticate method
        def authenticate(self, username=None, password=None, **kwargs):
            # most of this is copied directly from ModelBackend's authenticate method
            UserModel = get_user_model()
            if username is None:
                username = kwargs.get(UserModel.USERNAME_FIELD)
            try:
                user = UserModel._default_manager.get_by_natural_key(username)
    
                # This is the normal route that hands off to the password hasher pipeline
                # but we will sidestep it entirely and verify the password here
                #
                # if user.check_password(password):
                #    return user
    
                pwhash = hashlib.md5(force_bytes(password)).hexdigest()
                hash = hashlib.md5(force_bytes(pwhash+"salt"+username)).hexdigest()
                if hash == user.password:
                    # update the user's password if you want, so you can phase out this backend
                    user.set_password(password)
                    user.save(update_fields=["password"])
                    return user
    
            except UserModel.DoesNotExist:
                UserModel().set_password(password)
    

    Note that I haven't tested this code, but it should work as advertised. In addition, you don't have conflicts with new users, and old users will have their passwords updated to the new hashing algorithm (default is PBKDF2+SHA256? not sure).

    The second option is to write a one-off script to modify your database so the user.password fields look like legacymd5$username+salt$hash. Then you can write your custom password hasher as planned.