djangorestframework-simplejwt
djangorestframework-simplejwt copied to clipboard
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'
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'),
Thank you very much @danialbagheri and @alphacentauridigital
@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 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'
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 Is CustomTokenObtainPairView being called? Can you please elaborate if you are getting errors or you are getting default error message from Simple JWT
@alphacentauridigital Yes, CustomTokenObtainPairView is being called. And yes I am still getting the default error of 401 "unauthorised" for accounts whose is_active=False.
@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
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 I am getting this error now
if not self.user.is_active:
AttributeError: 'CustomTokenObtainPairSerializer' object has no attribute 'user'
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 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