djangodjango-viewscsrfdjango-authenticationdjango-3.1

How do I turn off CSRF checks with Django auth's PasswordResetView?


I'm using Django 3.1 with Django's auth application. I have the following middleware defined

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'directory.middleware.extend_token_response.ExtendTokenResponse'
]

I would like to use Django auth's reset password functionality so I added this in my urls.py view

path('reset_password', views.ResetPasswordView.as_view(), name='password_reset'),

and in my views.py file I have defined

from django.views.decorators.csrf import csrf_exempt
...
class ResetPasswordView(SuccessMessageMixin, PasswordResetView):
    template_name = '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')

    @csrf_exempt
    def post(self, request, *args, **kwargs):
        email = request.data.get('email')
        try:
            if User.objects.get(email=email).active:
                print("email: %s " % email)
                return super(ResetPasswordView, self).post(request, *args, **kwargs)
        except:
            # this for if the email is not in the db of the system
            return super(ResetPasswordView, self).post(request, *args, **kwargs)

However, the CSRF check doesn't seem to be getting checked for, because when I submit a request like the below

curl 'http://127.0.0.1:8000/reset_password' \
  -H 'Accept: */*' \
  -H 'Accept-Language: en-US,en;q=0.9' \
  -H 'Connection: keep-alive' \
  -H 'Content-Type: application/json' \
  -H 'Origin: http://localhost:3000' \
  -H 'Referer: http://localhost:3000/' \
  -H 'Sec-Fetch-Dest: empty' \
  -H 'Sec-Fetch-Mode: cors' \
  -H 'Sec-Fetch-Site: cross-site' \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36' \
  -H 'sec-ch-ua: "Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "macOS"' \
  --data-raw '{"username":"abc@dev.com"}' \
  --compressed

I continue to get 403 responses with this message

  <p>You are seeing this message because this site requires a CSRF cookie when submitting forms. This cookie is required for security reasons, to ensure that your browser is not being hijacked by third parties.</p>
  <p>If you have configured your browser to disable cookies, please re-enable them, at least for this site, or for “same-origin” requests.</p>

Solution

  • django.contrib.auth.views.PasswordResetView decorates its dispatch method with csrf_protect, where csrf_protect = decorator_from_middleware(CsrfViewMiddleware).

    With your csrf_exempt decorator on post and the CsrfViewMiddleware from MIDDLEWARE, we have something like CsrfViewMiddleware(csrf_protect(dispatch(csrf_exempt(post)))).

    We can trick csrf_protect by setting request.csrf_processing_done = True:

    class ResetPasswordView(SuccessMessageMixin, PasswordResetView):
        ...
    
        @method_decorator(csrf_exempt)
        def dispatch(self, request, *args, **kwargs):
            request.csrf_processing_done = True
            return super().dispatch(request, *args, **kwargs)
    
        # @csrf_exempt  # Does nothing
        def post(self, request, *args, **kwargs):
            ...
    

    Alternatively, you can restore PasswordResetView.dispatch to non-csrf_protect function:

    PasswordResetView.dispatch = csrf_exempt(PasswordResetView.dispatch.__wrapped__)