CodeSpent
CodeSpent

Reputation: 1914

Extending AbstractBaseUser not hitting ModelBackend - Django

I'm extending AbstractBaseUser with my custom user model. I can create a superuser via shell successfully with the UserManager() below which is created in the database correctly.

For testing, I've created a superuser with the username test & password of test.

check_password()

    def set_password(self, raw_password):
        self.password = make_password(raw_password)
        self._password = raw_password


    def check_password(self, raw_password):
        """
        Return a boolean of whether the raw_password was correct. Handles
        hashing formats behind the scenes.
        """
        def setter(raw_password):
            self.set_password(raw_password)
            # Password hash upgrades shouldn't be considered password changes.
            self._password = None
            self.save(update_fields=["password"])
        return check_password(raw_password, self.password, setter)

I can run this test user against the check_password("test", "test") method which returns True as expected, but if I try to login via /admin I get "Password Incorrect" with a 200 status code on the POST.

Update: check_password does return False when given the raw password & hash

>>> u = User.objects.get(pk=1)
>>> u.check_password('test')
False
>>> u.check_password('pbkdf2_sha256$150000$sWSs4Yj3gQe1$75A2JmFurNX2oOeKJ18TvsB2G3YU6mYjIuHlaH7i6/k=')
False

Relevant app versions

Django==2.2.3
djangorestframework==3.10.1

User Model

class User(AbstractBaseUser):
    USERNAME_FIELD = ('username')
    REQUIRED_FIELDS = ('email', 'password')

    username = models.CharField(max_length=15, unique=True)
    twitch_id = models.IntegerField(null=True)
    avatar = models.URLField(null=True, blank=True)
    is_live = models.BooleanField(default=False)
    email = models.EmailField(unique=True)
    password = models.CharField(max_length=50, default="password")
    register_date = models.DateTimeField(auto_now=True)
    twitch_token = models.ForeignKey(TwitchToken, on_delete=models.SET_NULL, null=True)
    twitter_token = models.ForeignKey(TwitterToken, on_delete=models.SET_NULL, null=True)

    # attempted giving flags from original User model
    is_admin = models.BooleanField(default=False)
    is_active = models.BooleanField(default=False)
    is_staff = models.BooleanField(default=False)
    is_superuser = models.BooleanField(default=False)

    objects = UserManager()

    class Meta:
        db_table = 'users_user'     

    def __str__(self):
        return self.username

"""
Properties are redundant with flags above


    @property
    def is_admin(self):
        return self.admin

    @property
    def is_active(self):
        return self.active

    @property
    def is_staff(self):
        return self.staff

    @property
    def is_superuser(self):
        return self.superuser
"""

UserManager()

class UserManager(BaseUserManager):
    def create_user(self, username, email, password):
        """
        Creates and saves a User with the given email and password.
        """
        if not email:
            raise ValueError('Users must have an email address')

        user = self.model(
            email=self.normalize_email(email),
        )

        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_staffuser(self, username, email, password):
        """
        Creates and saves a staff user with the given email and password.
        """
        user = self.create_user(
            username,
            email,
            password,
        )

        user.staff = True
        user.save(using=self._db)
        return user

    def create_superuser(self, username, email, password):
        """
        Creates and saves a superuser with the given email and password.
        """
        user = self.create_user(
            username,
            email,
            password,
        )
        user.is_staff = True
        user.is_admin = True
        user.is_active = True
        user.save(using=self._db)
        return user

I am explicitly stating to use django.contrib.auth.backends.ModelBackend (default) in my settings & my AUTH_USER_MODEL is set. (I have seen some use a tuple & others use a list. I've tried both, same results)

AUTH_USER_MODEL = 'users.User'


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

I suspect I'm not even hitting ModelBackend 'cause I've put some prints in the authenticate() method that aren't running, as well I have deleted the entire file & see the same results. So I suspect the issue is somewhere between Django's determination of the auth user model & actually attempting authentication.

I've looked through countless SO posts & forum posts and I'm not seeing any step of the extension process that I'm missing, but I can't get anything valuable from stack traces either.

Django ModelBackend for Reference

from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission

UserModel = get_user_model()


class ModelBackend:
    """
    Authenticates against settings.AUTH_USER_MODEL.
    """

    def authenticate(self, request, username=None, password=None, **kwargs):
        if username is None:
            username = kwargs.get(UserModel.USERNAME_FIELD)
        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.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, True, None)
        return is_active or is_active is None

    def _get_user_permissions(self, user_obj):
        return user_obj.user_permissions.all()

    def _get_group_permissions(self, user_obj):
        user_groups_field = get_user_model()._meta.get_field('groups')
        user_groups_query = 'group__%s' % user_groups_field.related_query_name()
        return Permission.objects.filter(**{user_groups_query: user_obj})

    def _get_permissions(self, user_obj, obj, from_name):
        """
        Return the permissions of `user_obj` from `from_name`. `from_name` can
        be either "group" or "user" to return permissions from
        `_get_group_permissions` or `_get_user_permissions` respectively.
        """
        if not user_obj.is_active or user_obj.is_anonymous or obj is not None:
            return set()

        perm_cache_name = '_%s_perm_cache' % from_name
        if not hasattr(user_obj, perm_cache_name):
            if user_obj.is_superuser:
                perms = Permission.objects.all()
            else:
                perms = getattr(self, '_get_%s_permissions' % from_name)(user_obj)
            perms = perms.values_list('content_type__app_label', 'codename').order_by()
            setattr(user_obj, perm_cache_name, {"%s.%s" % (ct, name) for ct, name in perms})
        return getattr(user_obj, perm_cache_name)

    def get_user_permissions(self, user_obj, obj=None):
        """
        Return a set of permission strings the user `user_obj` has from their
        `user_permissions`.
        """
        return self._get_permissions(user_obj, obj, 'user')

    def get_group_permissions(self, user_obj, obj=None):
        """
        Return a set of permission strings the user `user_obj` has from the
        groups they belong.
        """
        return self._get_permissions(user_obj, obj, 'group')

    def get_all_permissions(self, user_obj, obj=None):
        if not user_obj.is_active or user_obj.is_anonymous or obj is not None:
            return set()
        if not hasattr(user_obj, '_perm_cache'):
            user_obj._perm_cache = {
                *self.get_user_permissions(user_obj),
                *self.get_group_permissions(user_obj),
            }
        return user_obj._perm_cache

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

    def has_module_perms(self, user_obj, app_label):
        """
        Return True if user_obj has any permissions in the given app_label.
        """
        return user_obj.is_active and any(
            perm[:perm.index('.')] == app_label
            for perm in self.get_all_permissions(user_obj)
        )

    def get_user(self, user_id):
        try:
            user = UserModel._default_manager.get(pk=user_id)
        except UserModel.DoesNotExist:
            return None
        return user if self.user_can_authenticate(user) else None


class AllowAllUsersModelBackend(ModelBackend):
    def user_can_authenticate(self, user):
        return True


class RemoteUserBackend(ModelBackend):
    """
    This backend is to be used in conjunction with the ``RemoteUserMiddleware``
    found in the middleware module of this package, and is used when the server
    is handling authentication outside of Django.

    By default, the ``authenticate`` method creates ``User`` objects for
    usernames that don't already exist in the database.  Subclasses can disable
    this behavior by setting the ``create_unknown_user`` attribute to
    ``False``.
    """

    # Create a User object if not already in the database?
    create_unknown_user = True

    def authenticate(self, request, remote_user):
        """
        The username passed as ``remote_user`` is considered trusted. Return
        the ``User`` object with the given username. Create a new ``User``
        object if ``create_unknown_user`` is ``True``.

        Return None if ``create_unknown_user`` is ``False`` and a ``User``
        object with the given username is not found in the database.
        """
        if not remote_user:
            return
        user = None
        username = self.clean_username(remote_user)

        # Note that this could be accomplished in one try-except clause, but
        # instead we use get_or_create when creating unknown users since it has
        # built-in safeguards for multiple threads.
        if self.create_unknown_user:
            user, created = UserModel._default_manager.get_or_create(**{
                UserModel.USERNAME_FIELD: username
            })
            if created:
                user = self.configure_user(user)
        else:
            try:
                user = UserModel._default_manager.get_by_natural_key(username)
            except UserModel.DoesNotExist:
                pass
        return user if self.user_can_authenticate(user) else None

    def clean_username(self, username):
        """
        Perform any cleaning on the "username" prior to using it to get or
        create the user object.  Return the cleaned username.

        By default, return the username unchanged.
        """
        return username

    def configure_user(self, user):
        """
        Configure a user after creation and return the updated user.

        By default, return the user unmodified.
        """
        return user


class AllowAllUsersRemoteUserBackend(RemoteUserBackend):
    def user_can_authenticate(self, user):
        return True

Upvotes: 1

Views: 409

Answers (1)

Iain Shelvington
Iain Shelvington

Reputation: 32294

The ModelBackend.authenticate method first gets the user object from the database using the get_by_natural_key method of the user models default manager, if this fails then authentication will fail

def get_by_natural_key(self, username):
    return self.get(**{self.model.USERNAME_FIELD: username})

Because your create_user method is not setting the username field correctly this is failing

The reason why you were able to create the user even though the username field is required is probably because you are using MySQL and running in non-strict mode in which case null values will be converted to empty strings

Upvotes: 2

Related Questions