Django Authentication Part 4: Email Registration and Password Resets

Introduction

This is the forth article in a multipart series on implementing the Django authentication system. I will continue to use the same demo survey application, referred to as Django Survey, to aid in the demonstration of authentication features.

As with the earlier tutorials of this series, I will be focusing on a specific use case common to authentication in a web application. This article features the use of email confirmation for user registration and password resets utilizing emails with token embedded reset links.

Series Contents:

The code for this series can be found on GitHub.

Configuring Django to use Gmail

As a means to simplify the demonstration of using emails for registering users and resetting passwords I will be utilizing Google's Gmail because it is freely available and easy to setup. The steps to setup a Gmail account for use from a third party application is as follows.

Create a Gmail account Go to the Account page Then go to Security page In the Security page scroll down to Less secure app access and enable access

Now over in the Django project's .env file introduced in part 3 of this series I add two new variables for the username and password I used for signing into Gmail as shown below.

# .env SOCIAL_AUTH_GOOGLE_OAUTH2_KEY='196219946669-d3rlmh5677etn756sv7o9o0lvc0t077r.apps.googleusercontent.com' SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET='bMmmnUefr6yRGjtvqXwTNID6' SOCIAL_AUTH_GITHUB_KEY='my-github-oauth-key' SOCIAL_AUTH_GITHUB_SECRET='my-github-oath-secret' EMAIL_HOST_USER='jondoe@gmail.com' EMAIL_HOST_PASSWORD='password'

Then at the bottom of the django_survey/settings.py module I use those environment variables for the following email settings as well as change the default PASSWORD_RESET_TIMEOUT_DAYS value of 3 to 2.

EMAIL_USE_TLS = True EMAIL_HOST = 'smtp.gmail.com' EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER') EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD') EMAIL_PORT = 587 PASSWORD_RESET_TIMEOUT_DAYS = 2

Adding Email Confirmation to Registration

Updating the registration process to include a email confirmation for non-social auth users requires a unique token to be generated and only valid for a short period of time (2 days in this example). To generate a token I provide a custom implementation of the django.contrib.auth.tokens.PasswordResetTokenGenerator class provided by Django. By overriding the _make_hash_value method to include user data along with a 2 day expiry window specified in the PASSWORD_RESET_TIMEOUT_DAYS settings value I can be confident in sending an identifyable token to the user to verify their email with.

Inside a new survey/tokens.py module I place the code for the implementation of PasswordResetTokenGenerator like so.

# tokens.py from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.utils import six class UserTokenGenerator(PasswordResetTokenGenerator): def _make_hash_value(self, user, timestamp): user_id = six.text_type(user.pk) ts = six.text_type(timestamp) is_active = six.text_type(user.is_active) return f"{user_id}{ts}{is_active}" user_tokenizer = UserTokenGenerator()

Since I am now validating the authenticity of the newly registering user's email address I need to start collecting it in the registration page and it's form. Doing this is as simple as extending the Django UserCreationForm to include a email field which I place in a new survey/forms.py module.

# forms.py from django import forms from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.models import User class RegistrationForm(UserCreationForm): email = forms.EmailField(max_length=150) class Meta: model = User fields = ('username', 'email', 'password1', 'password2')

Then I update the RegisterView view to use this newly created RegistrationForm for collecting user data complete with their email. The post method now generates a token used to confirm the authenticity of the provided email address plus I also invalidate the newly created user by setting the is_valid field to False. By setting the is_valid value to False if a user tries to login the system will raise a Validation error due to the use of the AuthenticationForm and it's clean method during the login processes.

# survey/views.py import json from django.conf import settings from django.contrib.auth import authenticate, login from django.contrib.auth.forms import UserCreationForm, AuthenticationForm from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth.models import User, Group, Permission from django.core.exceptions import ValidationError from django.core.mail import EmailMessage from django.template.loader import get_template from django.db import transaction from django.db.models import Q from django.http import HttpResponse from django.utils.encoding import force_bytes, force_text from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode from django.shortcuts import render, redirect, reverse, get_object_or_404 from django.views import View from guardian.conf import settings as guardian_settings from guardian.mixins import PermissionRequiredMixin from guardian.shortcuts import assign_perm, get_objects_for_user from .models import Survey, Question, Choice, SurveyAssignment, SurveyResponse from .tokens import user_tokenizer class RegisterView(View): def get(self, request): return render(request, 'survey/register.html', { 'form': RegistrationForm() }) def post(self, request): form = RegistrationForm(request.POST) if form.is_valid(): user = form.save(commit=False) user.is_valid = False user.save() token = user_tokenizer.make_token(user) user_id = urlsafe_base64_encode(force_bytes(user.id)) url = 'http://localhost:8000' + reverse('confirm_email', kwargs={'user_id': user_id, 'token': token}) message = get_template('survey/register_email.html').render({ 'confirm_url': url }) mail = EmailMessage('Django Survey Email Confirmation', message, to=[user.email], from_email=settings.EMAIL_HOST_USER) mail.content_subtype = 'html' mail.send() return render(request, 'survey/login.html', { 'form': AuthenticationForm(), 'message': f'A confirmation email has been sent to {user.email}. Please confirm to finish registering' }) return render(request, 'survey/register.html', { 'form': form }) # ... omitting everything below for brevity

As seen above I now generate a token with the previously described UserTokenGenerator class, convert the user's id to a base64 encoded string representation and, build a confirmation link which gets embedded in a email which is sent to the given email address.

In order to save the email address of newly registering users I need to add the email field to the register.html template which I show in its updated form below.

<!-- register.html --> {% extends 'survey/base.html' %} {% load widget_tweaks %} {% block content %} <section class="hero is-success is-fullheight"> <div class="hero-body"> <div class="container"> <h1 class="title has-text-centered"> Django Survey </h1> <div class="columns"> <div class="column is-offset-2 is-8"> <h2 class="subtitle"> Register </h2> <form action="{% url 'register' %}" method="POST" autocomplete="off"> {% csrf_token %} <div class="field"> <label for="{{ form.username.id_for_label }}" class="label"> Username </label> <div class="control"> {{ form.username|add_class:"input" }} </div> <p class="help is-danger">{{ form.username.errors }}</p> </div> <div class="field"> <label for="{{ form.email.id_for_label }}" class="label"> Email </label> <div class="control"> {{ form.email|add_class:"input" }} </div> <p class="help is-danger">{{ form.email.errors }}</p> </div> <div class="field"> <label for="{{ form.password1.id_for_label }}" class="label"> Password </label> <div class="control"> {{ form.password1|add_class:"input" }} </div> <p class="help is-danger">{{ form.password1.errors }}</p> </div> <div class="field"> <label for="{{ form.password2.id_for_label }}" class="label"> Password Check </label> <div class="control"> {{ form.password2|add_class:"input" }} </div> <p class="help is-danger">{{ form.password2.errors }}</p> </div> <div class="field"> <div class="control"> <button class="button is-link">Submit</button> </div> </div> </form> </div> </div> </div> </div> </section> {% endblock %}

Below you can see the register_email.html template which bears the confirmation url.

<!-- register_email.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Registration Confirmation</title> </head> <body> <h1>Welcome to Django Survey</h1> <p>Please click this link to confirm your email and complete registering.</p> <p><a href="{{confirm_url}}">{{ confirm_url }}</a></p> </body> </html>

Next I create a class based view to handle the GET request of the link embedded in the confirmation email. I named this class ConfirmRegistrationView and it takes two url parameters, one for the string encoded user id and another for the token.

This view will decode the user id, look up the user, and validate the token. If validated the user's is_valid is flipped to true otherwise it will display a message that the token is invalid and they should reset their password to generate another confirmation token.

Below is the new ConfirmRegistrationView class.

# survey/views.py ... skipping down to just the new class view class ConfirmRegistrationView(View): def get(self, request, user_id, token): user_id = force_text(urlsafe_base64_decode(user_id)) user = User.objects.get(pk=user_id) context = { 'form': AuthenticationForm(), 'message': 'Registration confirmation error . Please click the reset password to generate a new confirmation email.' } if user and user_tokenizer.check_token(user, token): user.is_valid = True user.save() context['message'] = 'Registration complete. Please login' return render(request, 'survey/login.html', context)

I now need to add the confirmation url path and map it to this new view class in survey/urls.py like so.

# survey/urls.py from django.contrib.auth import views as auth_views from django.urls import path from . import views urlpatterns = [ path('register/', views.RegisterView.as_view(), name='register'), path('login/', auth_views.LoginView.as_view(template_name='survey/login.html'), name='login'), path('profile/', views.ProfileView.as_view(), name='profile'), path('logout/', auth_views.LogoutView.as_view(), name='logout'), path('surveys/create/', views.SurveyCreateView.as_view(), name='survey_create'), path('survey-assginment/<int:assignment_id>/', views.SurveyAssignmentView.as_view(), name='survey_assignment'), path('survey-management/<int:survey_id>/', views.SurveyManagerView.as_view(), name='survey_management'), path('survey-results/<int:survey_id>/', views.SurveyResultsView.as_view(), name='survey_results'), path('confirm-email/<str:user_id>/<str:token>/', views.ConfirmRegistrationView.as_view(), name='confirm_email') ]

Django Password Reset Emails with Token Embedded Reset Links

In this section I will demonstrate how to implement a password reset feature that uses an email confirmation token containing link. Luckily Django has provided class based views and forms to make this a super simple process. For example, to implement the password reset view which is shown when a user clicks a forgot password link is as easy as providing a url path to django.contrib.auth.views.PasswordResetView and specifying a view template, a email template, a redirect url, and an instance of the token generator used earlier which I've shown below.

from django.conf import settings from django.contrib.auth import views as auth_views from django.urls import path from . import views from .tokens import user_tokenizer urlpatterns = [ path('register/', views.RegisterView.as_view(), name='register'), path('login/', auth_views.LoginView.as_view(template_name='survey/login.html'), name='login'), path('profile/', views.ProfileView.as_view(), name='profile'), path('logout/', auth_views.LogoutView.as_view(), name='logout'), path('surveys/create/', views.SurveyCreateView.as_view(), name='survey_create'), path('survey-assginment/<int:assignment_id>/', views.SurveyAssignmentView.as_view(), name='survey_assignment'), path('survey-management/<int:survey_id>/', views.SurveyManagerView.as_view(), name='survey_management'), path('survey-results/<int:survey_id>/', views.SurveyResultsView.as_view(), name='survey_results'), path('confirm-email/<str:user_id>/<str:token>/', views.ConfirmRegistrationView.as_view(), name='confirm_email'), path( 'reset-password/', auth_views.PasswordResetView.as_view( template_name='survey/reset_password.html', html_email_template_name='survey/reset_password_email.html', success_url=settings.LOGIN_URL, token_generator=user_tokenizer), name='reset_password' ), ]

The next step is to add a simple reset password link to the login template which I've again shown below.

<!-- login.html --> {% extends 'survey/base.html' %} {% load widget_tweaks %} {% block content %} <section class="hero is-success is-fullheight"> <div class="hero-body"> <div class="container"> <h1 class="title has-text-centered"> Django Survey </h1> <div class="columns"> <div class="column is-offset-2 is-8"> <h2 class="subtitle"> Login </h2> <p class='has-text-centered'>{{ message }}</p> <form action="{% url 'login' %}" method="POST"> {% csrf_token %} <div class="field"> <label for="{{ form.username.id_for_label }}" class="label"> Username </label> <div class="control"> {{ form.username|add_class:"input" }} </div> <p class="help is-danger">{{ form.username.errors }}</p> </div> <div class="field"> <label for="{{ form.password.id_for_label }}" class="label"> Password </label> <div class="control"> {{ form.password|add_class:"input" }} </div> <p class="help is-danger">{{ form.password.errors }}</p> </div> <div class="field is-grouped"> <div class="control"> <button class="button is-link">Submit</button> </div> <div class="control"> <a href="{% url 'reset_password' %}">Forgot password?</a> </div> </div> </form> <hr> <label for="" class="label">Or login with Google</label> <div class="field"> <div class="control"> <a href="{% url 'social:begin' 'google-oauth2' %}" class="button"> <span class="icon"> <i class="fab fa-google-plus-g"></i> </span> <span>Google</span> </a> </div> </div> </div> </div> </div> </div> </section> {% endblock %}

Of course, now I need to provide an implementation inside the survey/reset_password.html to display an email input for the reset link. The template utilizes the django.contrib.auth.forms.PasswordResetForm form class which is the default behavior for posts back to the PasswordResetView view.

<!-- reset_password.html --> {% extends 'survey/base.html' %} {% load widget_tweaks %} {% block content %} <section class="hero is-success is-fullheight"> <div class="hero-body"> <div class="container"> <h1 class="title has-text-centered"> Django Survey </h1> <div class="columns"> <div class="column is-offset-2 is-8"> <h2 class="subtitle"> Password Reset </h2> <form action="{% url 'reset_password' %}" method="POST"> {% csrf_token %} <div class="field"> <label for="{{ form.email.id_for_label }}" class="label"> Email </label> <div class="control"> {{ form.email|add_class:"input" }} </div> <p class="help is-danger">{{ form.email.errors }}</p> </div> <div class="field"> <div class="control"> <button class="button is-link">Submit</button> </div> </div> </form> </div> </div> </div> </div> </section> {% endblock %}

The PasswordResetView class generates a token and utilize the specified email template named reset_password_email.html (shown below) to email the user providing the reset link.

<!-- reset_password_email.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Django Survey Password Reset</title> </head> <body> <h1>Django Survey Password Reset</h1> <p>Please click the link below to result your password.</p> <p><a href="{{ protocol}}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}">Reset password</a></p> </body> </html>

The url referenced by the view named 'password_reset_confirm' in the above email template can now be added to the survey/urls.py module. To accomplish this I again use another default Django view class of django.contrib.auth.views.PasswordResetConfirmView and override some of the defaults as shown below.

# survey/urls.py from django.conf import settings from django.contrib.auth import views as auth_views from django.urls import path from . import views from .tokens import user_tokenizer urlpatterns = [ path('register/', views.RegisterView.as_view(), name='register'), path('login/', auth_views.LoginView.as_view(template_name='survey/login.html'), name='login'), path('profile/', views.ProfileView.as_view(), name='profile'), path('logout/', auth_views.LogoutView.as_view(), name='logout'), path('surveys/create/', views.SurveyCreateView.as_view(), name='survey_create'), path('survey-assginment/<int:assignment_id>/', views.SurveyAssignmentView.as_view(), name='survey_assignment'), path('survey-management/<int:survey_id>/', views.SurveyManagerView.as_view(), name='survey_management'), path('survey-results/<int:survey_id>/', views.SurveyResultsView.as_view(), name='survey_results'), path('confirm-email/<str:user_id>/<str:token>/', views.ConfirmRegistrationView.as_view(), name='confirm_email'), path( 'reset-password/', auth_views.PasswordResetView.as_view( template_name='survey/reset_password.html', html_email_template_name='survey/reset_password_email.html', success_url=settings.LOGIN_URL, token_generator=user_tokenizer), name='reset_password' ), path( 'reset-password-confirmation/<str:uidb64>/<str:token>/', auth_views.PasswordResetConfirmView.as_view( template_name='survey/reset_password_update.html', post_reset_login=True, post_reset_login_backend='django.contrib.auth.backends.ModelBackend', token_generator=user_tokenizer, success_url=settings.LOGIN_REDIRECT_URL), name='password_reset_confirm' ), ]

As you can see the PasswordResetConfirmView has been assigned the reset_password_update.html template which displays a set of new password fields similar to the registration template along with the cusom implementation of the tokenizer class defined earlier. Additionally, I have specified that the user should be logged in once the password is successfully updated as well as where to redirect them to based off the LOGIN_REDIRECT_URL specified in the settings.py module. Since this project has multiple authentication backends I must explicitly tell it to use the standard ModelBackend for authentication.

The completed html template for reset_password_update.html is shown below.

<!-- login.html --> {% extends 'survey/base.html' %} {% load widget_tweaks %} {% block content %} <section class="hero is-success is-fullheight"> <div class="hero-body"> <div class="container"> <h1 class="title has-text-centered"> Django Survey </h1> <div class="columns"> <div class="column is-offset-2 is-8"> <h2 class="subtitle"> Password Reset </h2> <form method="POST"> {% csrf_token %} <div class="field"> <label for="{{ form.new_password1.id_for_label }}" class="label"> New Password </label> <div class="control"> {{ form.new_password1|add_class:"input" }} </div> <p class="help is-danger">{{ form.new_password1.errors }}</p> </div> <div class="field"> <label for="{{ form.new_password2.id_for_label }}" class="label"> New Password2 </label> <div class="control"> {{ form.new_password2|add_class:"input" }} </div> <p class="help is-danger">{{ form.new_password2.errors }}</p> </div> <div class="field"> <div class="control"> <button class="button is-link">Submit</button> </div> </div> </form> </div> </div> </div> </div> </section> {% endblock %}

Want to Learn More About Python and Django?

thecodinginterface.com earns commision from sales of linked products such as the books above. This enables providing continued free tutorials and content so, thank you for supporting the authors of these resources as well as thecodinginterface.com

Conclusion

In this tutorial I have demonstrated how to implement a registration and password reset workflow that utilizes emails with token embedded confirmation links. All of these implementations utilize pure Django functionality with the exception of a custom implementation of the PasswordResetTokenGenerator.

Thanks for joining along on this tour of some of the awesome authentication features that can be implemented in the Django web framework using Python. As always, don't be shy about commenting or critiquing below.