Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django Rest JWT login using username or email?

I am using django-rest-jwt for authentication in my app.

By default it user username field to autenticate a user but I want let the users login using email or username.

Is there any mean supported by django-rest-jwt to accomplish this. I know the last option would be write my own login method.

like image 963
ofnowhere Avatar asked Dec 17 '15 10:12

ofnowhere


People also ask

Which authentication is best in Django REST framework?

Django-Knox is a framework that makes the authentication of the API endpoints built with the Django Rest Framework easier. However, Knox is also a token-based authentication like JSON Web Token (JWT) auth. Django-Knox comes with well-detailed documentation for easy implementation.

How JWT token works in Django REST framework?

After verifying the credentials, the server issues two JSON Web Tokens to the user. One of them is an Access Token and the other is a Refresh Token. The frontend of your application then stores the tokens securely and sends the Access Token in the Authorization header of all requests it then sends to the server.


4 Answers

No need to write a custom authentication backend or custom login method.

A Custom Serializer inheriting JSONWebTokenSerializer, renaming the 'username_field' and overriding def validate() method.

This works perfectly for 'username_or_email' and 'password' fields where the user can enter its username or email and get the JSONWebToken for correct credentials.

from rest_framework_jwt.serializers import JSONWebTokenSerializer

from django.contrib.auth import authenticate, get_user_model
from django.utils.translation import ugettext as _
from rest_framework import serializers

from rest_framework_jwt.settings import api_settings


User = get_user_model()
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER

class CustomJWTSerializer(JSONWebTokenSerializer):
    username_field = 'username_or_email'

    def validate(self, attrs):

        password = attrs.get("password")
        user_obj = User.objects.filter(email=attrs.get("username_or_email")).first() or User.objects.filter(username=attrs.get("username_or_email")).first()
        if user_obj is not None:
            credentials = {
                'username':user_obj.username,
                'password': password
            }
            if all(credentials.values()):
                user = authenticate(**credentials)
                if user:
                    if not user.is_active:
                        msg = _('User account is disabled.')
                        raise serializers.ValidationError(msg)

                    payload = jwt_payload_handler(user)

                    return {
                        'token': jwt_encode_handler(payload),
                        'user': user
                    }
                else:
                    msg = _('Unable to log in with provided credentials.')
                    raise serializers.ValidationError(msg)

            else:
                msg = _('Must include "{username_field}" and "password".')
                msg = msg.format(username_field=self.username_field)
                raise serializers.ValidationError(msg)

        else:
            msg = _('Account with this email/username does not exists')
            raise serializers.ValidationError(msg)

In urls.py:

url(r'{Your url name}$', ObtainJSONWebToken.as_view(serializer_class=CustomJWTSerializer)),
like image 114
Shikhar Thapliyal Avatar answered Nov 08 '22 12:11

Shikhar Thapliyal


Building on top of Shikhar's answer and for anyone coming here looking for a solution for rest_framework_simplejwt (since django-rest-framework-jwt seems to be dead, it's last commit was 2 years ago) like me, here's a general solution that tries to alter as little as possible the original validation from TokenObtainPairSerializer:

from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

class CustomJWTSerializer(TokenObtainPairSerializer):
    def validate(self, attrs):
        credentials = {
            'username': '',
            'password': attrs.get("password")
        }

        # This is answering the original question, but do whatever you need here. 
        # For example in my case I had to check a different model that stores more user info
        # But in the end, you should obtain the username to continue.
        user_obj = User.objects.filter(email=attrs.get("username")).first() or User.objects.filter(username=attrs.get("username")).first()
        if user_obj:
            credentials['username'] = user_obj.username

        return super().validate(credentials)

And in urls.py:

url(r'^token/$', TokenObtainPairView.as_view(serializer_class=CustomJWTSerializer)),

like image 30
ladorm Avatar answered Nov 08 '22 10:11

ladorm


Found out a workaround.

@permission_classes((permissions.AllowAny,))
def signin_jwt_wrapped(request, *args, **kwargs):
    request_data = request.data
    host = request.get_host()
    username_or_email = request_data['username']
    if isEmail(username_or_email):
        # get the username for this email by model lookup
        username = Profile.get_username_from_email(username_or_email)
        if username is None:
            response_text = {"non_field_errors":["Unable to login with provided credentials."]}
            return JSONResponse(response_text, status=status.HTTP_400_BAD_REQUEST)
    else:
        username = username_or_email

    data = {'username': username, 'password':request_data['password']}
    headers = {'content-type': 'application/json'}
    url = 'http://' + host + '/user/signin_jwt/'
    response = requests.post(url,data=dumps(data), headers=headers)

    return JSONResponse(loads(response.text), status=response.status_code)

I check that whether the text that I received is a username or an email.

If email then I lookup the username for that and then just pass that to /signin_jwt/

like image 1
ofnowhere Avatar answered Nov 08 '22 12:11

ofnowhere


authentication.py

from django.contrib.auth.models import User

class CustomAuthBackend(object):
    """
    This class does the athentication-
    using the user's email address.
    """
    def authenticate(self, request, username=None, password=None):
        try:
            user = User.objects.get(email=username)
            if user.check_password(password):
                return user
            return None
        except User.DoesNotExist:
            return None

    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

settings.py

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'app_name.authentication.CustomAuthBackend',
]

How it works:

If user try to authenticate using their username django will look at the ModelBackend class. However, if the user adds its email instead, django will try ModelBackend but will not find the logic needed, then will try the CustomAuthBackend class making it work the authentication.

like image 1
Elias Prado Avatar answered Nov 08 '22 11:11

Elias Prado



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!