In Django's auth contrib module, how do I validate a token submitted from a reset password form?

617 Views Asked by At

I'm using Django 3.1 with its auth contrib module. I have an API-only application, in which I initiate a password reset using the following Django view

class ResetPasswordView(SuccessMessageMixin, PasswordResetView):
    reset_password_template_name = 'templates/users/password_reset.html'
    email_template_name = 'users/password_reset_email.html'
    subject_template_name = 'users/password_reset_subject'
    success_message = "We've emailed you instructions for setting your password, " \
                      "if an account exists with the email you entered. You should receive them shortly." \
                      " If you don't receive an email, " \
                      "please make sure you've entered the address you registered with, and check your spam folder."
    success_url = reverse_lazy('users-home')

    @method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):
        request.csrf_processing_done = True
        return super().dispatch(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        email = json.loads(request.body).get('username')
        try:
            if User.objects.get(email=email).is_active:
                form = PasswordResetForm({'email': email})
                print("form valid? %s" % form.is_valid())
                if form.is_valid():
                    request = HttpRequest()
                    request.META['SERVER_NAME'] = socket.gethostbyname('localhost') #'127.0.0.1'
                    request.META['SERVER_PORT'] = 8000
                    # calling save() sends the email
                    # check the form in the source code for the signature and defaults
                    form.save(request=request,
                        use_https=False,
                        from_email="[email protected]",
                        email_template_name='../templates/users/password_reset_email.html')
                print("email: %s " % email)
                return super(ResetPasswordView, self).post(request, *args, **kwargs)
        except Exception as e:
            print("\n\nerror ...\n\n")
            print(e)
            # this for if the email is not in the db of the system
            return super(ResetPasswordView, self).post(request, *args, **kwargs)

This generates an email in which a link appears, which looks similar to

http://127.0.0.1:8000/password-reset-confirm/Mg/bhd3nc-29fa9003c9c61c2bda5cff0a66b38bdf/

My question is, how do I submit this token (with the user's desired new password) back to the server so that the srver validates the token anad then updates the password for the user?

Edit: Stack trace per the answer given ...

Traceback (most recent call last):
  File "/Users/davea/Documents/workspace/chicommons/maps/web/venv/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/Users/davea/Documents/workspace/chicommons/maps/web/venv/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Users/davea/Documents/workspace/chicommons/maps/web/venv/lib/python3.9/site-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "/Users/davea/Documents/workspace/chicommons/maps/web/venv/lib/python3.9/site-packages/django/utils/decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "/Users/davea/Documents/workspace/chicommons/maps/web/venv/lib/python3.9/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/Users/davea/Documents/workspace/chicommons/maps/web/directory/views.py", line 395, in dispatch
    return super().dispatch(request, **data)
  File "/Users/davea/Documents/workspace/chicommons/maps/web/venv/lib/python3.9/site-packages/django/utils/decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "/Users/davea/Documents/workspace/chicommons/maps/web/venv/lib/python3.9/site-packages/django/views/decorators/debug.py", line 89, in sensitive_post_parameters_wrapper
    return view(request, *args, **kwargs)
  File "/Users/davea/Documents/workspace/chicommons/maps/web/venv/lib/python3.9/site-packages/django/utils/decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "/Users/davea/Documents/workspace/chicommons/maps/web/venv/lib/python3.9/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "/Users/davea/Documents/workspace/chicommons/maps/web/venv/lib/python3.9/site-packages/django/contrib/auth/views.py", line 260, in dispatch
    assert 'uidb64' in kwargs and 'token' in kwargs

Exception Type: AssertionError at /password-reset-complete
2

There are 2 best solutions below

0
alv2017 On

It depends on the authentication strategy implemented with your API. From the fact that you are sending a user a token, I assume that you are using token authentication. :) It means that on the API side you need to implement password reset endpoint with token authentication. Do not forget to expire/blacklist the token.

Another strategy would be to run the password reset process using Django password reset views, without messing up with the API.

6
aaron On

Reuse django.contrib.auth.views.PasswordResetConfirmView, which calls self.token_generator.check_token(self.user, session_token).

import json

from django.contrib.auth.views import INTERNAL_RESET_SESSION_TOKEN, PasswordResetConfirmView
from django.views.decorators.csrf import csrf_exempt


class ConfirmResetPasswordView(PasswordResetConfirmView):

    @csrf_exempt
    def dispatch(self, request):
        data = self.data = json.loads(request.body)
        # self.user = self.get_user(data["uidb64"])
        # self.token_generator.check_token(self.user, data['token'])
        self.request.session[INTERNAL_RESET_SESSION_TOKEN] = data.pop('token')
        data['token'] = self.reset_url_token
        return super().dispatch(request, **data)

    def get_form_kwargs(self):
        return {
            'data': self.data,
            'user': self.user,
        }

    def form_valid(self, form):
        _ = form.save()
        del self.request.session[INTERNAL_RESET_SESSION_TOKEN]
        return HttpResponse()

Sample POST request body:

{
    "uidb64": "Mg",
    "token": "bhd3nc-29fa9003c9c61c2bda5cff0a66b38bdf",
    "new_password1": "new_password",
    "new_password2": "new_password"
}