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

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'

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 6
  • Comments: 26

Most upvoted comments

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'),

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.

@alphacentauridigital thank you. For the time being I found a way to get a different detail about the inactive account. I have stated the ans here.

https://stackoverflow.com/a/72133625/16425029

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

you guys are awesome 🔥

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’`

@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.

For a quick fix, right now you can do this. Validate after checking whether the user is active or not.

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

Thank you very much @danialbagheri and @alphacentauridigital

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.