iMerica / dj-rest-auth

Authentication for Django Rest Framework
https://dj-rest-auth.readthedocs.io/en/latest/index.html
MIT License
1.64k stars 306 forks source link

Add explicit password_reset_confirm docs #445

Open edmundsj opened 1 year ago

edmundsj commented 1 year ago

[WORK IN PROGRESS] I have dj-rest-auth working for account registration, login/logout, and password changing. Now I'm having some (considerable) difficulty setting up password reset via e-mail. I would like to suggest more explicit docs be created for this step, as currently they do not exist and password reset behavior (critical to most web applications) does not work by default. Additional steps appear to be required beyond those stated the FAQ. This post is my attempt to figure out what those are and document it for posterity, and to plead for help from the community as I get stuck. Please bear with me, I'm rather new to this.

How I got this working, after following the Installation Instructions

  1. Added the following lines to my settings.py (I am using decouple.config() to load environment variables)
    
    from decouple import config

EMAIL_HOST = config('EMAIL_HOST') EMAIL_HOST_USER = config('EMAIL_HOST_USERNAME') EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD') EMAIL_PORT = 587 EMAIL_USE_TLS = True DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL')

2. Verified that `django.core.mail.send_mail()` works to send emails using the above settings by attempting it through django's shell.
3. Copied `templates/base.html`, `templates/password_reset_confirm.html` and `templates/fragments/password_reset_confirm_form.html` into one of my app `templates/` folder (in my case, the app is called `users`)
4. Deleted all references to other URLs in `base.html`
5. Sent e-mail by hitting the `password/reset` endpoint, and following the link in the e-mail.
6. Copy-pasted the UID (for me "1") and the token, along with my new desired password into the required fields in the django HTML view

## Debugging Story

When I hit the `password/reset` endpoint,  I am getting the following error when trying to send a password reset email:

django.urls.exceptions.NoReverseMatch: Reverse for 'password_reset_confirm' not found. 'password_reset_confirm' is not a valid view function or pattern name.

I understand this is addressed in the [FAQ](https://dj-rest-auth.readthedocs.io/en/latest/faq.html). However, I find the instructions rather vague - "You need to add password_reset_confirm url into your `urls.py` (at the top of any other included urls). Please check the urls.py module inside demo app example for more details." 

There are several password reset confirm URLs in the specified file, several of which (I think) are already in my urlpatterns. That's why I'm able to even get this error in the first place. The only URL pattern I see which I don't *think* is already included is the following:

repath(r'^password-reset/confirm/(?P[0-9A-Za-z-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,32})/$', TemplateView.as_view(template_name="password_reset_confirm.html"), name='password_reset_confirm'),


This looks like a view that corresponds to a token that will be generated. Also, it looks like the path is not consistent with that documented on the [API endpoints](https://dj-rest-auth.readthedocs.io/en/latest/api_endpoints.html) page, which uses `password/reset/` not `password-reset`. I'm going to use the following code in my urlpatterns: 

repath(r'^api/account/password/reset/confirm/(?P[0-9A-Za-z-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,32})/$', TemplateView.as_view(template_name="password_reset_confirm.html"), name='password_reset_confirm'),


I am using `api/account/` as my base API endpoint instead of `dj-rest-auth`. When I copy-paste this line into my main project `urls.py`, and hit the `password/reset` endpoint, it sends an e-mail to the e-mail that I specified. Woohoo! I am using the SendGrid API for sending e-mails, and followed their WebAPI instructions to generate a token and verify a sender e-mail. When I click on the link in the e-mail, it sends me to the following URL: `http://localhost/api/account/password/reset/confirm/1/bdsmih-3b2ec508502bae3d46a31865d55bccb6/`

And I get the following error:

TemplateDoesNotExist at /api/account/password/reset/confirm/1/bdsmih-3b2ec508502bae3d46a31865d55bccb6/

Which is not a huge surprise to me, given that my IDE told me that it could not find the "password_reset_confirm.html" view. Looking at the example project, I see there is a `templates/` folder in the main with a `password_reset_confirm.html` template, which looks promising. This appears to require two other files: `base.html` and `fragments/password_reset_confirm_form.html`, both of which I copy-paste into my own templates directory. 

However, after copying them I'm still getting the same error, and my IDE is still yelling at me. Tried restarting django server. No dice. After instead copying them into one of my existing apps `templates/` folders and leaving it in the root of that folder, that error appears to be fixed. Now I am getting the following error: 

NoReverseMatch at /api/account/password/reset/confirm/1/bdsmih-3b2ec508502bae3d46a31865d55bccb6/ django.urls.exceptions.NoReverseMatch: Reverse for 'signup' not found. 'signup' is not a valid view function or pattern name.

It looks like in `base.html` there are a bunch of lines in a dropdown menu containing different options - email verification, resend email verification, login, password reset, etc, most of which I am not using. So I'm going to delete those lines and see if that gets me anywhere. After deleting those lines, I get the following error: 

NoReverseMatch at /api/account/password/reset/confirm/1/bdsmih-3b2ec508502bae3d46a31865d55bccb6/ Reverse for 'password-reset-confirm' not found. 'password-reset-confirm' is not a valid view function or pattern name.

I suspect this may have to do with the modified API - it's not `password-reset-confirm`, but `password/reset/confirm`. I try to hange the following line in `base.html`:
  • Password Reset Confirm
  • to this:
  • Password Reset Confirm
  • However, I still get the same error. I am just going to try to delete this line entirely and see wat happens. I also had to delete the line with a reference to `api_docs`. WOOHOO! It finally brings me to a page that looks like what I want:
    
    This has a form with fields "uid" and "token" as well as "password" and "repeat password". I manually copy-paste what I think is the UID (`bdsmih`) and the token (`3b2ec508502bae3d46a31865d55bccb6`) into the corresponding fields, along with a new password. However, now I get an error at the bottom of the page:
    

    API Response: 400 Bad Request Content: {"uid":["Invalid value"]}

    
    Now, I'm stuck again. This is a pretty uninformative error. It checks out, though - my password remains unchanged from its old value. Oops, looks like I copy-pasted the token incorrectly. After copy-pasting the token correctly, I get the following error:

    API Response: 403 Forbidden Content: {"detail":"CSRF Failed: CSRF token from POST incorrect."}

    Hm. Odd. I do all my debugging for frontend and backend in the same browser, so perhaps I have an old CSRF token? I tried manually deleting the old CSRF cookie in my browser, and clicking on the page, but I get the following error:

    API Response: 403 Forbidden Content: {"detail":"CSRF Failed: CSRF cookie not set."}

    Which indicates to me that the view is not properly sending the CSRF token. Which is weird, because the `password_reset_confirm_form.html` first line looks like this, with a very blatant CSRF token included:
    
    {% csrf_token %} ``` Reloading the page after deleting the cookie, I try again: ``` API Response: 400 Bad Request Content: {"uid":["Invalid value"]} ``` Sigh. Looking at the code in `dj_rest_auth.serializers.PasswordResetConfirmSerializer` I find the following line of code: ``` self.user = UserModel._default_manager.get(pk=uid) ``` Aha! It's looking up the user by primary key, which should be an integer. So the UID in my case is just "1". In hindsight that should have been obvious from the regex patterns in the URL. Good 'ol 20/20 hindsight. I do not fully understand why the default behavior is to have the UID and token copy-pasted from the URL when they should be passed in as parameters that the user never has to touch. That will be a separate issue ticket... Using A UID of "1" and a token of "bdsmih-3b2ec508502bae3d46a31865d55bccb6", I now get the following error: ``` API Response: 400 Bad Request Content: {"token":["Invalid value"]} ``` Well, that's weird. I'm pretty sure that's the token value. After attempting to send a second e-mail, I grabbed the new UID / token pair, and set my new password, and it worked! Not sure why that happened, perhaps the token aged out, or some of my debugging caused it to be tweaked? Regardless, it's now working. I will now try and get it working using my frontend, rather than the provided django views.