djangodjango-rest-frameworkdjango-authenticationdjango-rest-auth

Django: invalid token for password reset after account creation


within an application, a user with an administrator role, through a DRF endpoint, is able to create new user accounts.

The need is to automatically send the password reset link to the emails of the newly created users.

I have defined an url:

    path('v1/account/register/',
         AccountCreationView.as_view(),
         name='custom_account_creation'),

the view that first of all check that user role allow the creations of new users:

class AccountCreationView(RegisterView):
    """
    Accounts Creation
    """
    serializer_class = RegisterWithMailSendSerializer

    def get_response_data(self, user):
        # print('get_response_data', user)
        self.user = user

    def create(self, request, *args, **kwargs):
        role_section = 'UsersAdmins'
        #
        rights_check = role_rights_check(
            request.user,
            role_section,
            "R",
        )
        if rights_check[0] == False:
            return Response({"error": rights_check[1]},
                            status=status.HTTP_401_UNAUTHORIZED)
        response = super().create(request, *args, **kwargs)

and a custom serializer for that views, where after validating data, save and then create the password reset link and send via email to the newly created user:

class RegisterWithMailSendSerializer(RegisterSerializer):
    def save(self, request, **kwargs):
        adapter = get_adapter()
        user = adapter.new_user(request)
        self.cleaned_data = self.get_cleaned_data()
        user = adapter.save_user(request, user, self, commit=False)
        if "password1" in self.cleaned_data:
            try:
                adapter.clean_password(self.cleaned_data['password1'],
                                       user=user)
            except DjangoValidationError as exc:
                raise serializers.ValidationError(
                    detail=serializers.as_serializer_error(exc))
        user.save()
        self.custom_signup(request, user)
        setup_user_email(request, user, [])

        pg = PasswordResetTokenGenerator()
        pg_token = pg.make_token(user)
        print('>>> pg_token', pg_token)

        frontend_site = settings.FRONTEND_APP_BASE_URL
        token_generator = kwargs.get('token_generator',
                                     default_token_generator)
        temp_key = token_generator.make_token(user)
        path = reverse(
            'password_reset_confirm',
            args=[user_pk_to_url_str(user), temp_key],
        )
        full_url = frontend_site + path
        context = {
            'current_site': frontend_site,
            'user': user,
            'password_reset_url': full_url,
            'request': request,
        }
        if app_settings.AUTHENTICATION_METHOD != app_settings.AuthenticationMethod.EMAIL:
            context['username'] = user_username(user)

        email = self.get_cleaned_data()['email']
        get_adapter(request).send_mail('password_reset_key', email, context)
        return user

in settings.py CSRF_COOKIE_SECURE isn't set and has it's default False value.

everything seems to work, the user is created and the link with uid and token is sent to the relative email BUT the token seem is invalid when the user tries to reset his password...

Printed 'pg_token' is the same founded into the sended URL.

For completeness here the custom serializer used to reset the password:

in settings.py

REST_AUTH_SERIALIZERS = {
    'PASSWORD_RESET_SERIALIZER':
    'api.serializers.serializers_auth.CustomPasswordResetSerializer',
    'TOKEN_SERIALIZER': 'api.serializers.serializers_auth.TokenSerializer',
}

serializers_auth.py

class CustomAllAuthPasswordResetForm(AllAuthPasswordResetForm):

    def save(self, request, **kwargs):
        frontend_site = settings.FRONTEND_APP_BASE_URL
        email = self.cleaned_data['email']
        token_generator = kwargs.get('token_generator',
                                     default_token_generator)
        for user in self.users:
            temp_key = token_generator.make_token(user)
            path = reverse(
                'password_reset_confirm',
                args=[user_pk_to_url_str(user), temp_key],
            )
            full_url = frontend_site + path
            context = {
                'current_site': frontend_site,
                'user': user,
                'password_reset_url': full_url,
                'request': request,
            }
            if app_settings.AUTHENTICATION_METHOD != app_settings.AuthenticationMethod.EMAIL:
                context['username'] = user_username(user)
            get_adapter(request).send_mail('password_reset_key', email,
                                           context)
        return self.cleaned_data['email']


class CustomPasswordResetSerializer(PasswordResetSerializer):
    @property
    def password_reset_form_class(self):
        return CustomAllAuthPasswordResetForm

I tried everything, including the same calls for creation and reset through Postman thinking that, for some reason, the token was invalidated by the automatic login in the DRF web interface after the user was created but I don't understand why the token is not valid.

If i try manually POST email address on /api/v1/auth/password/reset/ and then use provided uid/token on /api/v1/auth/password/reset/confirm/ the password reset works as expected.

Some experience and tips are really appreciated.


Solution

  • Solved by calling password reset endpoint with email parameter immediately after the user is created, without any custom logic or overrides:

    from rest_framework.test import APIClient
    if settings.SEND_EMAIL_PWD_CHANGE_TO_NEW_USERS == True:
                    client = APIClient()
                    client.post('/api/v1/auth/password/reset/', {'email': user.email}, format='json')
    

    And now the email with the reset link contain a valid token for the password reset.