Reset User Passwords in Django

June 5, 2020, 5:11 p.m.

Django · 14 min read

Reset User Passwords in Django

Last Modified: Oct. 5, 2020, 11:27 a.m.

Django's authentication system has a wide range of built-in features and functions, including the ability to handle user permissions and passwords. User objects are at the center of this system, with the primary attributes being username, password, email, first name, and last name.

If you have created a superuser and logged in to the Django administration site, you have probably seen the ability to change your password in the admin.

But how do you implement this same password reset functionality for regular site users?

Well, Django's Authentication views help manage password resets for users without superuser status. 

In this tutorial, we will use the Django stock password reset form to allow users to send a reset password email to themselves that then links to a change password form. This allows users to reset their passwords as needed.

Let's get started.

 

NOTE: If you haven't already configured your site for users, follow  A Guide to User Registration, Login, and Logout in Django before continuing with this article. You will not be able to test the password reset if you do not have an existing user account.

 

Authentication URLs

accounts/logout/ [name='logout']
accounts/password_change/ [name='password_change']
accounts/password_change/done/ [name='password_change_done']
accounts/password_reset/ [name='password_reset']
accounts/password_reset/done/ [name='password_reset_done']
accounts/reset/<uidb64>/<token>/ [name='password_reset_confirm']
accounts/reset/done/ [name='password_reset_complete']

Django has eight URL patterns associated with the authentication views. We will only be using the last four URLs related to password reset.

 

env > mysite > mysite > urls.py

"""mysite URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/2.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include ('main.urls')),
    path('accounts/', include('django.contrib.auth.urls')),
]

But instead of listing the URLs individually, we can just list the 'accounts/' path. Add this path to your project's urls.py file, in this case, the mysite > urls.py file. 

 


 

View the built-in Django password reset forms in the browser

Now we can access any of these URLs in the browser. For example, you can go to the URL in the image. 

Default password reset template

These pages have all of the functionality we need for users to reset their password, but it says Django administration at the top and does not match the rest of the site. So let's connect the password reset forms to custom templates. 

 

But first, a quick overview of each URL and their connected templates.

 


 

The default Django password reset URLs and templates

The password_reset URL connects to the form requesting an email associated with the user, the password_reset_done presents a message saying an email was sent providing further instructions, the password_reset_confirm requests the new password, and the password_reset_complete says log in with the new password.

 


 

Make a password folder containing all of the custom templates

For each of the four URLs, we will create a custom template. For organization purposes, let’s create a new folder called password in the templates > main folder. Within it, add each of the HTML templates listed below. Be careful not to make a spelling mistake.

env > mysite > main > templates > main > (New Folder) password > (New File) password_reset.html

{% extends 'main/header.html' %}


{% block content %}

	{% load crispy_forms_tags %}          

	<!--Reset Password-->
	<div class="container p-5">
  	 	<h2 class="font-weight-bold mt-3">Reset Password</h2>
		<hr>
		<p>Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one.</p>
        <form method="POST">
            {% csrf_token %}
            {{ password_reset_form|crispy }}                    
            <button class="btn btn-primary" type="submit">Send email</button>
        </form>
  	</div> 

{% endblock %}

 

env > mysite > main > templates > main > password > (New File) password_reset_done.html

{% extends 'main/header.html' %}


{% block content %}

	<!--Password reset sent-->
	<div class="container p-5">
  	<h2 class="font-weight-bold mt-3">Password reset sent</h2>
		<hr>
		<p>We've emailed you instructions for setting your password, if an account exists with the email you entered. You should receive them shortly.<br>If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder.</p>
  </div>

{% endblock %}

 

env > mysite > main > templates > main > password > (New File) password_reset_confirm.html

{% extends "main/header.html" %}

{% block content %}

  {% load crispy_forms_tags %}  
  
  <!--Password Reset Confirm-->
    <div class="container p-5">
	    <h2 class="font-weight-bold mt-3">Password Reset Confirm</h2>
		<hr>
        <p>Please enter your new password.</p>
        <form method="POST">
            {% csrf_token %}
            {{ form|crispy }}                    
            <button class="btn btn-primary" type="submit">Reset password</button>
        </form>
    </div>

{% endblock %}

 

env > mysite > main > templates > main > password > (New File) password_reset_complete.html

{% extends "main/header.html" %}

{% block content %} 
  
  <!--Password Reset Complete-->
    <div class="container p-4">
	    <h2 class="font-weight-bold mt-3">Password reset complete</h2>
		<hr>
        <p>Your password has been set. You may go ahead and log in now.</p>                 
        <a href="/login" class="btn btn-primary">Log in</a>
    </div>

{% endblock %}

Note that all of these templates are extending the header.html that contains the Bootstrap CDN. The forms are also using django-crispy-forms, a Python package used to quickly style Django forms. 

 


 

Connect the templates to the URLs

env > mysite > mysite > urls.py

"""mysite URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/2.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.contrib.auth import views as auth_views #import this


urlpatterns = [
    path('', include ('main.urls')),
    path('admin/', admin.site.urls),
    #path('accounts/', include('django.contrib.auth.urls')),
    path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(template_name='main/password/password_reset_done.html'), name='password_reset_done'),
    path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(template_name="main/password/password_reset_confirm.html"), name='password_reset_confirm'),
    path('reset/done/', auth_views.PasswordResetCompleteView.as_view(template_name='main/password/password_reset_complete.html'), name='password_reset_complete'),      
]

Now that we have custom templates, we need to assign them to the existing URL patterns. First, let's import auth_views at the top of the page. Then we can comment out the 'accounts/' URL and add three new paths that specify each template_name. 

 


 

Make a password reset email

env > mysite > main > templates > main > password > (New FIle) password_reset_email.txt

{% autoescape off %}
Hello,

We received a request to reset the password for your account for this email address. To initiate the password reset process for your account, click the link below.

{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}

This link can only be used once. If you need to reset your password again, please visit {{ protocol }}://{{domain}} and request another reset.

If you did not make this request, you can simply ignore this email.

Sincerely,
The Website Team

{% endautoescape %}

The last file we need to add in the password folder is a text file containing the reset instructions.

 


 

Add a reset password URL

env > mysite > main > urls.py

from django.urls import path
from . import views

app_name = "main"   


urlpatterns = [
    path("", views.homepage, name="homepage"),
    ...
    path("password_reset", views.password_reset_request, name="password_reset")
]

Now, to send the email to the user, we need to add a password_reset view that connects to the template but also allows us to send an email via a view function. 

 


 

Sending the reset password email

env > mysite > main > views.py

from django.shortcuts import render, redirect
from django.core.mail import send_mail, BadHeaderError
from django.http import HttpResponse
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.models import User
from django.template.loader import render_to_string
from django.db.models.query_utils import Q
from django.utils.http import urlsafe_base64_encode
from django.contrib.auth.tokens import default_token_generator
from django.utils.encoding import force_bytes



def password_reset_request(request):
	if request.method == "POST":
		password_reset_form = PasswordResetForm(request.POST)
		if password_reset_form.is_valid():
			data = password_reset_form.cleaned_data['email']
			associated_users = User.objects.filter(Q(email=data))
			if associated_users.exists():
				for user in associated_users:
					subject = "Password Reset Requested"
					email_template_name = "main/password/password_reset_email.txt"
					c = {
					"email":user.email,
					'domain':'127.0.0.1:8000',
					'site_name': 'Website',
					"uid": urlsafe_base64_encode(force_bytes(user.pk)),
					"user": user,
					'token': default_token_generator.make_token(user),
					'protocol': 'http',
					}
					email = render_to_string(email_template_name, c)
					try:
						send_mail(subject, email, 'admin@example.com' , [user.email], fail_silently=False)
					except BadHeaderError:
						return HttpResponse('Invalid header found.')
					return redirect ("/password_reset/done/")
	password_reset_form = PasswordResetForm()
	return render(request=request, template_name="main/password/password_reset.html", context={"password_reset_form":password_reset_form})

We need to add a view function that sends an email to the user if their email connects to an existing user account.

Specify the subject of the email along with the email template we want to send. Then we can pass in all of the information needed for the email's content, such as the email, domain, uid, and token. The last two values of the dictionary are what generates the unique domain slug that allows the user to only reset their password. 

The last thing to add is the send_mail function that passes in the subject, email, from, and to addresses before redirecting the user to the /password_reset/done page.

Keep in mind this function is configured to send the reset email to your Terminal, not an actual email address.

During production, the domain, site name, protocol, and from email address will need to be changed. 

 


 

Configuring the settings to send emails

env > mysite > main > settings.py

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

But seeing how we are only testing, make sure you have added the EMAIL_BACKEND listed above to the settings file so we can send the email to the CLI.

For production, the backend is changed to an email sending service.

 


 

Test the password reset email

To access the new reset page you can just type in http://127.0.0.1:8000/password_reset/ or you can add <a href="/password_reset">Forgot password?</a> to your login page.

Either way, go to this page in your browser and type in an email address connected to an existing user account.

Custom Password reset form

 

Once you get the /password_reset/done page, open your CLI and you should see a similar message like the one below. 

Custom email template sent

Terminal

[05/Jun/2020 17:07:29] "POST /password_reset HTTP/1.1" 500 80466
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Password Reset Requested
From: admin@example.com
To: admin@example.com
Date: Sat, 06 Jun 2020 00:11:42 -0000


Hello,

We received a request to reset the password for your account for this email address. To initiate the password reset process for your account, click the link below.

http://127.0.0.1:8000/reset/<uid>/<token>/

This link can only be used once. If you need to reset your password again, please visit http://127.0.0.1:8000 and request another reset.

If you did not make this request, you can simply ignore this email.

Sincerely,
The Website Team


-------------------------------------------------------------------------------

 

You will need to copy the exact link sent to the CLI and paste it in your browser to view the change password page. You will then enter and confirm your new password.

Custom confirm password form

 

Once that is complete, you will get the password reset complete page that instructs you to log in with your new password. 

Login in with new password

 


 

Use Django Messages framework with Django Password Reset

In the example above, we chose to add a redirect to the /password_reset/done page when the password_reset form was submitted correctly, but instead, we can choose to use Django messages to notify the user that an email has been sent to their inbox.

To do this, we just have to configure the project to use the Django Messages Framework then add the message to the view function.

 

Add a Django success message

env > mysite > main > views.py

from django.shortcuts import render, redirect
from django.core.mail import send_mail, BadHeaderError
from django.http import HttpResponse
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.models import User
from django.template.loader import render_to_string
from django.db.models.query_utils import Q
from django.utils.http import urlsafe_base64_encode
from django.contrib.auth.tokens import default_token_generator
from django.utils.encoding import force_bytes
from django.contrib import messages #import messages

def homepage(request):
	return render (request=request, template_name="main/home.html")

def password_reset_request(request):
	if request.method == "POST":
		password_reset_form = PasswordResetForm(request.POST)
		if password_reset_form.is_valid():
			data = password_reset_form.cleaned_data['email']
			associated_users = User.objects.filter(Q(email=data))
			if associated_users.exists():
				for user in associated_users:
					subject = "Password Reset Requested"
					email_template_name = "main/password/password_reset_email.txt"
					c = {
					"email":user.email,
					'domain':'127.0.0.1:8000',
					'site_name': 'Website',
					"uid": urlsafe_base64_encode(force_bytes(user.pk)),
					'token': default_token_generator.make_token(user),
					'protocol': 'http',
					}
					email = render_to_string(email_template_name, c)
					try:
						send_mail(subject, email, 'admin@example.com' , [user.email], fail_silently=False)
					except BadHeaderError:

						return HttpResponse('Invalid header found.')
						
					messages.success(request, 'A message with reset password instructions has been sent to your inbox.')
					return redirect ("main:homepage")
	password_reset_form = PasswordResetForm()
	return render(request=request, template_name="main/password/password_reset.html", context={"password_reset_form":password_reset_form})

See How to use Django Messages Framework first to learn the project configurations necessary for Django messages, then add a success message before the return that now redirects to the homepage rather than password_reset/done.

Save the file.

You can then delete the password_reset_done.html and the URL pattern to that template. 

Now, when the user submits the reset password form, a message will appear instead of the template. 

Password reset with messages

 

Add a Django error message

env > mysite > main > views.py

from django.shortcuts import render, redirect
from django.core.mail import send_mail, BadHeaderError
from django.http import HttpResponse
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.models import User
from django.template.loader import render_to_string
from django.db.models.query_utils import Q
from django.utils.http import urlsafe_base64_encode
from django.contrib.auth.tokens import default_token_generator
from django.utils.encoding import force_bytes
from django.contrib import messages #import messages

def homepage(request):
	return render (request=request, template_name="main/home.html")

def password_reset_request(request):
	if request.method == "POST":
		password_reset_form = PasswordResetForm(request.POST)
		if password_reset_form.is_valid():
			data = password_reset_form.cleaned_data['email']
			associated_users = User.objects.filter(Q(email=data))
			if associated_users.exists():
				for user in associated_users:
					subject = "Password Reset Requested"
					email_template_name = "main/password/password_reset_email.txt"
					c = {
					"email":user.email,
					'domain':'127.0.0.1:8000',
					'site_name': 'Website',
					"uid": urlsafe_base64_encode(force_bytes(user.pk)),
					'token': default_token_generator.make_token(user),
					'protocol': 'http',
					}
					email = render_to_string(email_template_name, c)
					try:
						send_mail(subject, email, 'admin@example.com' , [user.email], fail_silently=False)
					except BadHeaderError:

						return HttpResponse('Invalid header found.')
						
					messages.success(request, 'A message with reset password instructions has been sent to your inbox.')
					return redirect ("main:homepage")
			messages.error(request, 'An invalid email has been entered.')
	password_reset_form = PasswordResetForm()
	return render(request=request, template_name="main/password/password_reset.html", context={"password_reset_form":password_reset_form})

You can also choose to add an error message outside of the if condition that appears if the associated user does not exist within the database.

 


 

Sending emails in production

The Django settings.py and views.py configurations above are meant for testing in development mode. They will not work once your Django app is deployed and in production.

To configure your Django app for production:

  1. Sign up for an SMTP email service
  2. Update your settings.py with the new email_backend
  3. Update your views.py password_reset_request function

 

For this tutorial we will be using AWS SES, an email service provided through Amazon Web Services 12-month free tier.

 

Sending emails with AWS Simple Email Services

Create a free AWS account at https://aws.amazon.com/free/.

After creating an account, you need to connect your Django project to your AWS account.

Visit the tutorial, Setting up AWS SES Email Backend for Production for in-depth instructions on how to complete the Django AWS SES integrations. 

Please note: AWS SES only allows emails to be sent and received by an AWS verified email address unless you upgrade your account out of the AWS SES sandbox.

 

Updates to the Django settings.py

env > mysite > main > settings.py

# EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

EMAIL_BACKEND = 'django_ses.SESBackend'
AWS_ACCESS_KEY_ID = 'YOUR-ACCESS-KEY-ID'
AWS_SECRET_ACCESS_KEY = 'YOUR-SECRET-ACCESS-KEY'
AWS_SES_REGION_NAME = 'REGION-NAME' #(ex: us-east-2)
AWS_SES_REGION_ENDPOINT ='REGION-ENDPOINT' #(ex: email.us-east-2.amazonaws.com)

After following the instructions for sending emails with AWS SES, your settings.py file should look like the setting above. 

As for the AWS SES region name, you have the following 13 regions are supported:

  • us-east-1 (US East - N. Virginia)
  • us-east-2 (US East - Ohio)
  • us-west-2 (US West - Oregon)
  • ap-south-1 (Asia Pacific - Mumbai)
  • ap-northeast-2 (Asia Pacific - Seoul)
  • ap-southeast-1 (Asia Pacific - Singapore)
  • ap-southeast-2 (Asia Pacific - Sydney)
  • ap-northeast-1 (Asia Pacific - Tokyo)
  • ca-central-1 (Canada - Central)
  • eu-central-1 (Europe - Frankfurt)
  • eu-west-1 (Europe - Ireland)
  • eu-west-2 (Europe - London)
  • sa-east-1 (South America - São Paulo)

If you do not live in one of these regions, select a region that is closest to you. 

 

 EXAMPLE - Updated the settings.py

env > mysite > main > settings.py (EXAMPLE)

# EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

EMAIL_BACKEND = 'django_ses.SESBackend'
AWS_ACCESS_KEY_ID = 'don45nogo45ouyho45hy5'
AWS_SECRET_ACCESS_KEY = 'orebgojtiopj45tpngpnerpg'
AWS_SES_REGION_NAME = 'ap-southeast-2'
AWS_SES_REGION_ENDPOINT ='email.ap-southeast-2.amazonaws.com'

For clarification, here is a more accurate example of my settings.py with (fake) AWS secret access keys and region filled in.

If my region is Asia Pacific - Sydney, my AWS region name would be ap-southeast-2 and my AWS region endpoint email.ap-southeast-2.amazonaws.com.

 

 

Updates to the Django views.py

env > mysite > main > views.py

from django.shortcuts import render, redirect
from django.core.mail import send_mail, BadHeaderError
from django.http import HttpResponse
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.models import User
from django.template.loader import render_to_string
from django.db.models.query_utils import Q
from django.utils.http import urlsafe_base64_encode
from django.contrib.auth.tokens import default_token_generator
from django.utils.encoding import force_bytes
from django.contrib import messages #import messages

def homepage(request):
	return render (request=request, template_name="main/home.html")

def password_reset_request(request):
	if request.method == "POST":
		password_reset_form = PasswordResetForm(request.POST)
		if password_reset_form.is_valid():
			data = password_reset_form.cleaned_data['email']
			associated_users = User.objects.filter(Q(email=data))
			if associated_users.exists():
				for user in associated_users:
					subject = "Password Reset Requested"
					email_template_name = "main/password/password_reset_email.txt"
					c = {
					"email":user.email,
					'domain':'your-website-name.com',
					'site_name': 'Website Name',
					"uid": urlsafe_base64_encode(force_bytes(user.pk)),
					'token': default_token_generator.make_token(user),
					'protocol': 'https',
					}
					email = render_to_string(email_template_name, c)
					try:
						send_mail(subject, email, 'AWS_verified_email_address', [user.email], fail_silently=False)
					except BadHeaderError:

						return HttpResponse('Invalid header found.')
						
					messages.success(request, 'A message with reset password instructions has been sent to your inbox.')
					return redirect ("main:homepage")
			messages.error(request, 'An invalid email has been entered.')
	password_reset_form = PasswordResetForm()
	return render(request=request, template_name="main/password/password_reset.html", context={"password_reset_form":password_reset_form})

As for the views.py, you need to change the domain listed in the dictionary from 'domain':'127.0.0.1:8000' to the actual name of your website.

Then you need to change the 'protocol' from 'http' to 'https', if you are using HTTPS.

Finally, update the send_mail function with your verified AWS SES email address. To get a verified AWS email address, you need to set up an AWS SES Email Backend.

 


 

Customizing the Django reset email template

The reset password email currently sends as a plain text email.

If you are interested in making it into an HTML email template, check out the article Using HTML Email Templates for Password Reset Emails

Reset Password email as an HTML template

 


0
Subscribe now

Subscribe to stay current on our latest articles and promos





Post a Comment
Join the community

2 Comments


Stelity Oct. 18, 2020, 11:41 p.m.

This only works for people who are verified on SES. When I send an email to someone who isn't verified on AWS SES, I get: "An error occurred (MessageRejected) when calling the SendRawEmail operation: Email address is not verified."

Jaysha replying to Stelity Oct. 19, 2020, 9:42 a.m.

All new AWS SES accounts are held in sandbox unless you request production access. The article now reflects this and links to the AWS SES sandbox instructions.