djangorestframework-simplejwt icon indicating copy to clipboard operation
djangorestframework-simplejwt copied to clipboard

Different error code and error message should be raised when AuthenticationFailed on TokenObtainPairView.

Open danialbagheri opened this issue 4 years ago • 26 comments

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'

danialbagheri avatar Feb 01 '21 23:02 danialbagheri

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 avatar Feb 05 '21 15:02 alphacentauridigital

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

deepanshu-nickelfox avatar Feb 08 '21 17:02 deepanshu-nickelfox

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

deepanshu-nickelfox avatar Feb 08 '21 17:02 deepanshu-nickelfox

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.

danialbagheri avatar Feb 09 '21 09:02 danialbagheri

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.

danialbagheri avatar Feb 09 '21 09:02 danialbagheri

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

deepanshu-nickelfox avatar Feb 09 '21 12:02 deepanshu-nickelfox

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

danialbagheri avatar Feb 09 '21 22:02 danialbagheri

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

alphacentauridigital avatar Feb 10 '21 07:02 alphacentauridigital

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

you guys are awesome :fire:

deepanshu-nickelfox avatar Feb 10 '21 08:02 deepanshu-nickelfox

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.

danialbagheri avatar Feb 10 '21 23:02 danialbagheri

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.

alphacentauridigital avatar Feb 11 '21 05:02 alphacentauridigital

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

anoited007 avatar Feb 13 '21 12:02 anoited007

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

alphacentauridigital avatar Feb 13 '21 13:02 alphacentauridigital

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

anoited007 avatar Feb 13 '21 13:02 anoited007

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

anoited007 avatar Feb 13 '21 22:02 anoited007

Thank you very much @danialbagheri and @alphacentauridigital

Acel-01 avatar Mar 30 '22 14:03 Acel-01

@danialbagheri and @alphacentauridigital I tried your solutions but for me, it is not working. Maybe I did some mistake. I had the same problem, I want a different error code to be raised for inactive accounts. Below are the codes

custom serializer

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'
    print('in active has been called')


class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
    @classmethod
    def get_token(cls, user):
        token = super().get_token(user)

        # Add custom claims
        token['is_active'] = user.is_active
        token['is_teacher'] = user.is_teacher
        token['is_student'] = user.is_student
        token['is_superuser'] = user.is_superuser
        token['is_staff'] = user.is_staff

        return token
    
    def validate(self, attrs):
        data = super().validate(attrs)
        if not self.user.is_active:
            raise InActiveUser()
        token = self.get_token(self.user)
        
        data['refresh'] = str(token)
        data['access'] = str(token.access_token)
        
        if api_settings.UPDATE_LAST_LOGIN:
            update_last_login(None, self.user)
     
        return data

custom view

class CustomTokenObtainPairView(TokenObtainPairView):
    serializer_class = CustomTokenObtainPairSerializer
    
    print('token function called')
    
    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        try:
            serializer.is_valid(raise_exception=True)
        except AuthenticationFailed:
            print('in active user')
            raise InActiveUser()
        except TokenError:
            raise InvalidToken()

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


custom_authentication.py For some reason when I print the user I receive none

def custom_user_authentication_rule(user):
  
    print(f'custom authentication has been applied')
    print(f'custom authentication rule has been applied for {user}')

    return True if user is not None else False

simple jwt settings in setting.py

SIMPLE_JWT = {

    'USER_ID_CLAIM': 'user_id',
    'USER_AUTHENTICATION_RULE': 'account.custom_authentication.custom_user_authentication_rule',

}

Please help me and correct me wherever I am wrong. I too want a different error code and message for in active user accounts. I am beginner in DRF so perhaps I have done something wrong in implementing your codes.

rickbhatt avatar May 04 '22 17:05 rickbhatt

@rickbhatt Have you defined InActiveUser Class?

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'

alphacentauridigital avatar May 04 '22 17:05 alphacentauridigital

Oh yes Sorry I forgot to put it here, But I have defined this class same as you did. I shall edit the question above. I have also edited the codes to their original form removing the extra print statements

rickbhatt avatar May 04 '22 18:05 rickbhatt

@rickbhatt Is CustomTokenObtainPairView being called? Can you please elaborate if you are getting errors or you are getting default error message from Simple JWT

alphacentauridigital avatar May 04 '22 18:05 alphacentauridigital

@alphacentauridigital Yes, CustomTokenObtainPairView is being called. And yes I am still getting the default error of 401 "unauthorised" for accounts whose is_active=False.

rickbhatt avatar May 04 '22 18:05 rickbhatt

@alphacentauridigital What I understood after writing some print statements and using try and except around

data = super().validate(attrs)

is that this validation is itself throwing the "No active account found with the given credentials" for an inactive account, hence stopping the rest of the function to execute and stopping the manual handling of this error. I do not understand why this is happening. Please help me get these codes working to get a different error code for in active account

rickbhatt avatar May 04 '22 20:05 rickbhatt

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)

alphacentauridigital avatar May 04 '22 20:05 alphacentauridigital

@alphacentauridigital I am getting this error now

 if not self.user.is_active:
AttributeError: 'CustomTokenObtainPairSerializer' object has no attribute 'user'

rickbhatt avatar May 05 '22 17:05 rickbhatt

Try to do it manually by getting the user and verifying whether the user exists or is inactive before calling data = super().validate(attrs) . You can get an email/username from the POST data and get the user and check whether a user exists or is inactive.

alphacentauridigital avatar May 08 '22 07:05 alphacentauridigital

@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

rickbhatt avatar May 08 '22 07:05 rickbhatt