Handle User password - change, recover

We'll continue to cover how to complete email functionality in the Django webserver to allow users to change and restore their passwords

Welcome everyone to part 16 of the web development in python with Django. User registration and authentication are crucial parts of any web application, but an integral part is changing passwords and password recovery. The complete emailing process must be integrated into the system to protect users and keep the communication between the website owner and a user. So, in this part, we'll continue to cover how to complete email functionality in the Django webserver to allow users to change and restore their passwords.

Usually, when a user creates or owns an account in our website application, we might want to send them an email when:

  • They successfully complete and confirms their account;
  • They request to reset a password;
  • Some significant or suspicious activity is tracked on their account;
  • Send and receive the communication messages;
  • Send a 2 Factor authentication code if one is enabled for the user;
  • For any other reason, you may need to communicate with the user.

This tutorial will consist of two parts: the first will be a simple password change, and the second will be when a user forgets his password and wants to recover it. It's essential to complete the previous tutorial part to be able to continue on this one.

Django change the password:

Like before, each new feature mostly requires a new link, which is not an exception this time. To change the password, a user needs to open some URLs, and we'll begin by creating one. We head to our user's application, and in our "urls.py", we make a new path naming it "password_change":

# django_project/users/urls.py
from django.urls import path
from . import views

urlpatterns = [
    ...
    path("password_change", views.password_change, name="password_change"),
]

This URL will be accessed from the user profile, so we need to add it there. Head to the "django_project/users/templates/users/profile.html" template and change line 44 from "<a href="/">Change password?</a>" to "<a href="{% url 'password_change' %}">Change password?</a>".

This URL is targeting the "password_change" function that we also need to create in our views:

# django_project/users/views.py
from .forms import SetPasswordForm

...
@login_required
def password_change(request):
    user = request.user
    form = SetPasswordForm(user)
    return render(request, 'password_reset_confirm.html', {'form': form})

That's not a complete function, only the basic one, to confirm our URL, views, templates, and forms work. Remember that we are using the "@login_required" decorator to guarantee that only logged-in users will be able to change the password.

We might see that this function is targeting "SetPasswordForm" and "password_reset_confirm.html" templates that we don't have yet. Let's create the form first:

# django_project/users/forms.py
from django.contrib.auth.forms import SetPasswordForm
...
class SetPasswordForm(SetPasswordForm):
    class Meta:
        model = get_user_model()
        fields = ['new_password1', 'new_password2']

It's a pretty simple form that inherits everything from the Django default "SetPasswordForm", this way it would be easier to make any modifications in the future if we would like to. So, because we are sure that only authenticated users will be able to access this, we don't need to ask what the user's original password is.

 

The final step is creating a template for our password reset form. We head to our "django_project/users/templates" and create a new template, "password_reset_confirm.html":

<-- django_project/users/templates/password_reset_confirm.html -->
{% extends "main/header.html" %}
{% load crispy_forms_tags %}
{% block content %}
    <div class="content-section">
        <h2>Password Reset Confirm</h2><hr>
        <p>Please enter your new password.</p>
        <form method="POST">
            {% csrf_token %}
            {{ form|crispy }}
            <button class="btn btn-outline-info" type="submit">Reset password</button>
        </form>
    </div>
{% endblock %}

This form will ask the user to type a new password twice and submit the form. Let's check if it works:

Great! It works. Now we need to complete the view function to save a new password into our database:

# django_project/users/views.py
from .forms import SetPasswordForm
...
@login_required
def password_change(request):
    user = request.user
    if request.method == 'POST':
        form = SetPasswordForm(user, request.POST)
        if form.is_valid():
            form.save()
            messages.success(request, "Your password has been changed")
            return redirect('login')
        else:
            for error in list(form.errors.values()):
                messages.error(request, error)

    form = SetPasswordForm(user)
    return render(request, 'password_reset_confirm.html', {'form': form})

Here everything is pretty simple. We check whether the form's password meets the Django password requirements and whether password1 and password2 are the same. If so, changes will be saved into the database, and the user will be redirected to the login page to sign in with a new password. Let's give it a try:

Django recover the password:

Well, we completed the simple part, where we were changing the logged-in user password. Now, it will be way more complicated because we'll write a function to recover the password of the user who is not logged in. This will involve the email stuff we implemented in the previous tutorial; if you haven't checked it yet, do it first!

Here will be two tiny parts: where the user requests a password reset and where a user performs a password change after the request. We'll begin with URLs, same as before:

# django_project/users/urls.py
from django.urls import path
from . import views

urlpatterns = [
    ...
    path("password_change", views.password_change, name="password_change"),
    path("password_reset", views.password_reset_request, name="password_reset"),
    path('reset/<uidb64>/<token>', views.passwordResetConfirm, name='password_reset_confirm'),
]

Usually, we add a password reset URL on the login page, so let's do this change. Open our "django_project/users/templates/users/login.html" template and replace line 21 "<a href="/">Forgot password?</a>" to "<a href="{% url 'password_reset' %}">Forgot password?</a>".

Same as we did before, for both of these new URLs, we need to create a view function, we begin by creating a basic one:

# django_project/users/views.py
from .forms import PasswordResetForm

...
@user_not_authenticated
def password_reset_request(request):
    form = PasswordResetForm()
    return render(
        request=request, 
        template_name="password_reset.html", 
        context={"form": form}
        )

def passwordResetConfirm(request, uidb64, token):
    return redirect("homepage")

Right now, we are sure that all users on our website have a verified email address, so in my opinion, that's the best way to recover a forgotten password. So, we need to create a new form where we'll ask users to type an email of the user for which we want to recover the password. We head to our "django_project/users/templates" and create a new template, "password_reset.html":

<-- django_project/users/templates/password_reset.html -->
{% extends "main/header.html" %}
{% load crispy_forms_tags %}
{% block content %}
	<div class="content-section">
  	 	<h2>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 %}
            {{ form|crispy }}
            <button class="btn btn-outline-info" type="submit">Send email</button>
        </form>
  	</div>
{% endblock %}

Here we ask the user to type an email and submit it. But we don't have a form for this template, so let's create it in our "forms.py" file:

# django_project/users/forms.py
from django.contrib.auth.forms import PasswordResetForm
...
class PasswordResetForm(PasswordResetForm):
    def __init__(self, *args, **kwargs):
        super(PasswordResetForm, self).__init__(*args, **kwargs)

    captcha = ReCaptchaField(widget=ReCaptchaV2Checkbox())

We are using Django built-in "PasswordResetForm" here, and we inherit everything from this object. The purpose of doing so is because I would like to add a reCAPTCHA protection field to this one (we implemented reCAPTCHA in this tutorial) to protect our website from bots. Otherwise, creating our object here is unnecessary; you can use the default one. Let's test if it works for us:

Excellent, everything works; we will return to the "passwordResetConfirm" function a little later. So, the idea here is that the user types his email address, and our Django server sends him a unique URL that the user must use to reset his password. Here we'll use some code from the previous tutorial where we were sending a unique URL when the user registers and is asked to confirm his email address:

# django_project/users/views.py
from .forms import PasswordResetForm
from django.db.models.query_utils import Q
...

@user_not_authenticated
def password_reset_request(request):
    if request.method == 'POST':
        form = PasswordResetForm(request.POST)
        if form.is_valid():
            user_email = form.cleaned_data['email']
            associated_user = get_user_model().objects.filter(Q(email=user_email)).first()
            if associated_user:
                subject = "Password Reset request"
                message = render_to_string("template_reset_password.html", {
                    'user': associated_user,
                    'domain': get_current_site(request).domain,
                    'uid': urlsafe_base64_encode(force_bytes(associated_user.pk)),
                    'token': account_activation_token.make_token(associated_user),
                    "protocol": 'https' if request.is_secure() else 'http'
                })
                email = EmailMessage(subject, message, to=[associated_user.email])
                if email.send():
                    messages.success(request,
                        """
                        <h2>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>
                        """
                    )
                else:
                    messages.error(request, "Problem sending reset password email, <b>SERVER PROBLEM</b>")

            return redirect('homepage')

        for key, error in list(form.errors.items()):
            if key == 'captcha' and error[0] == 'This field is required.':
                messages.error(request, "You must pass the reCAPTCHA test")
                continue

    form = PasswordResetForm()
    return render(
        request=request, 
        template_name="password_reset.html", 
        context={"form": form}
        )

Not getting deep line by line into the code, the idea here is to check whether the user exists in the database with the given email. If so, send him an email with a unique link to reset a password. And if the email is successfully sent, we message that we emailed instructions to reset a password. We are not telling if the email is correct or not in case someone tries to guess the email for many attempts. 

But the same as before, we used a template to construct our mail while registering a new account. It's not an exception now, and we create one in our templates:

<-- django_project/users/templates/users/template_reset_password.html -->
{% autoescape off %}
Hello {{ user.username }},

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,
PyLessons

{% endautoescape %}

Let's check if the template works:

Now, let's type a valid email address for which we would like to recover a password and we'll see if our code works:

Nice, we received an email to recover a password, where a unique link is given to us. But this link doesn't work yet, and we need to complete our "passwordResetConfirm" function to benefit from this link. This function will be very similar to the one we used in the previous tutorial, where registered users had to click on a link to confirm their registration:

# django_project/users/views.py
from .forms import SetPasswordForm
...

def passwordResetConfirm(request, uidb64, token):
    User = get_user_model()
    try:
        uid = force_str(urlsafe_base64_decode(uidb64))
        user = User.objects.get(pk=uid)
    except:
        user = None

    if user is not None and account_activation_token.check_token(user, token):
        if request.method == 'POST':
            form = SetPasswordForm(user, request.POST)
            if form.is_valid():
                form.save()
                messages.success(request, "Your password has been set. You may go ahead and <b>log in </b> now.")
                return redirect('homepage')
            else:
                for error in list(form.errors.values()):
                    messages.error(request, error)

        form = SetPasswordForm(user)
        return render(request, 'password_reset_confirm.html', {'form': form})
    else:
        messages.error(request, "Link is expired")

    messages.error(request, 'Something went wrong, redirecting back to Homepage')
    return redirect("homepage")

Now, instead of saving our user in the database when clicking on the link, we'll redirect the user to the password-changing template. And after successfully changing the password, the user is redirected to a homepage. Let's click on the link in our mail, and let's see if it works as expected:

Yoo! it worked. Now we can log in with our new password! How cool is that?

Conclusion:

So, now our website has even better protection, and from now on, our website users will be able to recover and change their password if they want to. If you thought that it's tough and frustrating to implement such functions to recover user passwords, I proved you were wrong! Now our website has all the essential parts, from which you can continue and expand the functionality into the limitless space!

Can you believe how far we got with this website from our first introduction tutorial? That's amazing! Still, I think that you are interested in how to insert images into our articles to make them more informative and attractive, right? We'll cover this topic in the next tutorial!

See you in my next tutorial, final tutorial files you can download from my GitHub page.