Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UidAndTokenSerializer validates data in a wrong order when reset_password_confirm endpoint is called #697

Open
arudzinska opened this issue Sep 29, 2022 · 0 comments

Comments

@arudzinska
Copy link

arudzinska commented Sep 29, 2022

I am trying to confirm the password reset (with retype) at the /users/reset_password_confirm/ endpoint. The error I am getting is:

Traceback (most recent call last):
# ...
File "/home/django/venv/lib/python3.10/site-packages/rest_framework/views.py", line 506, in dispatch
  response = handler(request, *args, **kwargs)
File "/home/django/venv/lib/python3.10/site-packages/djoser/views.py", line 248, in reset_password_confirm
  serializer.is_valid(raise_exception=True)
File "/home/django/venv/lib/python3.10/site-packages/rest_framework/serializers.py", line 227, in is_valid
  self._validated_data = self.run_validation(self.initial_data)
File "/home/django/venv/lib/python3.10/site-packages/rest_framework/serializers.py", line 429, in run_validation
  value = self.validate(value)
File "/home/django/venv/lib/python3.10/site-packages/djoser/serializers.py", line 169, in validate
  validated_data = super().validate(attrs)
File "/home/django/venv/lib/python3.10/site-packages/djoser/serializers.py", line 229, in validate
  attrs = super().validate(attrs)
File "/home/django/venv/lib/python3.10/site-packages/djoser/serializers.py", line 210, in validate
  user = self.context["request"].user or self.user
AttributeError: 'PasswordResetConfirmRetypeSerializer' object has no attribute 'user'

I investigated on the resolution order of validate methods called when starting with PasswordResetConfirmRetypeSerializer and apparently it's something like this:
MRO_before
The problem appears in PasswordSerializer (D) where the user is not yet attached to our serializer instance, because current UidAndTokenSerializer (B) runs its two validate steps in a wrong order (see red text in the image above). As a fix for this I decided to change the order of these two steps (see green text in the image below):
MRO_after

This is the override code I prepared:
settings.py:

DJOSER = {
    "PASSWORD_RESET_CONFIRM_RETYPE": True,
    "PASSWORD_RESET_CONFIRM_URL": "reset_password_confirm/{uid}/{token}",
    "SERIALIZERS": {
        "password_reset_confirm_retype": "my_app.users.serializers.PasswordResetConfirmRetypeSerializerOverride",
    },
    # ...
}

my_app.users.serializers.py:

from django.contrib.auth import get_user_model

from djoser import utils
from djoser.conf import settings
from djoser.serializers import (
    UserCreatePasswordRetypeSerializer, PasswordRetypeSerializer,
)
from rest_framework import serializers
from rest_framework.exceptions import ValidationError

User = get_user_model()


class UidAndTokenSerializerOverride(serializers.Serializer):
    """
    This class is a rough copy of UidAndTokenSerializer with an overridden validate 
    method. We don't want to inherit from UidAndTokenSerializer, because there's no 
    need to run its validate method with super().validate(attrs) once we perform the 
    validation logic here.
    """
    uid = serializers.CharField()
    token = serializers.CharField()

    default_error_messages = {
        "invalid_token": settings.CONSTANTS.messages.INVALID_TOKEN_ERROR,
        "invalid_uid": settings.CONSTANTS.messages.INVALID_UID_ERROR,
    }

    def _assign_user_if_valid_uid_and_token(self, uid, token):
        """
        Assigns user to self if provided uid and token are valid. Otherwise, raises
        ValidationErrors.
        """
        try:
            decoded_uid = utils.decode_uid(uid)
            self.user = User.objects.get(pk=decoded_uid)
        except (User.DoesNotExist, ValueError, TypeError, OverflowError):
            key_error = "invalid_uid"
            raise ValidationError(
                {"uid": [self.error_messages[key_error]]}, code=key_error
            )

        is_token_valid = self.context["view"].token_generator.check_token(
            self.user, token
        )
        if not is_token_valid:
            key_error = "invalid_token"
            raise ValidationError(
                {"token": [self.error_messages[key_error]]}, code=key_error
            )

    def validate(self, attrs):
        """
        Overriding this method as original UidAndTokenSerializer first runs
        super().validate(attrs) and only then assigns user to the instance based on
        credentials. This results in an error, as super().validate(attrs) requires the
        user to be already authenticated. This override switches the order of these
        two steps.
        """

        self._assign_user_if_valid_uid_and_token(
            self.initial_data.get("uid", ""), self.initial_data.get("token", "")
        )
        validated_data = super().validate(attrs)
        return validated_data


class PasswordResetConfirmRetypeSerializerOverride(
    UidAndTokenSerializerOverride,
    PasswordRetypeSerializer,
):
    pass

The app seems to be working properly now and the password is reset. Is there something wrong in how I'm using/understanding Djoser in this scenario or is it a bug?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant