Django - Reset Password

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">&times;</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">&times;</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.

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 and EMAIL_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.

16