Django for Beginners /

Chapter 12: Password Change and Reset

Course Contents

In this chapter, we will complete the authorization flow of our Newspaper app by adding password change and password reset functionality. We'll implement Django's built-in views and URLs for password changes and resets before customizing them with our own Bootstrap-powered templates.

Password Change

Many websites allow users to change their passwords, and Django provides a default implementation that already works at this stage. To try it out, click the "Log In" button to ensure you're logged in. Then, navigate to the "Password change" page at http://127.0.0.1:8000/accounts/password_change/.

Password Change

Enter your old password and a new one. Then click the "Change My Password" button, and you will be redirected to the "Password change successful" page.

Password Change Done

Customizing Password Change

Let's customize these two password change pages to match the look and feel of our Newspaper site. Because Django already has created the views and URLs for us, we only need to change the templates; however, we must use the names password_change_form.html and password_change_done.html. In your text editor, create two new template files in the registration directory:

  • templates/registration/password_change_form.html
  • templates/registration/password_change_done.html

Update password_change_form.html with the following code. At the top, we extend base.html, load crispy forms, and set our page meta title, which appears in the tab of a web browser but not on the visible webpage itself. The form uses POST since we send data, a csrf_token for security reasons, and {{ form|crispy }} to use crispy forms styling. As a final tweak, we include a submit button that uses Bootstrap's btn btn-success styling to make it green.

<!-- templates/registration/password_change_form.html -->
{% extends "base.html" %}
{% load crispy_forms_tags %}

{% block title %}Password Change{% endblock title %}

{% block content %}
<h1>Password change</h1>
<p>Please enter your old password, for security's sake, and then enter
your new password twice so we can verify you typed it in correctly.</p>

<form method="POST">{% csrf_token %}
  {{ form|crispy }}
  <input class="btn btn-success" type="submit"
    value="Change my password">
</form>
{% endblock content %}

Load the page at http://127.0.0.1:8000/accounts/password_change/) to see our changes.

New Password Change Form

Next up is the password_change_done template. It also extends base.html and includes a new meta title; however, there's no form on the page, just new text.

<!-- templates/registration/password_change_done.html -->
{% extends "base.html" %}

{% block title %}Password Change Successful{% endblock title %}

{% block content %}
<h1>Password change successful</h1>
<p>Your password was changed.</p>
{% endblock content %}

This updated page is at:

http://127.0.0.1:8000/accounts/password_change/done/

New Password Change Done

That wasn't too bad, right? Certainly, it was much less work than creating everything from scratch, especially all the code around securely updating a user's password. Next up is the password reset functionality.

Password Reset

Password reset handles the typical case of users forgetting their passwords. The steps are very similar to configuring password change, as we just did. Django already provides a default implementation that we will use, and then we will customize the templates to match the look and feel of the rest of our site.

The only configuration required is telling Django how to send emails. After all, a user can only reset a password if they can access the email linked to the account. For testing purposes, we can rely on Django's console backend setting, which outputs the email text to our command-line console instead.

Add the following one-line change at the bottom of the django_project/settings.py file.

# django_project/settings.py
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"  # new

And we're all set! Django will take care of all the rest for us. Let's try it out. Navigate to http://127.0.0.1:8000/accounts/password_reset/ to view the default password reset page.

Default Password Reset Page

Make sure the email address you enter matches one of your existing user accounts. Recall that testuser does not have a linked email account, so you should use testuser2, which, if you follow my example, has an email address of [email protected]. Upon submission, you'll then be redirected to the password reset done page at:

http://127.0.0.1:8000/accounts/password_reset/done/

Default Password Reset Page Done

This page says to check our email. Since we've told Django to send emails to the command line console, the email text will now be there. In my console, I see the following:

Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit
Subject: Password reset on 127.0.0.1:8000
From: webmaster@localhost
To: [email protected]
Date: Fri, 28 Jun 2024 15:36:30 -0000
Message-ID: 
 <168235059067[email protected]>


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

Please go to the following page and choose a new password:

http://127.0.0.1:8000/accounts/reset/Mw/bn63cu-b77b6590a2e38a75c4b2af244c40297e/

Your username, in case you've forgotten: testuser2

Thanks for using our site!

The 127.0.0.1:8000 team


-------------------------------------------------------------------------------
[11/Jun/2024 15:36:30] "POST /accounts/password_reset/ HTTP/1.1" 302 0
[11/Jun/2024 15:36:30] "GET /accounts/password_reset/done/ HTTP/1.1" 200 3014

Your email text should be identical except for three lines:

  • the "To" on the sixth line contains the email address of the user
  • the URL link contains a secure token that Django randomly generates for us and can be used only once
  • the username which we're helpfully reminded of by Django

We will customize all of the default email text shortly, but for now, focus on finding the link provided and entering it into your web browser. In this example, mine is:

http://127.0.0.1:8000/accounts/reset/Mw/bn63cu-b77b6590a2e38a75c4b2af244c40297e/

You'll be redirected to the "Password reset confirmation" page.

Default Password Reset Confirmation

Enter a new password and click the "Change my password" button. This final step will redirect you to the "Password reset complete" page.

Default Password Reset Complete

To confirm everything worked, click the "Log in" link and use your new password. It should work.

Custom Templates

As with the password change pages, we can create new templates to customize the look and feel of the entire password reset flow. If you noticed, four separate templates are used. Create these new files now in your templates/registration/ directory.

  • templates/registration/password_reset_form.html
  • templates/registration/password_reset_done.html
  • templates/registration/password_reset_confirm.html
  • templates/registration/password_reset_complete.html

Start with the password reset form, which is password_reset_form.html. At the top, we extend base.html, load crispy_forms_tags, and set the meta page title. Because we used "block" titles in our base.html file, we can override them here. The form uses POST since we send data, a csrf_token for security reasons, and {{ form|crispy }} for the forms. And we again updated the submit button to green. At this point, updating these template pages should start to feel familiar.

<!-- templates/registration/password_reset_form.html -->
{% extends "base.html" %}
{% load crispy_forms_tags %}

{% block title %}Forgot Your Password?{% endblock title %}

{% block content %}
<h1>Forgot your password?</h1>
<p>Enter your email address below, and we'll email instructions
for setting a new one.</p>

<form method="POST">{% csrf_token %}
  {{ form|crispy }}
  <input class="btn btn-success" type="submit"
    value="Send me instructions!">
</form>
{% endblock content %}

Start up the server again with python manage.py runserver and navigate to:

http://127.0.0.1:8000/accounts/password_reset/

Refresh the page, and you will see our new page.

New Password Reset

Now we can update the other three pages. Each involves extending base.html, setting a new meta title, and adding new content text. When a form is involved, we switch to loading and using crispy forms.

Let's begin with the password_reset_done.html template.

<!-- templates/registration/password_reset_done.html -->
{% extends "base.html" %}

{% block title %}Email Sent{% endblock title %}

{% block content %}
<h1>Check your inbox.</h1>
<p>We've emailed you instructions for setting your password.
You should receive the email shortly!</p>
{% endblock content %}

Confirm the changes by going to http://127.0.0.1:8000/accounts/password_reset/done/.

New Reset Done

Next up is password_reset_confirm.html. Note that it has a form so we'll use crispy forms here.

<!-- templates/registration/password_reset_confirm.html -->
{% extends "base.html" %}
{% load crispy_forms_tags %}

{% block title %}Enter new password{% endblock title %}

{% block content %}
<h1>Set a new password!</h1>
<form method="POST">{% csrf_token %}
  {{ form|crispy }}
  <input class="btn btn-success" type="submit" value="Change my password">
</form>
{% endblock content %}

In the command line, grab the URL link from the custom email previously outputted to the console, and you'll see the following:

New Set Password

Finally, here is the password reset complete code:

<!-- templates/registration/password_reset_complete.html -->
{% extends "base.html" %}

{% block title %}Password reset complete{% endblock title %}

{% block content %}
<h1>Password reset complete</h1>
<p>Your new password has been set.</p>
<p>You can log in now on the
<a href="{% url 'login' %}">Log In page</a>.</p>
{% endblock content %}

You can view it at http://127.0.0.1:8000/accounts/reset/done/.

New Password Reset Complete

Try It Out

Let's confirm everything is working by resetting the password for the testuser2 account. Log out of your current account and head to the login page--the logical location for a "Forgot your password?" link that sends a user into the password reset section. Let's add that link now.

First, we'll need to add the password reset link to the existing login page since we can't assume the user will know the correct URL! That goes on the bottom of the form.

<!-- templates/registration/login.html -->
{% extends "base.html" %}
{% load crispy_forms_tags %}

{% block title %}Log In{% endblock title %}

{% block content %}
<h2>Log In</h2>
<form method="post">{% csrf_token %}
  {{ form|crispy }}
  <button class="btn btn-success ms-2" type="submit">Log In</button>
</form>
<!-- new code here -->
<p><a href="{% url 'password_reset' %}">Forgot your password?</a></p>
<!-- end new code -->
{% endblock content %}

Refresh the login webpage to confirm the new "Forgot your password?" link is there.

Forgot Your Password Link

Click on the link to bring up the password reset template and complete the flow using the email for [email protected]. Remember that the unique link will be outputted to your console. Set a new password and use it to log in to the testuser2 account. Everything should work as expected.

Git

Another chunk of work has been completed. Use Git to save our work before continuing.

(.venv) $ git status
(.venv) $ git add -A
(.venv) $ git commit -m "password change and reset"
(.venv) $ git push origin main

Conclusion

In the next chapter, we will build out our actual Newspaper app that displays articles.