Hello everyone!
In addition to login and logout views, django's auth app has views that can let users reset their password if they forget it. Let's go ahead and see how we can add this functionality into our app.
Workflow:
1) User clicks on "forgot password" link - This will take them to a page where they will be prompted to enter their email address. This view is going to be handled by extending from the built in PasswordResetView
.
PasswordResetView
- This view displays the form where the user will submit his/her email address -> checks if a user with the provided email address exists in the database or not -> generates a password reset link that is going to be used only once -> and finally sends that link to the user's email address.
-
Note - Django doesn't throw in an error if the provided email address isn't associated with any user but won't send the email and you might be wondering why, The reason is to "prevent information leaking to potential attackers". If you don't like this feature and would rather let your users know when they have entered a correct email address and when not, you can always inherit from the
PasswordResetForm
class and customize it a bit.
So, why not just use this view instead of making our own by extending from it??
- Let's walthrough the code and talk about it below.
views.py
from django.urls import reverse_lazy
from django.contrib.auth.views import PasswordResetView
from django.contrib.messages.views import SuccessMessageMixin
class ResetPasswordView(SuccessMessageMixin, PasswordResetView):
template_name = 'users/password_reset.html'
email_template_name = 'users/password_reset_email.html'
subject_template_name = 'users/password_reset_subject'
success_message = "We've emailed you instructions for setting your password, " \
"if an account exists with the email you entered. You should receive them shortly." \
" If you don't receive an email, " \
"please make sure you've entered the address you registered with, and check your spam folder."
success_url = reverse_lazy('users-home')
-
template_name
- if not given any, django defaults to registration/password_reset_form.html to render the associated template for the view, but since our template is going to be in our users app template directory we needed to explicitly tell django. -
email_template_name
- The template used for generating the body of the email with the reset password link. -
subject_template_name
- The template used for generating the subject of the email with the reset password link. -
success_message
- The message that will be displayed upon a successful password reset request. -
success_url
- If not given any, django defaults to 'password_reset_done' after a successful password request. But I think it makes sense to just redirect the user to the home page without providing any additional template.
We haven't set up our app to send emails yet, but we will do that later.
-> Map this view to our main project's url patterns.
user_management/urls.py
from django.urls import path
from users.views import ResetPasswordView
urlpatterns = [
path('password-reset/', ResetPasswordView.as_view(), name='password_reset'),
]
-> Now let's provide the templates associated with the view. Create the following templates inside users/templates/users/ directory.
password_reset.html
{% extends "users/base.html" %}
{% block content %}
<div class="form-content my-3 p-3">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-5">
<div class="card shadow-lg border-0 rounded-lg mt-0 mb-3">
<div class="card-header justify-content-center">
<div id="error_div"></div>
<h3 class="font-weight-light my-4 text-center">Forgot Password?</h3>
</div>
{% if form.errors %}
<div class="alert alert-danger alert-dismissible" role="alert">
<div id="form_errors">
{% for key, value in form.errors.items %}
<strong>{{ value }}</strong>
{% endfor %}
</div>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
{% endif %}
<div class="card-body">
<form method="POST">
{% csrf_token %}
<div class="form-row">
<div class="col-md-10 offset-md-1">
<div class="form-group">
<label class="small mb-1" for="id_email">Email</label>
<input type="email" name="email" class="form-control"
autocomplete="email" maxlength="254" required id="id_email"
placeholder="Enter email">
</div>
</div>
</div>
<div class="form-row">
<div class="col-md-10 offset-md-1">
<div class="form-group mt-0 mb-1">
<button type="submit" class="col-md-12 btn btn-dark">Submit
</button>
</div>
</div>
</div>
</form>
</div>
<div class="card-footer text-center">
<div class="small">
<a href="{% url 'users-register' %}">Create A New Account</a><br><br>
<a href="{% url 'login' %}">Back To Login</a><br>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
password_reset_email.html
{% autoescape off %}
To initiate the password reset process for your {{ user.email }} Django Registration/Login App Account,
click the link below:
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
If clicking the link above doesn't work, please copy and paste the URL in a new browser
window instead.
Sincerely,
The Developer
{% endautoescape %}
password_reset_subject.txt
Django - Registration/Login App Password Reset
-> Modify the dead link inside login.html to point to password_reset.
login.html
<a href="{% url 'password_reset' %}"><i>Forgot Password?</i></a>
2) Okay after the password reset request is done, the user will be redirected to the home page with a message informing him/her to go and check their email address. Notice that as I've mentioned earlier, this message will be displayed even if a user requesting the password reset doesn't exist.
3) User goes to his/her email and checks for a message. Let's say all went well and he/she has an instruction for setting their password. It should look something like this.
4) User clicks on the generated link and he/she will be provided with a form for entering a new password.
PasswordResetConfirmView
is the view responsible for presenting this password reset form, and validating the token i.e. whether or not the token has expired, or if it has been used already.
-> Map this PasswordResetConfirmView
to our main project's url patterns.
user_management/urls.py
from django.urls import path
from django.contrib.auth import views as auth_views
urlpatterns = [
path('password-reset-confirm/<uidb64>/<token>/',
auth_views.PasswordResetConfirmView.as_view(template_name='users/password_reset_confirm.html'),
name='password_reset_confirm'),
]
What's in the URL's parameter?
- uidb64: The user’s id encoded in base 64.
- token: Password recovery token to check that the password is valid.
-> Now, let's provide the template for this view. Go ahead and create password_reset_confirm.html inside our users app templates directory.
password_reset_confirm.html
{% extends "users/base.html" %}
{% block title %} Password Reset {% endblock title%}
{% block content %}
<div class="form-content my-3 p-3">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-5">
{% if validlink %}
<div class="card shadow-lg border-0 rounded-lg mt-0 mb-3">
<div class="card-header justify-content-center">
<h3 class="font-weight-light my-4 text-center">Reset Your Password</h3>
</div>
{% if form.errors %}
<div class="alert alert-danger alert-dismissible" role="alert">
<div id="form_errors">
{% for key, value in form.errors.items %}
<strong>{{ value }}</strong>
{% endfor %}
</div>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
{% endif %}
<div class="card-body">
<form method="POST">
{% csrf_token %}
<div class="form-row">
<div class="col-md-10 offset-md-1">
<div class="form-group">
<label class="small mb-1" for="id_new_password1">New Password</label>
<input type="password" name="new_password1" autocomplete="new-password"
class="form-control" required id="id_new_password1"
placeholder="Enter password"/>
<span>
</span>
</div>
</div>
</div>
<div class="form-row">
<div class="col-md-10 offset-md-1">
<div class="form-group">
<label class="small mb-1" for="id_new_password2">New Password Confirmation</label>
<input type="password" name="new_password2" autocomplete="new-password"
required id="id_new_password2" class="form-control"
placeholder="Confirm password"/>
</div>
</div>
</div>
<div class="form-row">
<div class="col-md-10 offset-md-1">
<div class="form-group mt-0 mb-1">
<button type="submit" class="col-md-12 btn btn-dark" id="reset">Reset Password</button>
</div>
</div>
</div>
</form>
</div>
</div>
{% else %}
<div class="alert alert-warning">
The password reset link was invalid, possibly because it has already been used.
Please request a new password reset.
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock content %}
5) If the password reset is successful PasswordResetCompleteView
will present a view letting the user know that his/her password is successfully changed.
-> Map this view to our main project's url patterns.
user_management/urls.py
from django.urls import path
from django.contrib.auth import views as auth_views
urlpatterns = [
path('password-reset-complete/',
auth_views.PasswordResetCompleteView.as_view(template_name='users/password_reset_complete.html'),
name='password_reset_complete'),
]
-> Provide the template for the PasswordResetCompleteView
password_reset_complete.html
{% extends "users/base.html" %}
{% block content %}
<div class="container my-3 p-3">
<div class="row justify-content-center">
<div class="col-lg-5">
<div class="card shadow-lg border-0 rounded-lg mt-0 mb-3">
<div class="alert alert-info">
Your password has been set. You may go ahead and <a href="{% url 'login' %}">Login Here</a>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
Set up sending email in Django
We have configured everything that django needs to let users reset their password, but we aren't actually sending the email to the user so let's go ahead and do that.
Option 1 - The recommended way to allow our app to send emails is by enabling two factor authentication(2FA) for your Google account. Follow this link to set this up.
Option 2 - But if you don't want to enable 2FA, then simply go here and allow less secure apps for your Google account.
Edit:- As of May 30, 2022, the less secure apps setting is no longer available. Learn why here.
Alright, next go to settings.py and add the following.
settings.py
# email configs
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_USE_TLS = True
EMAIL_PORT = 587
EMAIL_HOST_USER = str(os.getenv('EMAIL_USER'))
EMAIL_HOST_PASSWORD = str(os.getenv('EMAIL_PASSWORD'))
-
EMAIL_BACKEND
is the SMTP backend that Email will be sent through. -
EMAIL_HOST
is the host to use for sending email. -
EMAIL_USE_TLS
- Whether to use a TLS (secure) connection when talking to the SMTP server. This is used for explicit TLS connections, generally on port 587. -
EMAIL_PORT
- Port to use for the SMTP server defined in EMAIL_HOST. -
EMAIL_HOST_USER
andEMAIL_HOST_PASSWORD
are username and password to use for the SMTP server respectively. To keep them safe, put them in .env file and retrieve them here as we have seen how to do in the last part of the series.
Is it working as it should?
- Start the development server and run the usual command
python manage.py runserver
in your terminal. - Go to localhost and see if it's working. Facing any issues? feel free to ask me.
Thanks for your time. You can find the finished app in github. See you next time with another part of the series.
Top comments (28)
Hi, Thanks for this tutorial. I'm having some trouble receiving password reset email. I didn't get any error message in my console. I tested my SMTP server online and it works fine.
I'm getting the following output:
[05/Nov/2022 20:49:36] "GET /pr/ HTTP/1.1" 200 3657
[05/Nov/2022 20:49:39] "POST /pr/ HTTP/1.1" 302 0
[05/Nov/2022 20:49:39] "GET /pr/ HTTP/1.1" 200 4128
But didn't receive any mail.
Hi, thanks for the feedback.
Did you check your spam folder?
Yes. It's not even there.
Hi Hana
You may have mentioned this is your other posts but I thought I would highlight it.
As we are extending existing system templates the order of the INSTALLED_APPS in the settings.py file matters.
Django will look for the first View, Form, or Template that matches, so your new template must come before the default version.
For example the Users app comes before the django.contrib.admin & django.contrib.auth apps, as below.
INSTALLED_APPS = [
# Project Apps
"Users",
Thank you for the comment. This section of the doc explains it clearly docs.djangoproject.com/en/4.1/ref/...
Hi, Thank you for this tutorial. I have the following error for reset password. Would you please help me:
SMTPAuthenticationError at /password-reset/
(535, b'5.7.8 Username and Password not accepted. Learn more at\n5.7.8 support.google.com/mail/?p=BadCred... u6-20020a056870304600b000eba1ab2e02sm4002395oau.18 - gsmtp')
Hi, Thanks for reading.
Check if you have turned on less secure apps on your Google account myaccount.google.com/lesssecureapps
and also make sure you have put the correct credentials in your settings.
I have turned on the less secure on my google account, so I'm sure about it. However, I didn't change anything inside the setting. I just want to run your app. Should I change something in the setting part in the following commands?
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_USE_TLS = True
EMAIL_PORT = 587
EMAIL_HOST_USER = str(os.getenv('EMAIL_USER'))
EMAIL_HOST_PASSWORD = str(os.getenv('EMAIL_PASSWORD'))
You have to include your email credentials in your environment variable.
Hello! I don't think it's possible to send email to users through a Google account anymore. I saw this warning message from Google as I tried to set up an app password: "To help keep your account secure, from May 30, 2022, Google no longer supports the use of third-party apps or devices which ask you to sign in to your Google Account using only your username and password."
Hey Andrew, Thanks for the feedback and you are right. I will update my article.
Hi, thanks for the tutorial, I've followed the previous ones and helped me a lot!
I want to specify some things:
1) I think you've an error in the password_reset_subject template, you created it with a .txt extension instead of .html. Or is there something I'm misunderstanding?
2) It would be awesome if you can create a tutorial about Custom User models, how to create them, how to use them, how to use email instead of username for authentication. I've had a lot of problems with that, I'm new in Django and still struggling.
3) Can you tell me please how did you learn Python+Django? I mean, resources, guidelines and topic plan or something. I used Free code camp tutorials, also a book called Django for Beginners, but I still feel that I just know a litttle bit, there are a lot of things that I've never read/heard about and I don't know if it is part of the process of learning or what. Anyway it feels frustrating.
Thanks again
Hey, thanks for the feedback. Glad I could help.
It's not an error. The subject of the email can be created with a .txt extension as mentioned here in the doc
I'll try to write about those topics. If you want a demo of authentication using email instead of username, you can find here. You can also contact me about the problems you have faced.
Don't get frustrated. I know the feeling :) What worked for me is, once you know the basics, move to doing projects on your own without following a tutorial on youtube. Instead use Google, docs, StackOverflow and blogs to solve a problem during your project. Good luck!
Thanks again, I will take your tips into account
I encountered this error:
Exception Type: TemplateDoesNotExist
Exception Value: password_reset_email.html
and from your article there was no template set for 'password_reset_email.html', any assistance will be appreciated. @earthcomfy
Hi.
password_reset_email.html
file already exists in the article. I think you missed it. Here it is:password_reset_email.html
If you encounter any issues throughout the tutorial, you can check out the code in the GitHub repository
what I mean is your urls.py but I've sorted that out.
I'm getting an error
TemplateDoesNotExist
forusers/password_reset_subject
When entering a valid email but does go through to the "we'll send you reset email" message bit if the email address does not belong to a user. No email is created or a password reset link is created for a valid email as far as I can see.
I've verified that my email configs in the settings file are fine by sending from the manage.py shell using the EmailMessage module.
Any idea why?
Turns out there was a typo in the tutorial
subject_template_name = 'users/password_reset_subject'
should be:
subject_template_name = 'users/password_reset_subject.txt'
Went through a lot of headache trying to figure that one out.
I am getting below error: Environment:
Request Method: POST
Request URL: 127.0.0.1:8000/password_reset/
Django Version: 4.1.13
Python Version: 3.10.12
Installed Applications:
['django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'upload_file_app',
'tailwind',
'theme',
'django_browser_reload']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django_browser_reload.middleware.BrowserReloadMiddleware']
Traceback (most recent call last):
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/djongo/cursor.py", line 51, in execute
self.result = Query(
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/djongo/sql2mongo/query.py", line 783, in init
self.query = self.parse()
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/djongo/sql2mongo/query.py", line 875, in parse
raise e
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/djongo/sql2mongo/query.py", line 856, in parse
return handler(self, statement)
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/djongo/sql2mongo/query.py", line 932, in _select
return SelectQuery(self.db, self.connection_properties, sm, self._params)
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/djongo/sql2mongo/query.py", line 116, in __init_
super().init(*args)
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/djongo/sql2mongo/query.py", line 62, in init
self.parse()
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/djongo/sql2mongo/query.py", line 152, in parse
self.where = WhereConverter(self, statement)
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/djongo/sql2mongo/converters.py", line 27, in init
self.parse()
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/djongo/sql2mongo/converters.py", line 119, in parse
self.op = WhereOp(
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/djongo/sql2mongo/operators.py", line 476, in init
self.evaluate()
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/djongo/sql2mongo/operators.py", line 465, in evaluate
op.evaluate()
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/djongo/sql2mongo/operators.py", line 465, in evaluate
op.evaluate()
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/djongo/sql2mongo/operators.py", line 279, in evaluate
raise SQLDecodeError
The above exception (
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/db/backends/utils.py", line 89, in _execute
return self.cursor.execute(sql, params)
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/djongo/cursor.py", line 59, in execute
raise db_exe from e
The above exception () was the direct cause of the following exception:
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/core/handlers/exception.py", line 56, in inner
response = get_response(request)
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/core/handlers/base.py", line 197, in get_response
response = wrapped_callback(request, callback_args, **callback_kwargs)
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/views/generic/base.py", line 103, in view
return self.dispatch(request, *args, **kwargs)
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/utils/decorators.py", line 46, in _wrapper
return bound_method(*args, **kwargs)
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/utils/decorators.py", line 134, in _wrapped_view
response = view_func(request, *args, **kwargs)
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/contrib/auth/views.py", line 242, in dispatch
return super().dispatch(*args, **kwargs)
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/views/generic/base.py", line 142, in dispatch
return handler(request, *args, **kwargs)
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/views/generic/edit.py", line 153, in post
return self.form_valid(form)
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/contrib/messages/views.py", line 12, in form_valid
response = super().form_valid(form)
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/contrib/auth/views.py", line 255, in form_valid
form.save(*opts)
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/contrib/auth/forms.py", line 339, in save
for user in self.get_users(email):
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/contrib/auth/forms.py", line 308, in get_users
return (
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/db/models/query.py", line 394, in __iter_
self.fetch_all()
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/db/models/query.py", line 1867, in _fetch_all
self._result_cache = list(self._iterable_class(self))
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/db/models/query.py", line 87, in __iter_
results = compiler.execute_sql(
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/db/models/sql/compiler.py", line 1398, in execute_sql
cursor.execute(sql, params)
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/db/backends/utils.py", line 102, in execute
return super().execute(sql, params)
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/db/backends/utils.py", line 67, in execute
return self.execute_with_wrappers(
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/db/backends/utils.py", line 80, in _execute_with_wrappers
return executor(sql, params, many, context)
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/db/backends/utils.py", line 84, in _execute
with self.db.wrap_database_errors:
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/db/utils.py", line 91, in __exit_
raise dj_exc_value.with_traceback(traceback) from exc_value
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/django/db/backends/utils.py", line 89, in _execute
return self.cursor.execute(sql, params)
File "/home/mohanrajjj/.local/lib/python3.10/site-packages/djongo/cursor.py", line 59, in execute
raise db_exe from e
Exception Type: DatabaseError at /password_reset/
Exception Value:
Its weird, I cannot get Django to use the templates im passing on the class view, I can do it for log in and password change, but not reset password
Hi. Can I see your code and the error you are getting?
I actually was able to make it show up with a bit of a different approach, it did not like the templates being called 'registration/...' that was an oversight
Hi, I have followed the steps for password reset and i am not getting any errors.
I have entered my email and the app password in the setting part where we setup email.
I have enabled 2factor authentication.
When i enter the email for resetting the password it redirects to the homepage but i am not receiving any mail,not even in spam folder.
There are no errors showing and the message that "the mail is sent" is not showing and it redirects to homepage.
The app loads properly with no errors
Hana Please help.