[Fixed]-Django REST-Auth Password Reset

31👍

Luckily, I found a nice library which made my life so easy today:

https://github.com/anx-ckreuzberger/django-rest-passwordreset

pip install django-rest-passwordreset

Got it working like this:

  1. Followed instructions on their website.

My accounts/urls.py now has the following paths:

# project/accounts/urls.py
from django.urls import path, include
from . import views as acc_views

app_name = 'accounts'
urlpatterns = [
    path('', acc_views.UserListView.as_view(), name='user-list'),
    path('login/', acc_views.UserLoginView.as_view(), name='login'),
    path('logout/', acc_views.UserLogoutView.as_view(), name='logout'),
    path('register/', acc_views.CustomRegisterView.as_view(), name='register'),
    # NEW: custom verify-token view which is not included in django-rest-passwordreset
    path('reset-password/verify-token/', acc_views.CustomPasswordTokenVerificationView.as_view(), name='password_reset_verify_token'),
    # NEW: The django-rest-passwordreset urls to request a token and confirm pw-reset
    path('reset-password/', include('django_rest_passwordreset.urls', namespace='password_reset')),
    path('<int:pk>/', acc_views.UserDetailView.as_view(), name='user-detail')
]

Then I also added a little TokenSerializer for my CustomTokenVerification:

# project/accounts/serializers.py
from rest_framework import serializers

class CustomTokenSerializer(serializers.Serializer):
    token = serializers.CharField()

Then I added a Signal Receiver in the previous derived CustomPasswordResetView, which now is no longer derived from rest_auth.views.PasswordResetView AND added a new view CustomPasswordTokenVerificationView:

# project/accounts/views.py
from django.dispatch import receiver
from django_rest_passwordreset.signals import reset_password_token_created
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from vuedj.constants import site_url, site_full_name, site_shortcut_name
from rest_framework.views import APIView
from rest_framework import parsers, renderers, status
from rest_framework.response import Response
from .serializers import CustomTokenSerializer
from django_rest_passwordreset.models import ResetPasswordToken
from django_rest_passwordreset.views import get_password_reset_token_expiry_time
from django.utils import timezone
from datetime import timedelta

class CustomPasswordResetView:
    @receiver(reset_password_token_created)
    def password_reset_token_created(sender, reset_password_token, *args, **kwargs):
        """
          Handles password reset tokens
          When a token is created, an e-mail needs to be sent to the user
        """
        # send an e-mail to the user
        context = {
            'current_user': reset_password_token.user,
            'username': reset_password_token.user.username,
            'email': reset_password_token.user.email,
            'reset_password_url': "{}/password-reset/{}".format(site_url, reset_password_token.key),
            'site_name': site_shortcut_name,
            'site_domain': site_url
        }

        # render email text
        email_html_message = render_to_string('email/user_reset_password.html', context)
        email_plaintext_message = render_to_string('email/user_reset_password.txt', context)

        msg = EmailMultiAlternatives(
            # title:
            "Password Reset for {}".format(site_full_name),
            # message:
            email_plaintext_message,
            # from:
            "noreply@{}".format(site_url),
            # to:
            [reset_password_token.user.email]
        )
        msg.attach_alternative(email_html_message, "text/html")
        msg.send()


class CustomPasswordTokenVerificationView(APIView):
    """
      An Api View which provides a method to verifiy that a given pw-reset token is valid before actually confirming the
      reset.
    """
    throttle_classes = ()
    permission_classes = ()
    parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
    renderer_classes = (renderers.JSONRenderer,)
    serializer_class = CustomTokenSerializer

    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data)
        serializer.is_valid(raise_exception=True)
        token = serializer.validated_data['token']

        # get token validation time
        password_reset_token_validation_time = get_password_reset_token_expiry_time()

        # find token
        reset_password_token = ResetPasswordToken.objects.filter(key=token).first()

        if reset_password_token is None:
            return Response({'status': 'invalid'}, status=status.HTTP_404_NOT_FOUND)

        # check expiry date
        expiry_date = reset_password_token.created_at + timedelta(hours=password_reset_token_validation_time)

        if timezone.now() > expiry_date:
            # delete expired token
            reset_password_token.delete()
            return Response({'status': 'expired'}, status=status.HTTP_404_NOT_FOUND)

        # check if user has password to change
        if not reset_password_token.user.has_usable_password():
            return Response({'status': 'irrelevant'})

        return Response({'status': 'OK'})

Now my frontend will provide an option to request the pw-reset link, so the frontend will send a post request to django like this:

// urls.js
const SERVER_URL = 'http://localhost:8000/' // FIXME: change at production (https and correct IP and port)
const API_URL = 'api/v1/'
const API_AUTH = 'auth/'
API_AUTH_PASSWORD_RESET = API_AUTH + 'reset-password/'


// api.js
import axios from 'axios'
import urls from './urls'

axios.defaults.baseURL = urls.SERVER_URL + urls.API_URL
axios.defaults.headers.post['Content-Type'] = 'application/json'
axios.defaults.xsrfHeaderName = 'X-CSRFToken'
axios.defaults.xsrfCookieName = 'csrftoken'

const api = {
    get,
    post,
    patch,
    put,
    head,
    delete: _delete
}

function post (url, request) {
    return axios.post(url, request)
        .then((response) => Promise.resolve(response))
        .catch((error) => Promise.reject(error))
}


// user.service.js
import api from '@/_api/api'
import urls from '@/_api/urls'

api.post(`${urls.API_AUTH_PASSWORD_RESET}`, email)
    .then( /* handle success */ )
    .catch( /* handle error */ )

And the created email will contain a link like this:

Click the link below to reset your password.

localhost:8000/password-reset/4873759c229f17a94546a63eb7c3d482e73983495fa40c7ec2a3d9ca1adcf017

… which is not defined in the django-urls by intention!
Django will let every unknown url pass through and the vue router will decide if the url makes sense or not.
Then I let the frontend send the token to see if it is valid, so the user can already see if the token is already used, expired, or whatever…

// urls.js
const API_AUTH_PASSWORD_RESET_VERIFY_TOKEN = API_AUTH + 'reset-password/verify-token/'

// users.service.js
api.post(`${urls.API_AUTH_PASSWORD_RESET_VERIFY_TOKEN}`, pwResetToken)
    .then( /* handle success */ )
    .catch( /* handle error */ )

Now the user will get an error message through Vue, or password-input fields, where they can finally reset the password, which will be sent by the frontend like this:

// urls.js
const API_AUTH_PASSWORD_RESET_CONFIRM = API_AUTH + 'reset-password/confirm/'

// users.service.js
api.post(`${urls.API_AUTH_PASSWORD_RESET_CONFIRM}`, {
    token: state[token], // (vuex state)
    password: state[password] // (vuex state)
})
.then( /* handle success */ )
.catch( /* handle error */ )

This is the main code. I used custom vue routes to decouple the django rest-endpoints from the frontend visible routes. The rest is done with api requests and handling their responses.

Hope this helps anybody who will have struggles like me in the future.

0👍

We have the same setup and I can tell you that it works but I can’t help you with the base 36 except that even the Django documentation says that it is base 64!

However, you’ve written that this theoretical part is not so important for you and let’s find the point you are missing. The setup is a bit confusing because you don’t need everything of allauth. I don’t understand exactly where you are stuck. Therefore, I want to tell you how I did it:

I defined the password reset URL just for Django/allauth to find it when creating the link in the email:

from django.views.generic import TemplateView

PASSWORD_RESET = (
    r'^auth/password-reset-confirmation/'
    r'(?P<uidb64>[0-9A-Za-z_\-]+)/'
    r'(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})$'
)

urlpatterns += [
    re_path(
        PASSWORD_RESET,
        TemplateView.as_view(),
        name='password_reset_confirm',
    ),
]

You don’t have to do that (because you include('allauth.urls'), you actually don’t need these URLs) but I want to make clear that this URL does not point to the backend! That said, let your frontend serve this URL with a form to enter a new password and then use axios or something to POST uid, token, new_password1 and new_password2 to your endpoint.

In your case the endpoint is

path(
    'reset-password-confirm/',
    acc_views.CustomPasswordResetConfirmView.as_view(),
    name='reset-password-confirm'
),

Does this help you? Otherwise, please let me know.

👤yofee

Leave a comment