Reputation: 1914
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 if I try to login via /admin I get "Password Incorrect" with a 200 status code on the POST.check_password("test", "test")
method which returns True
as expected, but
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
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