Djangorestframework-simplejwt: Different error code and error message should be raised when AuthenticationFailed on TokenObtainPairView.

Created on 2 Feb 2021  路  15Comments  路  Source: jazzband/djangorestframework-simplejwt

I have noticed when user login credentials fails on TokenObtainPairView, it still returns the same 401 error which is same when the Token is invalid. I think this is to do how the exceptions are handled with serializer.is_valid(raise_exception=True). I have fixed this for my code but I thought this simple change could benefit others too, if you want I can fix it myself and commit. Here is my simple solution:

class TokenObtainPairView(TokenViewBase):
    """
    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 = TokenObtainPairSerializer

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        try:
            serializer.is_valid(raise_exception=True)
        except when as e:
            raise InvalidUser(e.args[0])
        except TokenError as e:
            raise InvalidToken(e.args[0])

        return Response(serializer.validated_data, status=status.HTTP_200_OK)

class InvalidUser(AuthenticationFailed):
    status_code = status.HTTP_406_NOT_ACCEPTABLE
    default_detail = ("Credentials is invalid or didn't match")
    default_code = 'user_credentials_not_valid'

Most helpful comment

I managed to get it to work. Thanks, @danialbagheri and @alphacentauridigital for the help.
I will put everything in one so that in case someone needs to see the full picture.

# custom_serializers.py
from django.contrib.auth.models import update_last_login
from rest_framework_simplejwt.serializers import TokenObtainSerializer
from rest_framework_simplejwt.exceptions import AuthenticationFailed
from rest_framework import status
from rest_framework_simplejwt.settings import api_settings
from rest_framework_simplejwt.tokens import RefreshToken


class InActiveUser(AuthenticationFailed):
    status_code = status.HTTP_406_NOT_ACCEPTABLE
    default_detail = "User is not active, please confirm your email"
    default_code = 'user_is_inactive'


# noinspection PyAbstractClass
class CustomTokenObtainPairSerializer(TokenObtainSerializer):

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

    def validate(self, attrs):
        data = super().validate(attrs)
        if not self.user.is_active:
            raise InActiveUser()

        refresh = self.get_token(self.user)

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

        if api_settings.UPDATE_LAST_LOGIN:
            update_last_login(None, self.user)

        return data

# custom_authentication.py
def custom_user_authentication_rule(user):
    """
    Override the default user authentication rule for Simple JWT Token to return true if there is a user and let
    serializer check whether user is active or not to return an appropriate error
    :param user: user to be authenticated
    :return: True if user is not None
    """

    return True if user is not None else False

# views.py
from .custom_serializer import CustomTokenObtainPairSerializer, InActiveUser
from rest_framework.response import Response
from rest_framework_simplejwt.exceptions import AuthenticationFailed, InvalidToken, TokenError
from rest_framework_simplejwt.views import TokenViewBase

class CustomTokenObtainPairView(TokenViewBase):
    """
    Takes a set of user credentials and returns an access and refresh JSON web
    token pair to prove the authentication of those credentials.

    Returns HTTP 406 when user is inactive and HTTP 401 when login credentials are invalid.
    """
    serializer_class = CustomTokenObtainPairSerializer

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        try:
            serializer.is_valid(raise_exception=True)
        except AuthenticationFailed:
            raise InActiveUser()
        except TokenError:
            raise InvalidToken()

        return Response(serializer.validated_data, status=status.HTTP_200_OK)

# urls.py
 path('api/token/', CustomTokenObtainPairView.as_view(),
         name='token_obtain_pair'),
 path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
 path('api/token/verify/', TokenVerifyView.as_view(), name='token_verify'),

All 15 comments

Hey Danial,

Thanks for sharing the fix. Can you please let me know which file we need to modify for raising different error codes? I am trying to raise an error code when the user is inactive. Right now simple jwt gives a generic message "No active account found with the given credentials"

Thanks

There must be Exception of **Invalid Token** whenever user trying with the wrong token or with the Expired token.

the default Exception is very annoying it says
sequence item 0: expected str instance, dict found,

it is better, if it will return the Response or validation error for invalid Token

Hey Danial,

Thanks for sharing the fix. Can you please let me know which file we need to modify for raising different error codes? I am trying to raise an error code when the user is inactive. Right now simple jwt gives a generic message "No active account found with the given credentials"

Thanks

@alphacentauridigital If I'm not wrong, simple_jwt already checks if the user is active. There is a default function which you can override as this is added to simple_jwt after Django 1.10. the function need to return a True or False and you can check other things if you wish to override it:

def default_user_authentication_rule(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.
    return True if user is not None and user.is_active else False

If you wish to override it, you need to reference it in your settings under simple_jwt settings (below is the default and how it is referenced:

'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',

If you need more help let me know.

There must be Exception of **Invalid Token** whenever user trying with the wrong token or with the Expired token.

@deepanshu-nickelfox Yes, there is **Invalid Token** token exception, but In order to handle the error correctly for the user you need to show the relevant error. It could be scenarios where the user credentials are correct but the token is invalidated perhaps by blocking the users and etc and I think showing the relevant error is necessary. Perhaps we need show if the user is inActive too, rather than simply raise an **Invalid Token** exception.

how can i override and show *Invalid Token*, can you share me any code snippet?

@deepanshu-nickelfox Here is my solution for the issue I mentioned above.

from rest_framework.exceptions import AuthenticationFailed
from rest_framework_simplejwt.views import TokenViewBase
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError

class InvalidUser(AuthenticationFailed):
    status_code = status.HTTP_406_NOT_ACCEPTABLE
    default_detail = ('Credentials is invalid or expired')
    default_code = 'user_credentials_not_valid'

class CustomTokenObtainPairView(TokenViewBase):
    """
    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 = TokenObtainPairSerializer

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        try:
            serializer.is_valid(raise_exception=True)
        except AuthenticationFailed as e:
            raise InvalidUser(e.args[0])
        except TokenError as e:
            raise InvalidToken(e.args[0])

        return Response(serializer.validated_data, status=status.HTTP_200_OK)

Hope the above helps you.

Thanks, Danial,

I am using the default authentication class from simple JWT.

'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework_simplejwt.authentication.JWTAuthentication', ]

Still, I was getting the same error code. So I used your code and did checks on whether the user is not active.

`class MyTokenObtainPairView(TokenObtainPairView):
serializer_class = MyTokenObtainPairSerializer

def post(self, request, *args, **kwargs):
    req_data = request.data.copy()
    try:
        current_user = User.objects.get(username=req_data['email'])
    except User.DoesNotExist:
        raise AuthenticationFailed('account_doesnt_exist')
    if current_user is not None:
        if not current_user.is_active:
            #raise AuthenticationFailed('account_not_active')
            raise InactiveUser('account_not_active')
        else:
            pass
    serializer = self.get_serializer(data=request.data)
    try:
        serializer.is_valid(raise_exception=True)
    except Exception as e:
        print(e)
        raise InvalidUser(e.args[0])
    except TokenError as e:
        print(e)
        raise InvalidToken(e.args[0])
    return Response(serializer.validated_data, status=status.HTTP_200_OK)

class InvalidUser(AuthenticationFailed):
status_code = status.HTTP_406_NOT_ACCEPTABLE
default_detail = ("Credentials is invalid or didn't match")
default_code = 'user_credentials_not_valid'

class InactiveUser(AuthenticationFailed):
status_code = status.HTTP_406_NOT_ACCEPTABLE
default_detail = ("Credentials is invalid or didn't match")
default_code = 'user_inactive'`

Thank you so much Guys for your Help
@danialbagheri @alphacentauridigital

you guys are awesome :fire:

Dear @alphacentauridigital

Sorry I didn't understood your issue correctly last time. I just read your first message again and it seems that you want to change the error code not the message when the user is inactive, to do so you would have to make this change some another how. I think you will have to override the serializer rather than the view.before doing so you would have to also return True for default_user_authentication_rule method when the user is inactive so that the parent class TokenObtainSerializer doesn't return that error and you handle it yourself. once you customised the serializers you would have to use the new serializer in your view (below code is just an example and I have not tested it):

## serializers.py
from rest_framework_simplejwt.serializers import TokenObtainSerializer
from rest_framework_simplejwt.exceptions import AuthenticationFailed
from rest_framework import status

class InActiveUser(AuthenticationFailed):
          status_code = status.HTTP_406_NOT_ACCEPTABLE
          default_detail = ("User is not active, please confirm your email")
          default_code = 'user_is_inactive'

class CustomTokenObtainPairSerializer(TokenObtainSerializer):

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

    def validate(self, attrs):
        data = super().validate(attrs)
        if not self.user.active:
                raise InActiveUser()

        refresh = self.get_token(self.user)

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

        if api_settings.UPDATE_LAST_LOGIN:
            update_last_login(None, self.user)

        return data

hope the above works. I recommend you to read the source code and you will be able to understand it a lot better.

Hello Danial,

Yes. I wanted to override the error code for the Inactive user. So that frontend has better communication with the backend in case of inactive users trying to login. Thanks for the updated code.

@danialbagheri Is there any URL to override after adding the code for the view?

@anoited007 Hello, You have to put the Danial's code in serializers.py and add the following lines inside urlpatterns array in urls.py

path('v1/token/', MyTokenObtainPairView.as_view(serializer_class=MyTokenObtainPairSerializer), name='token_obtain_pair'),
path('v1/token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'),

Okay. I will give it a try. Thanks for the help @alphacentauridigital

I managed to get it to work. Thanks, @danialbagheri and @alphacentauridigital for the help.
I will put everything in one so that in case someone needs to see the full picture.

# custom_serializers.py
from django.contrib.auth.models import update_last_login
from rest_framework_simplejwt.serializers import TokenObtainSerializer
from rest_framework_simplejwt.exceptions import AuthenticationFailed
from rest_framework import status
from rest_framework_simplejwt.settings import api_settings
from rest_framework_simplejwt.tokens import RefreshToken


class InActiveUser(AuthenticationFailed):
    status_code = status.HTTP_406_NOT_ACCEPTABLE
    default_detail = "User is not active, please confirm your email"
    default_code = 'user_is_inactive'


# noinspection PyAbstractClass
class CustomTokenObtainPairSerializer(TokenObtainSerializer):

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

    def validate(self, attrs):
        data = super().validate(attrs)
        if not self.user.is_active:
            raise InActiveUser()

        refresh = self.get_token(self.user)

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

        if api_settings.UPDATE_LAST_LOGIN:
            update_last_login(None, self.user)

        return data

# custom_authentication.py
def custom_user_authentication_rule(user):
    """
    Override the default user authentication rule for Simple JWT Token to return true if there is a user and let
    serializer check whether user is active or not to return an appropriate error
    :param user: user to be authenticated
    :return: True if user is not None
    """

    return True if user is not None else False

# views.py
from .custom_serializer import CustomTokenObtainPairSerializer, InActiveUser
from rest_framework.response import Response
from rest_framework_simplejwt.exceptions import AuthenticationFailed, InvalidToken, TokenError
from rest_framework_simplejwt.views import TokenViewBase

class CustomTokenObtainPairView(TokenViewBase):
    """
    Takes a set of user credentials and returns an access and refresh JSON web
    token pair to prove the authentication of those credentials.

    Returns HTTP 406 when user is inactive and HTTP 401 when login credentials are invalid.
    """
    serializer_class = CustomTokenObtainPairSerializer

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        try:
            serializer.is_valid(raise_exception=True)
        except AuthenticationFailed:
            raise InActiveUser()
        except TokenError:
            raise InvalidToken()

        return Response(serializer.validated_data, status=status.HTTP_200_OK)

# urls.py
 path('api/token/', CustomTokenObtainPairView.as_view(),
         name='token_obtain_pair'),
 path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
 path('api/token/verify/', TokenVerifyView.as_view(), name='token_verify'),
Was this page helpful?
0 / 5 - 0 ratings