I'm using python-social-auth (not django-social-auth because it's depreciated) for authentication in Django REST backend applciation with Custom User Model described below.
from django.contrib.auth.models import AbstractBaseUser, UserManager
class User(AbstractBaseUser):
class Gender():
MALE = 0
FEMALE = 1
UNKNOWN = 2
CHOICES = [(MALE, 'Male'), (FEMALE, 'Female'), (UNKNOWN, 'Unknown')]
username = models.CharField(_('username'), max_length=30, unique=True,
help_text=_('Required. 30 characters or fewer. Letters, digits and '
'#/./+/-/_ only.'),
validators=[
validators.RegexValidator(r'^[\w.#+-]+$', _('Enter a valid username.'), 'invalid')
])
first_name = models.CharField(max_length=30, blank=True)
last_name = models.CharField(max_length=30, blank=True)
email = models.EmailField(blank=True)
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(_('active'), default=True)
date_joined = models.DateTimeField(default=timezone.now)
gender = models.IntegerField(choices=Gender.CHOICES, default=Gender.UNKNOWN)
birthday = models.DateField(default=timezone.now)
facebook_id = models.CharField(max_length=30, blank=True)
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email']
objects = UserManager()
def __unicode__(self):
return self.username
def save(self, *args, **kwargs):
""" ensure instance has usable password when created """
if not self.pk and self.has_usable_password() is False:
self.set_password(self.password)
super(User, self).save(*args, **kwargs)
Notice that I don't implement a custom UserManager. Social auth pipline is also straightforward.
AUTHENTICATION_BACKENDS = (
'social.backends.facebook.FacebookOAuth2',
'django.contrib.auth.backends.ModelBackend',
)
SOCIAL_AUTH_PIPELINE = (
'social.pipeline.social_auth.social_details',
'social.pipeline.social_auth.social_uid',
'social.pipeline.social_auth.auth_allowed',
'social.pipeline.social_auth.social_user',
'social.pipeline.user.get_username',
'social.pipeline.social_auth.associate_by_email',
'social.pipeline.user.create_user',
'social.pipeline.social_auth.associate_user',
'social.pipeline.social_auth.load_extra_data',
'social.pipeline.user.user_details'
)
However, when I try to authenticate with Facebook, it gives an error as below
TypeError at /api-token/login/facebook/
'is_superuser' is an invalid keyword argument for this function
The problem is, probably, python-social-auth try to use django's own User instead of custom User Model that I defined.
In django-social-auth there is a setting's parameter like SOCIAL_AUTH_USER_MODEL but I couldn't find any way to do it in python-social-auth
How can I make it possible to use my custom user model in python-social-auth?
There has to be a Custom UserManager as well described below. I removed is_superuser=is_superuser field while creating new user.
class UserManager(BaseUserManager):
def _create_user(self, username, email, password,
is_staff, is_superuser, **extra_fields):
"""
Creates and saves a User with the given username, email and password.
"""
now = timezone.now()
if not username:
raise ValueError('The given username must be set')
email = self.normalize_email(email)
user = self.model(username=username, email=email,
is_staff=is_staff, is_active=True, last_login=now,
date_joined=now, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, username, email=None, password=None, **extra_fields):
return self._create_user(username, email, password, False, False,
**extra_fields)
def create_superuser(self, username, email, password, **extra_fields):
return self._create_user(username, email, password, True, True,
**extra_fields)
Related
After implementing Simple JWT I can login using username and password but User needs to authenticate only with password. I tried to override the TokenObtainPairSerializer like this
class CustomTokenPairSerializer(TokenObtainPairSerializer):
username = serializers.SerializerMethodField(required=False)
def validate(self, data):
try:
user = User.objects.filter(password=data.get("password"))
except User.DoesNotExist:
raise serializers.ValidationError("No such user exists")
credentials = {
"password": data.get("password")
}
if user:
user = user.first()
credentials["password"] = user.username
return super().validate(credentials)
and this is the view.
class CutomObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenPairSerializer
but it still requires username. Can anyone help with this?
This is the User model
class User(AbstractBaseUser):
password = models.CharField('password', max_length=4, validators=[MinLengthValidator(4)], unique=True)
name = models.CharField(max_length=50, blank=True)
surname = models.CharField(max_length=100, blank=True)
shift = models.CharField(max_length=50, choices=SHIFTS)
role = models.CharField(max_length=50, choices=ROLES)
work_start_time = models.DateTimeField()
work_end_time = models.DateTimeField()
The detail description of the registration system and the bug are described below
The registration system works as follows:
Registration by email and password (the user is not created immediately), if the data is valid, an email is sent to the mail with the generated token, which is embedded in the link. The user clicks on the link, and we use the token to confirm that this user with this email is really the one he is trying to impersonate. Then the user is created with his set of characteristics. The problem is that after the user has given his email and password in order for us to send him an email, it is saved by his unique email as a user, without specifying the user (we have several categories of users). And in the case when the validity period of the token ends, the user's email is saved in the database as unique, and the user cannot repeat the request.
The common description of the bag:
The user registers on the site, verification comes to his email (which expires in 1 hour, for example), how to process such a task so that the user can pass verification in a day or two
class RegisterView(generics.GenericAPIView):
"""Register View
This class is made to do responses on
requests connected with a registration.
This view gets a request according to the mask:
{
"email": "user#example.com",
"password": "string"
}
Then, this view configures access token and sends it
on given email into the message body - link.
Then this token should be passed to other view -
VerifyEmail (rel url localhost/auth/email-verify/)
"""
serializer_class = RegisterSerializer
def post(self, request):
user = request.data
serializer = self.serializer_class(data=user)
serializer.is_valid(raise_exception=True)
serializer.save()
user_data = serializer.data
user_from_db = User.objects.get(email=user_data['email'])
token = RefreshToken.for_user(user_from_db).access_token
current_site = get_current_site(request)
relative_link = reverse('email-verify')
abs_url = 'http://' + str(current_site) + str(relative_link) + '?token=' + str(token)
email_body = 'Hi! ' \
+ user_from_db.email \
+ ' ! Use link below to verify email \n' \
+ abs_url
data_to_send = {
'domain': current_site,
'email_subject': 'Verify your email',
'email_body': email_body,
'to_address': user_from_db.email
}
Util.send_email(data_to_send)
return Response(user_data, status=status.HTTP_201_CREATED)
class VerifyEmail(views.APIView):
"""Verify Email View
This view checks if the given token is valid, and make a mark
in the db with current user - if token is valid -
the user is verified, if not - sends a response with a
short problem description.
"""
serializer_class = EmailVerificationSerializer
token_param_config = openapi.Parameter(
'token',
in_=openapi.IN_QUERY,
description='Insert token from email',
type=openapi.TYPE_STRING
)
#swagger_auto_schema(manual_parameters=[token_param_config])
def get(self, request):
token = request.GET.get('token')
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
user = User.objects.get(id=payload['user_id'])
if not user.is_verified:
user.is_verified = True
user.save()
return Response({'email': 'Successfully activated'}, status=status.HTTP_200_OK)
except jwt.ExpiredSignatureError:
return Response({
'error': 'Activation link expired'},
status=status.HTTP_400_BAD_REQUEST
)
except jwt.exceptions.DecodeError:
return Response({
'error': 'Invalid token'},
status=status.HTTP_400_BAD_REQUEST
)
class UserManager(BaseUserManager):
"""
This class is needed to determine class 'Manager' of custom users.
To inherit from BaseUserManager, we get a lot of the same code, which
was used by Django to create 'User'.
"""
def create_user(self, email, password=None):
""" Creates and returns a user with email, pswrd and nickname"""
if email is None:
raise TypeError('Users must have an email. / '
'У пользователя должет быть email')
user = self.model(
email=self.normalize_email(email)
)
user.set_password(password)
user.save()
return user
def create_superuser(self, email, password):
"""Creates and returns a user with super-admin permissions"""
if password is None:
raise TypeError('Superusers must have a password.')
user = self.create_user(email, password)
user.is_superuser = True
user.is_staff = True
user.save()
return user
class User(AbstractBaseUser, PermissionsMixin):
"""Model User - base model
This model is made to be inherited by other models.
So, this model has a number of main parameters to use
them in child classes
"""
# email is used to connect with users
email = models.EmailField(db_index=True, unique=True)
# is active - the way not to remove information.
# when a user will want to delete his info from
# the site, we will suggest just deactivate his profile
is_active = models.BooleanField(default=True)
# # Этот флаг определяет, кто может войти в административную часть нашего
# # сайта. Для большинства пользователей это флаг будет ложным.
is_staff = models.BooleanField(default=False)
is_verified = models.BooleanField(default=False)
# the point of user creation
created_at = models.DateTimeField(auto_now_add=True)
# when the profile was last time updated
updated_at = models.DateTimeField(auto_now=True)
# USERNAME_FIELD and REQUIRED_FIELDS is required parameters in
# current schema, when we use Manager
# USERNAME_FIELD shows what the field will be used
# to log in the system
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
# object connects to the manager class
objects = UserManager()
def __str__(self):
return self.email
def tokens(self):
refresh_token = RefreshToken.for_user(self)
return {
'refresh': str(refresh_token),
'access': str(refresh_token.access_token)
}
class CustomUser(models.Model):
""" Custom User Model """
user = models.OneToOneField(User, on_delete=models.CASCADE)
first_name = models.CharField(max_length=50, blank=False)
last_name = models.CharField(max_length=50, blank=False)
terms_is_accepted = models.BooleanField(default=False, blank=False)
phone_number = models.CharField(max_length=13, blank=False)
telegram_nick = models.CharField(max_length=20, blank=True)
linkedin = models.URLField(max_length=100, blank=False)
profile_photo = models.ImageField(upload_to="photos/%Y/%m/%d/", blank=True, null=True)
date_of_birth = models.DateField(blank=True, null=True)
location = models.ManyToManyField(Location, blank=True)
credo = models.TextField(max_length=1000, blank=True)
specialization = models.CharField(max_length=150, choices=SPECIALIZATIONS, blank=False)
hard_skills = models.CharField(max_length=1000, blank=True)
soft_skills = models.CharField(max_length=1000, blank=True)
language = models.CharField(max_length=100, choices=LANGUAGE, blank=True)
work_experience = models.ManyToManyField(Experience, blank=True)
education = models.ManyToManyField(Education, blank=True)
additional_education = models.CharField(max_length=500, blank=True)
other_info = models.TextField(blank=True)
portfolio_link = models.URLField(blank=True, null=True)
def __str__(self):
return str(self.user) if self.user else ''
After creating a superuser, for some reason, I was unable to log into the Django database. I think somehow the user I created is not accepted as a staff member account, though I set is_staff, True by default. By the way, creating a custom super user like that turned out to be a headache, lots of errors occurred and I am unable to get answers in the forums.
from asyncio.windows_events import NULL
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
class CustomUserManager(BaseUserManager,):
def create_superuser(self, email, user_name, password, about='Hi it is me', **other_fields):
other_fields.setdefault('is_staff', True)
other_fields.setdefault('is_superuser', True)
other_fields.setdefault('is_active', True)
if other_fields.get('is_staff') is not True:
raise ValueError('super user must be assigned to is_staff= True')
if other_fields.get('is_superuser') is not True:
raise ValueError(
'Super user must be assigned to is_super_user=True')
return self.create_user(email, user_name=user_name, password=password, about='Hi it is me', **other_fields)
def create_user(self, email, user_name, password, about=None, **other_fields):
if not email:
raise ValueError(('You must provide an email address'))
# for example ignore the case sensitivity
email = self.normalize_email(email)
user = self.model(email=email, user_name=user_name, about=about,)
user.set_password(password)
user.save()
return user
class NewUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(max_length=255, unique=True)
user_name = models.CharField(max_length=32, unique=True)
about = models.CharField(max_length=512, blank=True, null=True)
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=False)
objects = CustomUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['user_name']
I created a form where logged-in users can change their email address. After the new email is saved there seems to be no matching query for the email/password combination. I checked the database entry for the specific user instance and the email was changed as expected. Even when I used django.contrib.auth.authenticate to match email/password it couldn't match. What am I doing wrong? Below is the form and view I used:
Forms.py
class UpdateEmailForm(forms.ModelForm):
password = forms.CharField(widget=forms.PasswordInput())
new_email = forms.EmailField()
class Meta:
model = User
fields = ['new_email', 'password']
def clean(self):
cleaned_data = self.cleaned_data
old_password = cleaned_data.get('password')
if not check_password(old_password, self.instance.password):
raise forms.ValidationError('Incorrect password. Please try again.')
return cleaned_data
Views.py
def user_login(request):
form = LoginForm(request.POST)
context = {'form': form}
if request.POST.get('user_login'):
email = request.POST.get('email')
password = request.POST.get('password')
user = authenticate(email=email, password=password)
if user is not None:
login(request, user)
return redirect('profile')
else:
messages.error(request, 'Email and Password does not match.')
return redirect('login')
return render(request, 'users/login.html', context)
Below is where I handle the new email form data
#login_required
def profile(request):
...
elif request.POST.get('update_email'):
data = Customer.objects.get(email=request.user.email)
email_form = UpdateEmailForm(request.POST, instance=request.user)
if email_form.is_valid():
email_form.save()
email = request.POST.get('new_email')
user = User.objects.get(email=request.user.email)
user.email = email
data.email = email
data.save()
user.save()
messages.success(request, 'Your account has been updated successfully')
return redirect('profile')
else:
return render(request, 'users/profile.html', {'data': data, 'email_form': email_form})
I fixed the problem by creating a new field named confirm_password rather than using the actual field password. The password is changed or not saved when I save the form using the actual password field
Class UpdateEmailForm(forms.Forms.ModelForm):
...
confirm_password = models.CharField(widget=forms.PasswordInput())
class Meta:
fields = ['confirm_password', 'new_email']
I am new to Django and need to use an email address as a username for my app - plus add some custom fields. So I am following the full example in django docs to make a custom user model.
I am very worried about the part in the example that says:
This example illustrates how most of the components work together,
but is not intended to be copied directly into projects for production
use.
I am not sure what else is needed in order to make the user model production ready.
Is the example missing some crucial security features? (I would like my custom model and authentication to be as good as the default Django one.)
Does anyone have an example that is production ready?
In the end, what I did was follow the full example in the django website, and compare my code with the default Django user model. I believe the default Django user model is production ready, and really, the only difference from that and the custom user model is the use of email as username, so I feel quite reassured that my custom user model is production ready.
Here is my code:
in models.py
from django.contrib.auth.models import (
BaseUserManager, AbstractBaseUser
)
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.core.mail import send_mail
class MyUserManager(BaseUserManager):
def create_user(self, email, password=None):
"""
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_superuser(self, email, password):
"""
Creates and saves a superuser with the given email and password.
"""
user = self.create_user(email,
password=password
)
user.is_admin = True
user.save(using=self._db)
return user
class MyUser(AbstractBaseUser):
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(_('email address'), max_length=254, blank=False, unique=True, db_index=True)
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_admin = models.BooleanField(default=False)
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
gender_choices = (
('M', 'Male'),
('F', 'Female'),
)
gender = models.CharField(max_length=1, choices=gender_choices, blank=True)
date_of_birth = models.DateField(null=True, blank=True)
city = models.CharField(max_length=50, blank=True)
phone_number = models.CharField(max_length=15, blank=True)
description = models.TextField(blank=True)
work = models.TextField(blank=True)
objects = MyUserManager()
USERNAME_FIELD = 'email'
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
def get_full_name(self):
full_name = '%s %s' % (self.first_name, self.last_name)
return full_name.strip()
def get_short_name(self):
return self.first_name
def email_user(self, subject, message, from_email=None):
"""
Sends an email to this User.
"""
send_mail(subject, message, from_email, [self.email])
def __unicode__(self):
return self.first_name + ' ' + self.last_name
def has_perm(self, perm, obj=None):
return True
def has_module_perms(self, app_label):
return True
#property
def is_staff(self):
return self.is_admin
Also in my forms.py and views.py I used the User model, so I added the following:
from django.contrib.auth import get_user_model
User = get_user_model()