What to do when user forgot the password? We need to have a reset password view. User enters there her email address and email with reset link is sent. The reset link contains special information like uid and token. When user clicks the reset link, the reset password confirm view is displayed. This view takes the uid and token from the link and a new password and send POST request to the Django server to set a new password. In this post, we will create the reset password functionality with Django Rest Framework and Djoser package.

What will you learn in this post:

  • how does reset password work in the web service,
  • how to send reset password email,
  • how to set a new password with reset link,
  • how to test the above functionality.

The user interface and production email setup will be descibed in the next posts (in this post only backend code will be provided). Fill the form to be notified about future posts.

This article is a part of articles series on how to build SaaS from scratch with Django and React. I will use the code from the previous article: Django Rest Framework Email Verification.

Reset Password Flow

In the backend there are two endpoints for reset password functionality. Both endpoints are provided by the Djoser package:

  • Reset Password endpoint, that is available at /users/reset_password/ URL. The enpoint requires the email field to send an email with reset password link. It has variable PASSWORD_RESET_SHOW_EMAIL_NOT_FOUND in the settings, by default set to False. It controls what type of error returns in the case of email address in the request which doesn’t exist in the database - by default it will return HTTP_204_NO_CONTENT and doesn’t send any email. If you want different behaviour, just change this variable.
  • Reset Password Confirmation endpoint, that is available at /users/reset_password_confirm/. We need to send in POST request the uid and token from reset link to this endpoint. We also need to send a new password provided by user. If all goes well, this endpoint will return HTTP_204_NO_CONTENT.

The reset password functionality flow is presented below:

Reset Password Flow

To enable reset password functionality we need to configure Djoser in backend/server/server/settings.py file. We need to set PASSWORD_RESET_CONFIRM_URL variable that points to the URL address in the frontend with reset password confirm view.

# backend/server/server/settings.py

# ...
# configure Djoser
DJOSER = {
    "USER_ID_FIELD": "username",
    "LOGIN_FIELD": "email",
    "SEND_ACTIVATION_EMAIL": True,
    "ACTIVATION_URL": "activate/{uid}/{token}",
    "PASSWORD_RESET_CONFIRM_URL": "reset_password/{uid}/{token}", # the reset link 
    'SERIALIZERS': {
        'token_create': 'apps.accounts.serializers.CustomTokenCreateSerializer',
    },
}
# ...

The example of reset password email:

Subject: Password reset on SaaSitive

Body:

You're receiving this email because you requested a password reset for your user account at SaaSitive.

Please go to the following page and choose a new password:
http://testserver/reset_password/MQ/afnd93-8d1799decda7137bbf57c047ed33ee86
Your username, in case you've forgotten: test_user

Thanks for using our site!

The SaaSitive team

The link to reset the password:

http://testserver/reset_password/MQ/afnd93-8d1799decda7137bbf57c047ed33ee86

From our reset link:

  • uid = MQ,
  • token = afnd93-8d1799decda7137bbf57c047ed33ee86.

Don’t worry that the reset link has http://testserver - it is only for testing purposes. It will be set to the correct domain address in production.

Test reset password functionality

We are ready to write tests for reset password functionality. We will write three tests:

  • test for normal flow, the registered user with active account request the password reset,
  • test for reset password of inactive user (email not verified),
  • test for reset password with not existing email address.

Let’s add tests in backend/server/apps/accounts/tests.py file. Please add a new class PasswordResetTest (above the EmailVerificationTest class from the previous post).

# backend/server/apps/accounts/tests.py 
# ...

class PasswordResetTest(APITestCase):

    # endpoints needed
    register_url = "/api/v1/users/"
    activate_url = "/api/v1/users/activation/"
    login_url = "/api/v1/token/login/"
    send_reset_password_email_url = "/api/v1/users/reset_password/"
    confirm_reset_password_url = "/api/v1/users/reset_password_confirm/"
    
    # user infofmation
    user_data = {
        "email": "test@example.com", 
        "username": "test_user", 
        "password": "verysecret"
    }
    login_data = {
        "email": "test@example.com", 
        "password": "verysecret"
    }

    def test_reset_password(self):
        # register the new user
        response = self.client.post(self.register_url, self.user_data, format="json")
        # expected response 
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        # expected one email to be send
        self.assertEqual(len(mail.outbox), 1)
        
        # parse email to get uid and token
        email_lines = mail.outbox[0].body.splitlines()
        activation_link = [l for l in email_lines if "/activate/" in l][0]
        uid, token = activation_link.split("/")[-2:]
        
        # verify email
        data = {"uid": uid, "token": token}
        response = self.client.post(self.activate_url, data, format="json")
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

        # reset password
        data = {"email": self.user_data["email"]}
        response = self.client.post(self.send_reset_password_email_url, data, format="json")
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

        # parse reset-password email to get uid and token
        # it is a second email!
        email_lines = mail.outbox[1].body.splitlines()
        reset_link = [l for l in email_lines if "/reset_password/" in l][0]
        uid, token = activation_link.split("/")[-2:]

        # confirm reset password
        data = {"uid": uid, "token": token, "new_password": "new_verysecret"}
        response = self.client.post(self.confirm_reset_password_url, data, format="json")
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

        # login to get the authentication token with old password
        response = self.client.post(self.login_url, self.login_data, format="json")
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
        
        # login to get the authentication token with new password
        login_data = dict(self.login_data)
        login_data["password"] = "new_verysecret"
        response = self.client.post(self.login_url, login_data, format="json")
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        
# ...

Steps in the test_reset_password:

  • register a new user,
  • parse activation email,
  • activate the user,
  • request email with reset password link,
  • parse reset password email,
  • confirm reset password with uid, token and new_password,
  • try to login with old password (fails with HTTP_400_BAD_REQUEST),
  • try to login with new password (success with HTTP_200_OK).

Test the functionality for inactive user (without verified email):

# backend/server/apps/accounts/tests.py 
# ...

    def test_reset_password_inactive_user(self):
        # register the new user
        response = self.client.post(self.register_url, self.user_data, format="json")
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

        # reset password for inactive user
        data = {"email": self.user_data["email"]}
        response = self.client.post(self.send_reset_password_email_url, data, format="json")
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
        # the email wasnt send
        self.assertEqual(len(mail.outbox), 1)        
# ...

Please notice that there is HTTP_204_NO_CONTENT response but no email is sent. There is only one email in mail.outbox and it is activation email from registration step.

Test the functionality for a wrong email:

# backend/server/apps/accounts/tests.py 
# ...

    def test_reset_password_wrong_email(self):
        data = {"email": "wrong@email.com"}
        response = self.client.post(self.send_reset_password_email_url, data, format="json")
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
        # the email wasnt send
        self.assertEqual(len(mail.outbox), 0)

# ...

Similar like above the HTTP_204_NO_CONTENT is returned and no email is sent. To run our tests please use the following command:

./manage.py test apps 

The expected output:

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.......
----------------------------------------------------------------------
Ran 7 tests in 1.765s

OK
Destroying test database for alias 'default'...

We have 3 tests for reset password and 4 tests for email verification functionality.

Commit changes to the repository

That’s all. We have reset password functionality ready and tested. We need to commit changes to the repository:

git commit -am "reset password in the backend"
git push

Summary

  • We configured Djoser for reset password functionality (by setting one variable!).
  • We have two endpoints: reset password for sending email with reset link and reset password confirm to set a new password.
  • New functionality was tested with unit tests.
  • We will write user interface in React for this functionality in the next post.

The code for this tutorial is available at Github repository.


Let's stay in touch!

Would you like to be notified about new posts? Please fill this form.

Have you found a bug in the code? Please add a GitHub issue.

Do you have problems with running the code or setup and need help? Please add a StackOverflow question with django-react tag.


Boilerplate Tutorial Articles:
1. Starting SaaS with Django and React
2. React Routing and Components for Signup and Login
3. Token Based Authentication with Django Rest Framework and Djoser
4. React Token Based Authentication to Django REST API Backend
5. React Authenticated Component
6. CRUD in Django Rest Framework and React
7. Docker-Compose for Django and React with Nginx reverse-proxy and Let's encrypt certificate
8. Django Rest Framework Email Verification
9. Django Rest Framework Reset Password (↩ you are here!)
More articles coming soon!

Link to the code repository: saasitive/django-react-boilerplate