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

12👍

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.

7👍

Why did you copy and paste so much instead of subclassing? I got it to work with:

# serializers.py 
from rest_framework_simplejwt.serializers import TokenObtainSerializer

class EmailTokenObtainSerializer(TokenObtainSerializer):
    username_field = User.EMAIL_FIELD


class CustomTokenObtainPairSerializer(EmailTokenObtainSerializer):
    @classmethod
    def get_token(cls, user):
        return RefreshToken.for_user(user)

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

        refresh = self.get_token(self.user)

        data["refresh"] = str(refresh)
        data["access"] = str(refresh.access_token)

        return data

And

# views.py
from rest_framework_simplejwt.views import TokenObtainPairView

class EmailTokenObtainPairView(TokenObtainPairView):
    serializer_class = CustomTokenObtainPairSerializer

And of course

#urls.py
from rest_framework_simplejwt.views import TokenRefreshView
from .views import EmailTokenObtainPairView

url("token/", EmailTokenObtainPairView.as_view(), name="token_obtain_pair"),
url("refresh/", TokenRefreshView.as_view(), name="token_refresh"),

👤Mic

5👍

For those using a custom User model, you simply can add those lines:

class User(AbstractUser):
    ... 
    email = models.EmailField(verbose_name='email address', max_length=255, unique=True)
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

Then, in urls.py:

from rest_framework_simplejwt.views import TokenObtainPairView

urlpatterns = [
    ... 
    path('api/login/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
👤Marie

4👍

The question has been a while but, I add +1 for @Mic’s answer. By the way, wasn’t it sufficient to update to TokenObtainPairSerializer only as following?:

from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework_simplejwt.serializers import (
    TokenObtainPairSerializer, User
)

class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
    username_field = User.EMAIL_FIELD


class EmailTokenObtainPairView(TokenObtainPairView):
    serializer_class = CustomTokenObtainPairSerializer

3👍

Let summarize the above solutions:

1- Create two app by Django command. One for the new token and the other for the user:

python manage.py startapp m_token  # modified token
python manage.py startapp m_user  # modified user

2- In the m_token, create the serializers.py and override the serializer to replace username with email field:

# serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer, User


class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
        username_field = User.EMAIL_FIELD

3- In the m_token, override the view to replace the serializer with the new one:

# views.py
from rest_framework_simplejwt.views import TokenObtainPairView
from .serializer import CustomTokenObtainPairSerializer


class EmailTokenObtainPairView(TokenObtainPairView):
    serializer_class = CustomTokenObtainPairSerializer

4- In the m_token, create the urls.py and give the paths as follows:

# urls.py
from django.urls import path
from .views import TokenObtainPairView
from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView


urlpatterns = [
    path(r'token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path(r'token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path(r'token/verify/', TokenVerifyView.as_view(), name='token_verify'),
]

5- In the m_user, override the user model as follows:

# models.py
from django.contrib.auth.models import AbstractUser


class MUser(AbstractUser):
    USERNAME_FIELD = 'email'
    EMAIL_FIELD = 'email'
    REQUIRED_FIELDS = ['username']

6- In the django project root, add AUTH_USER_MODEL = 'm_user.MUser' to setting.py.

I tested it in my project and it worked perfectly. I hope I did not miss anything. This way the swagger also shows "email" instead of "username" in the token parameters:
enter image description here

1👍

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

1👍

Using this code you can allow users to login using either username or email in the username field. You can add some lines to validate the email.

class TokenPairSerializer(TokenObtainPairSerializer):

    def validate(self, attrs):
        raw_username = attrs["username"]
        users = User.objects.filter(email=raw_username)
        if(users):
            attrs['username'] = users.first().username
        # else:
        #     raise serializers.ValidationError("Only email is allowed!")
        data = super(TokenPairSerializer, self).validate(attrs)
        return data

Leave a comment