4

I am using Django 2.2.14 with the configuration below for Djoser 2.1.0 but when trying to get JWT token for an inactive user, it returns the same error as using a wrong password which makes it tricky to differentiate. I get HTTP STATUS 401 with the detail below

{ "detail": "No active account found with the given credentials }

My configuration Djoser is shown below:

 'LOGIN_FIELD': 'email', 
'SEND_CONFIRMATION_EMAIL': True, 
'PASSWORD_CHANGED_EMAIL_CONFIRMATION': True, 
'USER_CREATE_PASSWORD_RETYPE': True, 
'TOKEN_MODEL': None,  
'SEND_ACTIVATION_EMAIL': True, 
"LOGOUT_ON_PASSWORD_CHANGE": False,  
"PASSWORD_RESET_SHOW_EMAIL_NOT_FOUND": True,  
"USERNAME_RESET_SHOW_EMAIL_NOT_FOUND": True,  
'PASSWORD_RESET_CONFIRM_URL': 'account/password/reset/confirm/{uid}/{token}',  
'USERNAME_RESET_CONFIRM_URL': 'account/username/reset/  /{uid}/{token}', 
'ACTIVATION_URL': 'account/activate/{uid}/{token}', 

I am also using AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.AllowAllUsersModelBackend']

Manuel
  • 81
  • 7

1 Answers1

3

After some digging, it was noticed that Djoser uses simple-jwt module to generate its JWT tokens and hence, the error messages were coming from that module instead.

With help from some of the guys who contribute to simple-jwt, I was able to modify the code as shown below to make the endpoint check if a user is inactive and send appropriate error.

# 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.
Add 'USER_AUTHENTICATION_RULE': 'path_to_custom_user_authentication_rule' to simplejwt settings to override the default.
    :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'),
Manuel
  • 81
  • 7
  • This is good, but one might also require a custom [authentication backend](https://docs.djangoproject.com/en/4.0/topics/auth/customizing/#writing-an-authentication-backend) to get this working. Also refer to this [answer](https://stackoverflow.com/a/65924318/15993687) on custom model backend – Art Dec 24 '21 at 17:30
  • The default authentication returns None if the user is not active. The reason for the above comment. – Art Dec 24 '21 at 17:39
  • I tried your answer as I had the same problem but was still getting the same 401 error. What should I do I also posted a question https://stackoverflow.com/questions/72101444/how-to-customise-the-default-user-authentication-rule-in-django-rest-framework. If you would like to ans this please – Ritankar Bhattacharjee May 04 '22 at 13:15
  • @RitankarBhattacharjee you need to set a custom authenticaton backend to get this working; else, it will return 401, as authenticate method that's used behind the scenes will return None for inactivate users. refer [cutom backends](https://docs.djangoproject.com/en/4.0/topics/auth/customizing/#writing-an-authentication-backend) or just set the authentication backends to [django.contrib.auth.backends.AllowAllUsersModelBackend](https://docs.djangoproject.com/en/4.1/ref/contrib/auth/#django.contrib.auth.models.User.is_active) – Art Aug 20 '22 at 10:14
  • @Art i actually figured out a way without writing a custom backend. I have answered my question – Ritankar Bhattacharjee Aug 20 '22 at 14:24