8
votes

I use standard Django view, password_reset_confirm(), to reset user's password. After user follows password reset link in the letter, he enters new password and then view redirects him to the site root:

urls.py

url(r'^password-reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
        'django.contrib.auth.views.password_reset_confirm', {
        'template_name': 'website/auth/password_reset_confirm.html',
        'post_reset_redirect': '/',
    }, name='password_reset_confirm'),

After Django redirects user, he is not authenticated. I don't want him to type password again, instead, I want to authenticate him right after he set new password.

To implement this feature, I created a delegate view. It wraps standard one and handles its output. Because standard view redirects user only if password reset succeeded, I check status code of response it returns, and if it's a redirect, retrieve user from DB again and authenticate him.

urls.py

url(r'^password-reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
        views.password_reset_confirm_delegate, {
        'template_name': 'website/auth/password_reset_confirm.html',
        'post_reset_redirect': '/',
    }, name='password_reset_confirm'),

views.py

@sensitive_post_parameters()
@never_cache
def password_reset_confirm_delegate(request, **kwargs):
    response = password_reset_confirm(request, **kwargs)
    # TODO Other way?
    if response.status_code == 302:
        try:
            uid = urlsafe_base64_decode(kwargs['uidb64'])
            user = User.objects.get(pk=uid)
        except (TypeError, ValueError, OverflowError, User.DoesNotExist):
            pass
        else:
            user = authenticate(username=user.username, passwordless=True)
            login(request, user)
    return response

backends.py

class PasswordlessAuthBackend(ModelBackend):
    """Log in to Django without providing a password.

    """
    def authenticate(self, username, passwordless=False):
        if not passwordless:
            return None
        try:
            return User.objects.get(username=username)
        except User.DoesNotExist:
            return None

    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

settings.py

AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend',
    'website.backends.PasswordlessAuthBackend'
)

Are there any better ways to do this?

2
Thank you for this. You helped me on how to get the user id decoding from the kwargs. You can get the password from the request's POST parameters, that way you don't have to implement a different authentication backend.bbrik
Can't you just pull the password out of the POST parameters? Then you wouldn't need a password-less auth backend if I'm not mistaken.David Sanders

2 Answers

8
votes

Starting Django 1.11 your can do this by using the class based view. You need to override the password_reset_confirm url to pass post_reset_login=True and success_url to PasswordResetConfirmView:

urlpatterns += [
 url(
   r'^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
    views.PasswordResetConfirmView.as_view(
      post_reset_login=True,
      success_url=reverse_lazy('studygroups_login_redirect')
    ),
    name='password_reset_confirm'
  ),
]
1
votes

Vanilla Django

Since password_reset_confirm is not class-based-view, you cant cleanly customize it in any significant way without resorting to middleware-type tricks. Therefore what you are doing seems to be the most efficient way at the moment.

If django would be been passing request to the SetPasswordForm (similar to how DRF passes request to serializers), you could of overwritten the form's save() to login the user there however as now that is also not possible.

3rd party packages

You can also look into other 3rd party libs which implement auth as class based views. From a quick google search, the most promising are: