iMerica / dj-rest-auth

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

Reverse for 'password_reset_confirm' not found when ACCOUNT_EMAIL_VERIFICATION = "mandatory" #494

Open SaadatAliKlasra opened 1 year ago

SaadatAliKlasra commented 1 year ago

I'm using Email instead of a username to log in to my website. I'm using ACCOUNT_EMAIL_VERIFICATION = "mandatory" from django-allauth. I've also the following URL pattern as suggested in the docs. from dj_rest_auth.registration.views import VerifyEmailView path('dj-rest-auth/account-confirm-email/', VerifyEmailView.as_view(), name='account_email_verification_sent') but still getting this error.

gitexel commented 1 year ago

This issue is addressed in the documentation FAQ.

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.

I hope this can help.

ahmedesmail07 commented 1 year ago

This issue is addressed in the documentation FAQ.

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.

I hope this can help.

it is not working also

sowinski commented 1 year ago

Did you solve the problem?

Can someone explain how password_reset_confirm should look like in the urls.py? (Is this view not allready implemented in django-allauth somewhere? Do we have to implement in on our own?)

samueldy commented 1 year ago

I think I got it working by copying the following path from urls.py in the demo app:

# this url is used to generate email content
re_path(
    r"^password-reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[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",
),

Note that in that file, there are two similarly-named URLs defined. One is called password_reset_confirm (with underscores), while the other is called password-reset-confirm (with dashes). You want the one with underscores.

The password_reset_confirm.html template in this TemplateView doesn't appear to be important for REST operations--here it looks just to be a custom template for the demo app showing a password reset form that asks you to paste in the token you got in the email. But it's probably not necessary, and this TemplateView appears to exist mostly as a way to construct the password reset URL that will be sent in the email.

Edit: The password_reset_confirm view serves to both (1) generate the password reset link that you get in the email given a user ID and unique token and (2) allow for custom handling of the request when the user clicks on that link. In the demo app, it just shows a password reset form, but you can also replace the TemplateView with a redirect if you want to handle the password reset form on a different frontend. There's also the REST_AUTH["PASSWORD_RESET_USE_SITES_DOMAIN"] option you can set in settings.py to use your site's domain (when using django.contrib.site and the SITE_ID setting) as the base of this password reset URL, so all you'd have to do is set up a matching URL pattern on your frontend router.

When you POST to the /dj-rest-auth/password/reset/ endpoint and also have allauth as an installed app in settings.py, Django eventually loads the AllAuthPasswordResetForm form. The .save method of this form reverses the password_reset_confirm URL to construct the URL that will be sent in the password reset email, like this:

path = reverse(
    'password_reset_confirm',
    args=[user_pk_to_url_str(user), temp_key],
)

That's why it needs to have a path with name password_reset_confirm defined in your app's urls.py.

dperetti commented 8 months ago

This is definitely a bug. The error is here When using allauth, the default view to use should be allauth's "account_reset_password_from_key" view.

Here is a workaround.

# settings.py
REST_AUTH = {
    ...
    # Fix dj-rest-auth weird issue https://github.com/iMerica/dj-rest-auth/issues/494
    'PASSWORD_RESET_SERIALIZER': 'path.to.MyPasswordResetSerializer',
}

def my_reset_password_url_generator(request, user, temp_key):
    """
    Same code as dj_rest_auth.forms.default_url_generator but we reverse to 'account_reset_password_from_key' (the allauth view)
    instead of 'password_reset_confirm' (undefined view, see https://github.com/iMerica/dj-rest-auth/issues/494)
    """
    path = reverse(
        'account_reset_password_from_key',  # see in allauth/account/urls.py
        args=[user_pk_to_url_str(user), temp_key],
    )

    if api_settings.PASSWORD_RESET_USE_SITES_DOMAIN:
        url = build_absolute_uri(None, path)
    else:
        url = build_absolute_uri(request, path)

    url = url.replace('%3F', '?')

    return url

class MyPasswordResetSerializer(PasswordResetSerializer):
    """
    Fix dj-rest-auth issue https://github.com/iMerica/dj-rest-auth/issues/494
    """

    def get_email_options(self):
        """This hook thankfully gives us chance to use our url generator."""
        return {
            'url_generator': my_reset_password_url_generator
        }
benedwards44 commented 7 months ago

I do think the current solution proposed in the docs isn't a great solution... It looks like it's copying a similar implementation from django-allauth with how it generates the activate_url for the new user email verification process. But my view is that django-allauth assumes the Django app is serving all URLs whereas dj-rest-auth should really assume there's a separate frontend serving views...

For what it's worth, my approach using the recommends "urls" approach from the docs, taken from the demo app for reference:

# urls.py
from dj_rest_auth.views import PasswordResetConfirmView
from django.urls import re_path

urlpatterns = [
    # This URL is never actually accessed, and could be replaced with a redirect to the frontend using
    # Sites Domain. It's purely used by dj-rest-auth to build an absolute URL to inject into the email
    # that goes to the user. This path is actually accessed by the frontend.
    # The view being used (PasswordResetConfrimView) is never accessed and might be better served putting an actual redirect view here to the frontend in case someone did ever inadvertently access this URL
    re_path(r'^auth/password/reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,32})/$',
        PasswordResetConfirmView.as_view(),
        name='password_reset_confirm' # this path name is what dj-rest-auth is using to construct the URL
    ),
    ... other paths
]

Then in settings.py:

REST_AUTH = {
    ... other settings
    'PASSWORD_RESET_USE_SITES_DOMAIN': True,
}

I then use Sites Domain to actually redirect the user to my frontend application.

This does seem like a bit of a hack/workaround, as the sole purpose of the URL in urls.py is just to generate a URL to inject into the email for the user to direct to. In my scenario I don't actually want or need Django serving any HTML templates (which I assume most don't considering the common use cases of dj-rest-auth.

I think a better way would be to simply provide uid and token as variables as context to the email template (which they already are anyway), and let the developer construct their preferred URL/path in the email template. And then not even bother with trying to build password_reset_url within forms.py and just let the developer manage that in the email template.

Example would then just be:

# templates/account/email/password_reset_key_message.txt

Hey user, click here:
{{ current_site.domain }}/auth/password/reset/confirm/{{ uid }}/{{ token }}/
suskidee commented 2 months ago

from dj_rest_auth.views import PasswordResetConfirmView from django.urls import path, include, re_path from django.views.generic import TemplateView

urlpatterns = [ path('', include("dj_rest_auth.urls")), path('registration/', include('dj_rest_auth.registration.urls')), repath( r"^password-reset/confirm/(?P[0-9A-Za-z-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,32})/$", PasswordResetConfirmView.as_view(), name="password_reset_confirm", ), ]

this does all the magic