Thursday, October 26, 2023

Completing the basic user endpoint collection

Took a week’s break from this project and finally got back to a couple of days back. To continue with the API, the next that is needed is a login end-point, a reset password end-point and a change password endpoint.

The LoginUserView is quite easy – all that is needed is to use the authenticate method from django.contrib.auth and pass it the username and password from the request:

class LoginUserView(APIView):
    '''Login user and return token'''
    serializer_class = UserSerializer

    def post(self, *args, **kwargs):
        user_obj = authenticate(
            username=self.request.data.get('username', None),
            password=self.request.data.get('password', None)
        )
        if user_obj is not None:
            user_token = RefreshToken.for_user(user_obj)
            return Response(
                data=str(user_token),
                status=status.HTTP_200_OK
            )
        else:
            return Response(
                data='Invalid username/password',
                status=status.HTTP_401_UNAUTHORIZED
            )

The frontend will then use the JWT in all subsequent requests.

The ResetPasswordView is simply a get request with the user ID that will send an email to the user with a link through which he can change his password. Here I do see some security challenges which will be addressed later.

class ResetPasswordView(APIView):
    '''Send password reset link to user email'''

    def get(self, *args, **kwargs):
        user_id = self.kwargs['user_id']
        try:
            user_obj = User.objects.get(id=user_id)
            send_password_reset_email(user_obj)
        except Exception as e:
            return Response(
                data=str(e),
                status=status.HTTP_400_BAD_REQUEST
            )
        return Response(
            status=status.HTTP_200_OK
        )

The password reset email will merely have a link to /api/user/change-password with a new token for the user. Similar to the verification link, this token will expire in 15 minutes.

The next is the ChangePasswordView. In this case, we are performing an update but only with respect to the password. The username cannot change. For this reason, using the UserSerializer or the RegisterUserSerializer is not possible as these have the username as a field. For this reason, a ChangePasswordSerializer has been defined:

class ChangePasswordSerializer(RegisterUserSerializer):
    '''Serializer for resetting the password of a user'''

    def update(self, instance, validated_data):
        '''Update user password'''
        if validated_data.get('password') is None:
            raise serializers.ValidationError('New password missing')
        if not instance.is_active:
            raise serializers.ValidationError('User not found')
        instance.set_password(validated_data.get('password'))
        instance.save()
        return instance

    class Meta(RegisterUserSerializer.Meta):
        model = User
        fields = ['password', 'confirm_password']

The ChangePasswordSerializer can inherit the RegisterUserSerializer as that performs the validation that the password and confirm_password fields are the same. The only difference is that the username field is removed from the fields attribute in the Meta class.

class ChangePasswordView(APIView):
    '''Change a user password'''

    serializer_class = ChangePasswordSerializer

    def post(self, *args, **kwargs):
        verification_token = self.kwargs['token']
        if not verification_token:
            return Response(
                data='Missing token',
                status=status.HTTP_400_BAD_REQUEST
            )
        token_data = TokenRefreshSerializer(
            data={'refresh': verification_token}
        )
        try:
            # Check for expired token
            if not token_data.is_valid():
                return token_error_response(token_data)
            else:
                user_obj = User.objects.get_user_by_token(
                     verification_token
                )
                user_form = ChangePasswordSerializer(
                    user_obj,
                    data=self.request.data
                )
                # Check for password match
                if not user_form.is_valid():
                    return serializer_error_response(user_form)
                else:
                    user_form.save()
        except Exception as e:
            return Response(
                data=str(e),
                status=status.HTTP_400_BAD_REQUEST
            )
        return Response(status=status.HTTP_200_OK)

These endpoints have also been tested for successful as well as failed responses. The only other endpoint that remains with respect to basic user account handling is a logout functionality. Because JWT is being used, this is not very easy as JWTs are stateless with no entry being written into the database, and therefore no token can be deleted from the database. The other option is to blacklist a JWT when a user requests a log out and if any more requests are made with the JWT, these requests will be declined as long as the JWT has not expired. However, it might be easier to just implement a logout functionality only on the frontend. This still implies someone can make API requests using a JWT independent of a browser or mobile app. This might be worth investigating later, as from a security perspective, once a user logs out, it is advisable to clean out the session and not allow any more activity on the user account.

No comments:

Post a Comment