Signup and password reset with allauth's email verification in Django

In this tutorial we will allow users to sign up on our website and use email verification to make sure that the given email address is valid.

July 7, 2020, 6:30 a.m.
Themes: Authentication

We will use email as identifier and not use a username. allauth takes care of that when we add the following to our settings:

ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
ACCOUNT_USERNAME_REQUIRED = False

This tells allauth that we want to authenticate via email, that an email field is required, that we also want to verify our email address by sending a verification link and that we will not use the username. Behind the scenes Django will still use a unique username, which is generated by allauth. You can check that in Django admin; also logging in as superuser still requires a username. Verifying that the email address is present and unique is done by allauth, not by Django. It is possible to tell Django to use email as the unique user identifier, that would require a.o. redefining the email field. The username field can also be completely removed from the user model, by subclassing a user model from AbstractBaseUser. The allauth settings would then be different. This is beyond the scope of this tutorial.

To simulate email verification it is possible to use the console, however the email flow is more realistic when sending real emails. In production this would be done by a transactional mail service such as Mailgun, Sendgrid, Mandrill.; in development you can use a gmail account. The number of emails that can be sent is obviously limited, and it requires a setting in gmail to allow 'less secure apps'. Gmail by default uses OAuth 2.0, which means that access to a gmail account is given via a token. Allowing 'less secure apps' disables this, allowing an application to get access via username / password, which is, well, less secure. So it makes sense to use a gmail account different from your work or private mail.

Django has parameters for all email settings. For Gmail, put the following in your settings file (preferably in local.py that is not tracked):

EMAIL_HOST = 'smtp.gmail.com'
EMAIL_USE_TLS = True
EMAIL_PORT = 587
EMAIL_HOST_USER = 'yourusername@gmail.com'
EMAIL_HOST_PASSWORD = 'yourpassword'
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER

If your settings (dev.py in Wagtail) specify an EMAIL_BACKEND, comment it out:

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

This causes Django to use the default smtp backend. Of course you can also set this explicitly.

Time to make a signup template. allauth by default only asks for email (when this is the authentication method) and password. If we want to ask the user to fill in more fields, we have to use the setting ACCOUNT_SIGNUP_FORM_CLASS and specify a custom signup form. This should be a plain form deriving from forms.Form, not a ModelForm. To ask the user for first name, last name and display name, put the following in forms.py:

from django import forms
from django.utils.translation import gettext_lazy as _

class SignupForm(forms.Form):
    first_name = forms.CharField(max_length=30, label=_("First name"))
    last_name = forms.CharField(max_length=30, label=_("Last name"))
    display_name = forms.CharField(max_length=30, label=_("Display name"), help_text=_("Will be shown e.g. when commenting."))

    def signup(self, request, user):
        user.first_name = self.cleaned_data['first_name']
        user.last_name = self.cleaned_data['last_name']
        user.display_name = self.cleaned_data['display_name']
        user.save()

Tell allauth about this form by putting the following in your settings:

ACCOUNT_SIGNUP_FORM_CLASS = 'userauth.forms.SignupForm'

Now we will create a signup template. Copy allauth's template and put it in the account/ subdirectory of your app's template directory (in our case userauth/templates/userauth/). Adapt the format to your liking; here we will use a Bootstrap card format and widget-tweaks for the input fields. The complete template signup.html is then:

{% extends 'account/base_card.html' %}

{% load i18n %}

{% block head_title %}{% trans "Sign Up" %}{% endblock %}

{% block card-header %}
<h3>{% trans "Sign Up" %}</h3>
{% endblock %}

{% block card-body %}

<form method="POST" action="{% url 'account_signup' %}" class="needs-validation" novalidate>
    {% csrf_token %}
    <div class="form-row">
        <div class="form-group col-md-6">
            {% with field=form.email %}{% include "account/form_field.html" %}{% endwith %}
        </div>
        <div class="form-group col-md-6">
            {% with field=form.display_name %}{% include "account/form_field.html" %}{% endwith %}
        </div>
    </div>
    <div class="form-row">
        <div class="form-group col-md-6">
            {% with field=form.password1 %}{% include "account/form_field.html" %}{% endwith %}
        </div>
        <div class="form-group col-md-6">
            {% with field=form.password2 %}{% include "account/form_field.html" %}{% endwith %}
        </div>
    </div>
    <div class="form-row">
        <div class="form-group col-md-6">
            {% with field=form.first_name %}{% include "account/form_field.html" %}{% endwith %}
        </div>
        <div class="form-group col-md-6">
            {% with field=form.last_name %}{% include "account/form_field.html" %}{% endwith %}
        </div>
    </div>
    <button type="submit" class="btn btn-outline-primary">{% trans "Sign Up" %}</button>
</form>

{% endblock %}

{% block card-footer %}
<p>{% trans "Already have an account?" %} <a href="{% url 'account_login' %}">{% trans "Sign In" %}</a></p>
{% endblock %}

The templates base_card.html and form_field.html have been used before in login. The url names account_signup and account_login are provided by allauth.

Time to test it out! Start up the server and go to http://127.0.0.1:8000/accounts/signup/. If all goes well there is our new template. Register a new user. A new template appears telling us that a verification link has been sent. Go to you mail and click this link. A message appears asking you to confirm the email address, after which you can sign in.

Just a few things to tidy up. The verification and confirmation templates were still default allauth. Change them in the same style as signup.html. The verification mail mentions example.com in its subject. Head over to the Django admin, to Sites, and change the setting for the domain and its display name; that's the name that will be used. When using Wagtail, there is also a BASE_URL in the settings file with the same name example.com. This is used for Wagtail admin purposes, but let's change that as well into our domain. And in admin.py, in list_display, there is still the username; take that out and replace it with display_name.

Now that we have email set up, we can also implement password reset. This too is fully supported by allauth. The named url account_reset_password is the starting point, a logical place to put this is in our login template. From there on, the user is led through four templates:

  • password_reset.html: asks the user for his email address
  • password_reset_done.html: confirmation that an email has been sent to reset the password
  • password_reset_from_key.html: input of the new password
  • password_reset_from_key_done.html: confirmation that the password has been changed

Copy the templates. Change the styling using the format above or to your liking and we're done. Don't forget to push your changes to your git repository!

Read on if you want to allow the user to add more fields via an update template and to delete his profile.

Comment on this article (sign in first or confirm by name and email below)