Drf: authenticating without the USERNAME_FIELD - django

Extended from: Drf how to: simple-jwt authenticating without the USERNAME_FIELD
I was trying to figure out how to authenticate a user with a field that is not set as the USERNAME_FIELD and faced some issues, it lets me input in the correct data fields, but it never authenticates
I'm using this snippet from the previous questions answer:
class MyTokenStudentSerializer(TokenObtainPairSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['student_id'] = serializers.CharField(required=False)
# self.fields['password'] = serializers.CharField(write_only=True, required=True)
self.fields['password'] = PasswordField(trim_whitespace=False)
username_field = 'student_id'
auth_fields = ['student_id']
#login view extended from TokenObtainPairView
class LoginStudentView(TokenObtainPairView):
permission_classes = (AllowAny,)
serializer_class = MyTokenStudentSerializer
produces
{
"detail": "No active account found with the given credentials"
}
any modifications would be greatly appreciated.

If you are using default ModelBackend you should specify USERNAME_FIELD
class User(AbstractUser):
USERNAME_FIELD = 'student_id'
student_id = models.TextField(default="", unique=True) # Should be unique
Output
~ $ curl -X POST "http://127.0.0.1:8000/api/auth/login-student/" -d "password=admin&student_id=stdnt"
{"refresh":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTY0MzcxMTgzMCwiaWF0IjoxNjQxMTE5ODMwLCJqdGkiOiJkY2MyNTEwZGRiNWE0ZTJmODllMDI2OWRkYWI5ZGVjNSIsInVzZXJfaWQiOjF9.c0QTdBhiPUf4yvPP0l3a-XQ0iD6kycECAdb6MAROY8g","access":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjQxMTIzNDMwLCJpYXQiOjE2NDExMTk4MzAsImp0aSI6ImM0NjA0ZTlhMDBhNjQ5YjdhMTkxOGQ3OTJmOTMyYTJiIiwidXNlcl9pZCI6MX0.XoZXTJICE_PyZFXIIvsm3bci-e-O67AsYvIvY1ijNAo"}
Also, you can write your own auth backend and include it in settings.py.
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'project.apps.auth.backends.MyCustomModelBackend',
]
Example of backend
class MyCustomModelBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
student_id = kwargs.get("student_id")
if student_id is None or password is None:
return
try:
user = User.objects.get(student_id=student_id)
except User.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (#20760).
User().set_password(password)
else:
if user.check_password(password) and self.user_can_authenticate(user):
return user
With this approach you can preserve login via username field. Also you shouldn't specify USERNAME_FIELD = 'student_id' in User model

Related

Using a custom model for Django Admin

I have a custom user model, which an admin model inherits:
class User(AbstractBaseUser, PermissionsMixin):
...
class Staff(User):
is_staff = models.BooleanField(
_("staff status"),
default=True,
help_text=_("Designates whether the user is staff"),
)
...
class Admin(Staff):
is_admin = models.BooleanField(
_("staff status"),
default=True,
help_text=_("Designates whether the user can log into this admin site."),
)
...
As well as a custom backend for authorization
class AdminBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
if username is None:
username = kwargs.get(Admin.USERNAME_FIELD)
if username is None or password is None:
return
try:
user = Admin._default_manager.get_by_natural_key(username)
except Admin.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (#20760).
Admin().set_password(password)
else:
if user.check_password(password) and self.user_can_authenticate(user):
return user
...
However, I am unable to log in due to has_permission in admin/sites.py
Is there a way to avoid registering a custom site, as that will increase complexity?

how to change the alias of the username in rest_framework_simplejwt using TokenObtainPairSerializer class?

I'm working on a project where user can register using his mobile no and password( after verifying with otp) what i'm doing is inside username field i'm saving user's phone no. as username is a mandatory field. And I'm using simple_jwt to get access token and refresh token. Everything is working fine
urls.py
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns =[
path('register', RegisterView.as_view() ),
path('otp/send', SendOtpView.as_view() ),
path('otp/verify', VerifyOtpView.as_view() ),
path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
instead of creating a loginSerializer i'm using rest_framework_simplejwt inbuild class TokenObtainPairView
But when i go to the url
auth/api/token/ it ask me for username and password, which is confusing as a user . how can i change the name of the username to phone.
I don't have any idea how to do it as I'm new to the djangorestframework
serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class LoginSerializer(TokenObtainPairSerializer):
password = serializers.CharField(max_length=65, min_length=8, write_only=True)
**phone = serializers.CharField(max_length=20, source='username')**
class Meta:
model = User
fields = ['phone', 'password']
I tried doing this but then it add on another field with the name phone instead of replacing username. I even don't know whether it will work or not .
Ok. Somehow i found a way to do it. I created a model serializer an used an custom token generator method
https://django-rest-framework-simplejwt.readthedocs.io/en/latest/creating_tokens_manually.html
serializers.py
# for creating token manually
def get_tokens_for_user(user):
refresh = RefreshToken.for_user(user)
return {
'refresh': str(refresh),
'access': str(refresh.access_token),
}
class LoginSerializer(serializers.ModelSerializer):
password = serializers.CharField(max_length=65, min_length=8, write_only=True)
phone = serializers.CharField(max_length=20, source='username')
tokens = serializers.SerializerMethodField(read_only=True)
def get_tokens(self, obj):
return obj['tokens']
class Meta:
model = User
fields = ['phone', 'password', 'tokens']
def validate(self, attrs):
param_phone = attrs.get('username', "")
param_password=attrs.get('password', '')
if not param_phone.isnumeric():
raise serializers.ValidationError(
{'phone': ("Only Phone Number is accepted")}
)
if not len(param_phone) == 10:
raise serializers.ValidationError(
{'phone': ("Phone Number length can only be of 10 digits")}
)
user = auth.authenticate(username=param_phone, password=param_password)
if User.objects.filter(username=param_phone, is_active=False).exists():
raise AuthenticationFailed("Account disabled. Contact admin")
if not user:
raise AuthenticationFailed("Invalid Credential. Try again")
tokens = get_tokens_for_user(user)
return {
'username':param_phone,
'tokens': tokens
}
return super().validate(attrs)
views.py
class LoginView(GenericAPIView):
serializer_class = LoginSerializer
def post(self, request):
serializer = LoginSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
return Response(serializer.data, status=status.HTTP_200_OK)

Storing additional information in django using absrtactuser

I have created custom Userclass in django(AbstarctUser). Everything works fine but my password is getting stored as plain text in database even after registering in admin.py. I do not have any forms.py explicitly defined.
Also I am using nested serializers following tutorial.
My code is as below
from django.contrib import admin
from .models import BasicUserInfo
from django.contrib.auth.admin import UserAdmin
class BasicUserAdmin(UserAdmin):
pass
admin.site.register(BasicUserInfo, BasicUserAdmin)
Edited to add Models and views
Models.py
class BasicUserInfo(AbstractUser):
email = models.EmailField(primary_key=True, unique=True, db_index=True)
class UserInfo(models.Model):
user = models.ForeignKey(BasicUserInfo, on_delete=models.CASCADE)
Views.py
serializer = AddUserSerializer(data=request.data)
if serializer.is_valid(raise_exception=ValueError):
serializer.create(validated_data=request.data)
Serializers.py
class BasicUserSerializer(serializers.ModelSerializer):
class Meta:
model = BasicUserInfo
fields = ('username', 'password', 'email')
print("hete")
def create(self, validated_data):
retval = BasicUserInfo.objects.create(**validated_data)
password = validated_data.pop('password')
self.password = make_password(password)
# self._password = password
return retval
class AddUserSerializer(serializers.ModelSerializer):
user = BasicUserSerializer(required=True)
class Meta:
model = UserInfo
fields = ('phoneNo')
def create(self, validated_data):
user_data = validated_data.pop('user')
user = BasicUserSerializer.create(BasicUserSerializer(), validated_data=user_data)
user_info, created = UserInfo.objects.update_or_create(user=user, phoneNo=validated_data.pop('phoneNo'))
return user_info
The trick is to use user.set_password(password) -> this internally triggers the password hashing mechanism: Here's the Django code that does this:
def set_password(self, raw_password):
self.password = make_password(raw_password)
self._password = raw_password
def make_password(password, salt=None, hasher='default'):
"""
Turn a plain-text password into a hash for database storage
Same as encode() but generate a new random salt. If password is None then
return a concatenation of UNUSABLE_PASSWORD_PREFIX and a random string,
which disallows logins. Additional random string reduces chances of gaining
access to staff or superuser accounts. See ticket #20079 for more info.
"""
if password is None:
return UNUSABLE_PASSWORD_PREFIX + get_random_string(UNUSABLE_PASSWORD_SUFFIX_LENGTH)
hasher = get_hasher(hasher)
salt = salt or hasher.salt()
return hasher.encode(password, salt)
So the problem is serializers.create(**validated_data) is not performing the make_password operation. The above answer works perfectly fine, except it does two things differently
- It saves the user twice (once in serailizer.create and again during `user.save())
- It does not hande everything within the serializer, part of the work is being split b/w the serializer and the view.
If you want to keep it all within the serializer, you can do the following:
class AddUserSerializer(serializers.ModelSerializer):
class Meta:
model = BasicUserInfo
def validate_password(self, value):
return make_password(value)
Update:
I've made a bunch of edits; and tried to explain why. Please read patiently, and incorporate changes as you see fit.
class BasicUserSerializer(serializers.ModelSerializer):
class Meta:
model = BasicUserInfo
fields = ('username', 'password', 'email')
def validate_password(self, value):
return make_password(value)
class AddUserSerializer(serializers.ModelSerializer):
user = BasicUserSerializer(required=True)
class Meta:
model = UserInfo
fields = ('phoneNo')
def create(self, validated_data):
user_data = validated_data.pop('user')
user_serializer = BasicUserSerializer(data=user_data)
if user_serializer.is_valid(raise_exception=True):
user = user_serializer.save()
validated_data['user'] = user
return UserInfo.objects.create(**validated_data)
You should not use like this:
serializer = AddUserSerializer(data=request.data)
if serializer.is_valid(raise_exception=ValueError):
serializer.create(validated_data=request.data)
if password in validated data
it is better to use like this:
password = request.data.pop('password', '')
if not password:
raise ValidationError('password must not be empty')
serializer = AddUserSerializer(data=request.data)
serializer.is_valid(raise_exception=ValueError):
user = serializer.create(validated_data=request.data)
user.set_password(password)
user.save()

python-social not saving response to custom model

Ive been running into a number of problem in relation to using django's custom model. This one in particular is not raising any errors. For some reason after authenticating via steam and returning to the landing page the database tables for both steamuser_user (custom user) and social_auth_usersocialauth are empty. Nothing is being saved, no errors are being displayed etc.
My custom model which is quite similar to the one on django docs official page is as follows:
from django.db import models
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import BaseUserManager
# Create your models here.
class UserManager(BaseUserManager):
def create_user(self, steamid, username, password=None):
if not steamid:
msg = 'User has no Steam ID set'
raise ValueError(msg)
if not username:
msg = 'User has no name set'
raise ValueError(msg)
user = self.model(steamid=steamid,
username=username)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, steamid, username, password):
super_user = self.create_user(steamid=steamid,
username=username,
password=password)
super_user.is_staff = True
super_user.is_admin = True
super_user.is_mod = True
super_user.save(using=self._db)
return super_user
class User(AbstractBaseUser):
steamid = models.CharField(max_length=20, unique=True)
username = models.CharField(max_length=80)
email = models.EmailField(null=True,blank=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
is_admin = models.BooleanField(default=False)
is_mod = models.BooleanField(default=False)
date_joined = models.DateTimeField(auto_now_add=True)
reputation = models.IntegerField(max_length=6, default=0)
USERNAME_FIELD = 'steamid'
objects = UserManager()
def __unicode__(self):
return self.username
def get_full_name(self):
return self.steamid
def get_short_name(self):
return self.username
The settings I've used are as follows:
SOCIAL_AUTH_USER_MODEL = 'steamuser.User'
AUTH_USER_MODEL = 'steamuser.User'
TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + (
'social.apps.django_app.context_processors.backends',
'social.apps.django_app.context_processors.login_redirect',
)
AUTHENTICATION_BACKENDS = (
'social.backends.steam.SteamOpenId',
'django.contrib.auth.backends.ModelBackend',
)
#Steam OpenAuth
SOCIAL_AUTH_STEAM_API_KEY = 'B1D7C629D093D4B72577F2F11DE4EBE2'
LOGIN_URL = '/'
SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/'
SOCIAL_AUTH_ENABLED_BACKENDS = (
'steam',
)
Any help would be appreciated!
EDIT
Backends steam.py
def get_user_details(self, response):
player = self.get_json(USER_INFO, params={
'key': self.setting('API_KEY'),
'steamids': self._user_id(response)
})
if len(player['response']['players']) > 0:
player = player['response']['players'][0]
details = {'steamid': player.get('steamid'),
'username': player.get('personaname'),
}
else:
details = {}
return details
EDIT 2
Well despite my logical reasoning, I just gave up and created a custom pipeline to create the new steam user as follows:
from django.contrib.auth import get_user_model
def create_steamuser(details, user=None, *args, **kwargs):
if user:
return {'is_new': False}
if not details:
return
try:
steam_user = get_user_model().objects.get(steamid=details['steamid'])
except steam_user.DoesNotExist:
get_user_model().objects.create_user(details['steamid'], details['username'])
return {
'is_new': True,
}
Now I still have the problem where social_user is not being created. I've set the social user model to use my new custom model but there must be something that I am missing.
python-social-auth won't be able to pass the steamid and date_joined parameters to your custom create_user() method in the manager. To make that possible you have three options:
Set =None to those parameters and set some default vaules for them
Override the default create_user pipeline and pass the extra parameters.
Add a custom pipeline function before create_user and fill details with steamid and date_joined, then define SOCIAL_AUTH_STEAM_USER_FIELDS = ('username', 'email', 'steamid', 'date_joined').

django get data using multiple objects by referring to foreign keys

I have two data models, one is User and other one is ShibUser, ShibUser associate with User by storing User table's id as its Foreign Key.
Here is my ShibUser Table:
+----+--------------+------------------+----------------+
| id | auth_user_id | shib_username | shib_user_role |
+----+--------------+------------------+----------------+
| 1 | 4 | auser#domain.edu | Student |
| 2 | 5 | buser#domain.edu | Student |
+----+--------------+------------------+----------------+
from django.db import models
from askbot.deps.django_authopenid.models import User
class ShibUser(models.Model):
auth_user = models.ForeignKey(User)
shib_username = models.CharField(max_length = 200)
shib_user_role = models.CharField(max_length = 200)
Here is my User (auth_user) table:
+----+----------------+------------+--------+
| id | username | reputation | status |
+----+----------------+------------+--------+
| 4 | aaUser | 1 | w |
| 5 | MrBUser_Cool | 1 | w |
+----+----------------+------------+--------+
Model Definition for User:
class User(models.Model):
"""
Users within the Django authentication system are represented by this
model.
Username and password are required. Other fields are optional.
"""
username = models.CharField(_('username'), max_length=30, unique=True,
help_text=_('Required. 30 characters or fewer. Letters, numbers and '
'#/./+/-/_ characters'))
first_name = models.CharField(_('first name'), max_length=30, blank=True)
last_name = models.CharField(_('last name'), max_length=30, blank=True)
email = models.EmailField(_('e-mail address'), blank=True)
password = models.CharField(_('password'), max_length=128)
is_staff = models.BooleanField(_('staff status'), default=False,
help_text=_('Designates whether the user can log into this admin '
'site.'))
is_active = models.BooleanField(_('active'), default=True,
help_text=_('Designates whether this user should be treated as '
'active. Unselect this instead of deleting accounts.'))
is_superuser = models.BooleanField(_('superuser status'), default=False,
help_text=_('Designates that this user has all permissions without '
'explicitly assigning them.'))
last_login = models.DateTimeField(_('last login'), default=timezone.now)
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
groups = models.ManyToManyField(Group, verbose_name=_('groups'),
blank=True, help_text=_('The groups this user belongs to. A user will '
'get all permissions granted to each of '
'his/her group.'))
user_permissions = models.ManyToManyField(Permission,
verbose_name=_('user permissions'), blank=True,
help_text='Specific permissions for this user.')
objects = UserManager()
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
def __unicode__(self):
return self.username
def natural_key(self):
return (self.username,)
def get_absolute_url(self):
return "/users/%s/" % urllib.quote(smart_str(self.username))
def is_anonymous(self):
"""
Always returns False. This is a way of comparing User objects to
anonymous users.
"""
return False
def is_authenticated(self):
"""
Always return True. This is a way to tell if the user has been
authenticated in templates.
"""
return True
def get_full_name(self):
"""
Returns the first_name plus the last_name, with a space in between.
"""
full_name = u'%s %s' % (self.first_name, self.last_name)
return full_name.strip()
def set_password(self, raw_password):
self.password = make_password(raw_password)
def check_password(self, raw_password):
"""
Returns a boolean of whether the raw_password was correct. Handles
hashing formats behind the scenes.
"""
def setter(raw_password):
self.set_password(raw_password)
self.save()
return check_password(raw_password, self.password, setter)
def set_unusable_password(self):
# Sets a value that will never be a valid hash
self.password = make_password(None)
def has_usable_password(self):
return is_password_usable(self.password)
def get_group_permissions(self, obj=None):
"""
Returns a list of permission strings that this user has through his/her
groups. This method queries all available auth backends. If an object
is passed in, only permissions matching this object are returned.
"""
permissions = set()
for backend in auth.get_backends():
if hasattr(backend, "get_group_permissions"):
if obj is not None:
permissions.update(backend.get_group_permissions(self,
obj))
else:
permissions.update(backend.get_group_permissions(self))
return permissions
def get_all_permissions(self, obj=None):
return _user_get_all_permissions(self, obj)
def has_perm(self, perm, obj=None):
"""
Returns True if the user has the specified permission. This method
queries all available auth backends, but returns immediately if any
backend returns True. Thus, a user who has permission from a single
auth backend is assumed to have permission in general. If an object is
provided, permissions for this specific object are checked.
"""
# Active superusers have all permissions.
if self.is_active and self.is_superuser:
return True
# Otherwise we need to check the backends.
return _user_has_perm(self, perm, obj)
def has_perms(self, perm_list, obj=None):
"""
Returns True if the user has each of the specified permissions. If
object is passed, it checks if the user has all required perms for this
object.
"""
for perm in perm_list:
if not self.has_perm(perm, obj):
return False
return True
def has_module_perms(self, app_label):
"""
Returns True if the user has any permissions in the given app label.
Uses pretty much the same logic as has_perm, above.
"""
# Active superusers have all permissions.
if self.is_active and self.is_superuser:
return True
return _user_has_module_perms(self, app_label)
def email_user(self, subject, message, from_email=None):
"""
Sends an email to this User.
"""
send_mail(subject, message, from_email, [self.email])
def get_profile(self):
"""
Returns site-specific profile for this user. Raises
SiteProfileNotAvailable if this site does not allow profiles.
"""
if not hasattr(self, '_profile_cache'):
from django.conf import settings
if not getattr(settings, 'AUTH_PROFILE_MODULE', False):
raise SiteProfileNotAvailable(
'You need to set AUTH_PROFILE_MODULE in your project '
'settings')
try:
app_label, model_name = settings.AUTH_PROFILE_MODULE.split('.')
except ValueError:
raise SiteProfileNotAvailable(
'app_label and model_name should be separated by a dot in '
'the AUTH_PROFILE_MODULE setting')
try:
model = models.get_model(app_label, model_name)
if model is None:
raise SiteProfileNotAvailable(
'Unable to load the profile model, check '
'AUTH_PROFILE_MODULE in your project settings')
self._profile_cache = model._default_manager.using(
self._state.db).get(user__id__exact=self.id)
self._profile_cache.user = self
except (ImportError, ImproperlyConfigured):
raise SiteProfileNotAvailable
return self._profile_cache
I have a form which represent the user profile and I want to show the role of the user, I have import both the objects in my form but I am struggling on how to really get the user role based on User objects username.
Here is the exact place I am trying to add this:
from askbot.shibapp.models import ShibUser
from django.contrib.auth.models import User
def __init__(self, user, *args, **kwargs):
super(EditUserForm, self).__init__(*args, **kwargs)
logging.debug('initializing the form')
shib_user_role = ShibUser.objects.get(auth_user=4)
if askbot_settings.EDITABLE_SCREEN_NAME:
self.fields['username'] = UserNameField(label=_('Screen name'))
self.fields['username'].initial = user.username
self.fields['username'].user_instance = user
self.fields['email'].initial = user.email
self.fields['realname'].initial = user.real_name
self.fields['website'].initial = user.website
self.fields['city'].initial = user.location
if askbot_settings.EDITABLE_SCREEN_NAME:
self.fields['role'].initial = "test_role" (Instead of 'test_role')
I am very new to django world.
Ok so I think you're trying to go from auth.User.username to ShibUser to do this follow the ForeignKeys backwards:
user = User.objects.get(username=username)
# for reverse relationships the foo_set is created by django enabling
# reverse relationship. You can override this by providing a related_name
shibuser = user.shibuser_set.get()
# Alternative syntax
shibuser = user.shibuser_set.all()[0]
From there you can get your ShibUser role. If More than one ShibUser can exist per User then you want to drop the index and will instead have a queryset of ShibUser objects to work with.
If only one ShibUser object can exist per User you should make this a OneToOneField instead of a foreignkey and things become simpler:
shibuser = user.shibuser
Finally you can even start from the ShibUser model and work with it:
shibuser = ShibUser.objects.get(auth_user__username=username)
# Or if you already had the User object instance
shibuser = ShibUser.objects.get(auth_user=user)
Keep in mind several exceptions can be raised around this depending on the approach: the User could not exist or the ShibUser for the given User could not exist. Perhaps more than one ShibUser could be related to a single user and therefore the .get() calls will result in a MultipleObjectsReturned exception. Your schema isn't very tight to your use case it sounds like so I would probably improve that with a OneToOneField