Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I use `email` in "django-rest-framework-simplejwt" instead of `username` to generate token?

In django-rest-framework-simplejwt plugin username and password are used by default. But I wanted to use email instead of username. So, I did like below:

In serializer:

class MyTokenObtainSerializer(Serializer):
    username_field = User.EMAIL_FIELD

    def __init__(self, *args, **kwargs):
        super(MyTokenObtainSerializer, self).__init__(*args, **kwargs)

        self.fields[self.username_field] = CharField()
        self.fields['password'] = PasswordField()

    def validate(self, attrs):
        # self.user = authenticate(**{
        #     self.username_field: attrs[self.username_field],
        #     'password': attrs['password'],
        # })
        self.user = User.objects.filter(email=attrs[self.username_field]).first()
        print(self.user)

        if not self.user:
            raise ValidationError('The user is not valid.')

        if self.user:
            if not self.user.check_password(attrs['password']):
                raise ValidationError('Incorrect credentials.')
        print(self.user)
        # Prior to Django 1.10, inactive users could be authenticated with the
        # default `ModelBackend`.  As of Django 1.10, the `ModelBackend`
        # prevents inactive users from authenticating.  App designers can still
        # allow inactive users to authenticate by opting for the new
        # `AllowAllUsersModelBackend`.  However, we explicitly prevent inactive
        # users from authenticating to enforce a reasonable policy and provide
        # sensible backwards compatibility with older Django versions.
        if self.user is None or not self.user.is_active:
            raise ValidationError('No active account found with the given credentials')

        return {}

    @classmethod
    def get_token(cls, user):
        raise NotImplemented(
            'Must implement `get_token` method for `MyTokenObtainSerializer` subclasses')


class MyTokenObtainPairSerializer(MyTokenObtainSerializer):
    @classmethod
    def get_token(cls, user):
        return RefreshToken.for_user(user)

    def validate(self, attrs):
        data = super(MyTokenObtainPairSerializer, self).validate(attrs)

        refresh = self.get_token(self.user)

        data['refresh'] = text_type(refresh)
        data['access'] = text_type(refresh.access_token)

        return data

In view:

class MyTokenObtainPairView(TokenObtainPairView):
   """
    Takes a set of user credentials and returns an access and refresh JSON web
    token pair to prove the authentication of those credentials.
   """
    serializer_class = MyTokenObtainPairSerializer

And it works!!

Now my question is, how can I do it more efficiently? Can anyone give suggestion on this? Thanks in advance.

like image 743
Md Mahfuzur Rahman Avatar asked Jan 12 '19 05:01

Md Mahfuzur Rahman


People also ask

Can we use email as a primary user identifier in Django?

So we can use that email address as the primary ‘user identifier’ instead of a username for authentication. Default Django app will give you a User Model that has a mandatory username field, and an ‘optional’ email field. However if you’re starting a new project, Django highly recommends you to set up a custom User Model.

What kind of emails can I send from Django?

For example, when you create a Google account, the first mail you get would be something like, “Hi Xyz, Welcome to Google. Your new account comes with access to Google products, apps, and services…..” Sending these types of emails from your Django application is quite easy.

What is the default user model in Django?

Default Django app will give you a User Model that has a mandatory username field, and an ‘optional’ email field. However if you’re starting a new project, Django highly recommends you to set up a custom User Model. For more info on this subkect look at official doc: Custom User Model

What is the best way to authenticate a user in Django?

Django comes with built-in authentication views but they require manual configuration including urls and templates for a complete sign up page. So while we can roll our own a better approach--and the current default for many professionals--is to use the django-allauth package instead.


Video Answer


2 Answers

This answer is for future readers and therefore contains extra information.

In order to simplify the authentication backend, you have multiple classes to hook into. I would suggest to do option 1 (and optionally option 3, a simplified version of yours) below. Couple of notes before you read on:

  • Note 1: django does not enforce email as required or being unique on user creation (you can override this, but it's off-topic)! Option 3 (your implementation) might therefore give you issues with duplicate emails.
  • Note 1b: use User.objects.filter(email__iexact=...) to match the emails in a case insensitive way.
  • Note 1c: use get_user_model() in case you replace the default user model in future, this really is a life-saver for beginners!
  • Note 2: avoid printing the user to console. You might be printing sensitive data.

As for the 3 options:

  1. Adjust django authentication backend with f.e. class EmailModelBackend(ModelBackend) and replace authenticate function.
    • Does not adjust token claims
    • Not dependent on JWT class/middleware (SimpleJWT, JWT or others)
    • Also adjusts other authentication types (Session/Cookie/non-API auth, etc.)
    • The required input parameter is still username, example below. Adjust if you dont like it, but do so with care. (Might break your imports/plugins and is not required!)
  2. Replace django authenticate(username=, password=, **kwarg) from django.contrib.auth
    • Does not adjust token claims
    • You need to replace token backend as well, since it should use a different authentication, as you did above.
    • Does not adjust other apps using authenticate(...), only replaces JWT auth (if you set it up as such) parameters is not required and therefore this option is less adviced).
  3. Implement MyTokenObtainPairSerializer with email as claim.
    • Now email is sent back as JWT data (and not id).
    • Together with option 1, your app authentication has become username agnostic.

Option 1 (note that this also allows username!!):

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q

class EmailorUsernameModelBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        UserModel = get_user_model()
        try:
            user = UserModel.objects.get(Q(username__iexact=username) | Q(email__iexact=username))
        except UserModel.DoesNotExist:
            return None
        else:
            if user.check_password(password):
                return user
        return None

Option 2: Skipped, left to reader and not adviced.

Option 3: You seem to have this covered above.

Note: you dont have to define MyTokenObtainPairView, you can use TokenObtainPairView(serializer_class=MyTokenObtainPairSerializer).as_view() in your urls.py. Small simplification which overrides the used token serializer.

Note 2: You can specify the identifying claim and the added data in your settings.py (or settings file) to use email as well. This will make your frontend app use the email for the claim as well (instead of default user.id)

SIMPLE_JWT = {
    'USER_ID_FIELD': 'id', # model property to attempt claims for
    'USER_ID_CLAIM': 'user_id', # actual keyword in token data
}

However, heed the uniqueness warnings given by the creators:

For example, specifying a "username" or "email" field would be a poor choice since an account's username or email might change depending on how account management in a given service is designed.

If you can guarantee uniqueness, you are all set.

like image 117
David Zwart Avatar answered Sep 28 '22 03:09

David Zwart


And in addition to @Mic's answer, remember to set USERNAME_FIELD = 'email' and may be REQUIRED_FIELDS = ['username'] in the User model.

like image 31
Erisan Olasheni Avatar answered Sep 28 '22 04:09

Erisan Olasheni