Build an Email Activation Flow with Django and SendGrid

June 09, 2022
Written by
Daniel Diaz
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Mia Adjei
Twilion

Build an Email Activation Flow with Django and SendGrid

Any web application that includes authentication processes should have some sort of email activation flow. If you don’t, you’re decreasing the possibility of contacting your users and opening a door for spammers to create fake accounts and affect your website.

This tutorial will teach you how to build confirmation emails to verify users’ registration using Django and SendGrid.

Django has a built-in mail sending interface, which we’ll be using to send emails with SendGrid through the tutorial.

After we finish, you’ll have an authentication system that requires users to verify their account with a confirmation email.

We’ll create a complete project from scratch but if you already have one, don’t hesitate in following along with this tutorial.

Whenever you want a quick reference to the code, you can visit this GitHub repository. 

Prerequisites

To complete this tutorial, you’ll need the following:

Create a Python virtual environment

It’s considered a good practice to set up a Python virtual environment for every Django project you start. This allows you to manage the dependencies of each project you have.

If you’re following along with a Unix based OS like Linux or macOS, open a terminal and run the following commands to create and source a virtual environment named venv:

python -m venv venv
source venv/bin/activate

In case the activation command doesn’t work, review this activation script table.

If you’re a Windows user, run the commands below:

python -m venv venv
venv\Scripts\activate

Now that our virtual environment is active, let’s install all the packages we need:

To install them all, run this pip command:

pip install django django-environ django-crispy-forms crispy-bootstrap5

Create a Django project

Now, create a folder called email-verification which will be the directory your project will be placed in. After that, start a Django project named config (considered a best practice) in the new directory:

mkdir email-verification
cd email-verification/
django-admin startproject config .

After doing this, you should have the following file structure:

.
── email-verification
│   ├── config
│   │   ├── asgi.py
│   │   ├── __init__.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   └── manage.py
── venv
    ├── bin
    ├── include
    ├── lib
    ├── lib64 -> lib

Run the server with the following command:

python manage.py runserver

If you run the server and visit your localhost, you should see Django’s welcome page.

Django welcome page

We have a dedicated tutorial where we explain how to send emails with Django and SendGrid via the Django shell. Make sure to check it out if you want to get more detailed instructions.

1. Get a SendGrid API key

Go to SendGrid’s API keys page, and get an API key for this project.

SendGrid API keys page

Click on the Create API Key button, which will open the following panel:

Create API key screen

Enter a name for your API key, give it Full Access permissions, and then click the Create & View button.

After creating it, you’ll get a private API key. Copy it and save it somewhere safe, because we’ll need it later.

2. Create the .env file

You shouldn’t include sensitive information like API keys directly in the code. That’s why we’ll be using environmental variables.

Create an empty .env file inside the config folder, which will store your SendGrid API key. You can do it from the terminal with the commands below:

pwd
# /home/daniel/Documents/email-verification/config
touch .env

Open that .env file and set the following key-value pairs:

SENDGRID_API_KEY=<your-api-key>
FROM_EMAIL=<your-email-address>

Replace <your-api-key> with your private SendGrid API key you got from the first step, and do the same for the <your-email-address> value.

We’ll be using these two environment variables in our settings file with the help of django-environ.

If you’re using a Version Control System (VCS) like git, make sure to include .env in your .gitignore file. Including private API keys in one of your commits is a terrible security mistake.

3. Configure config/settings.py to send emails with SendGrid

The settings file located at config/settings.py stores all the configurations of our project, which of course includes email sending configuration.

Open the settings.py file and import the django-environ package:


# config/settings.py

# After importing Path
import environ

env = environ.Env()
environ.Env.read_env() # Reads the .env file

The code above imports django-environ and creates an env variable that contains the environmental variables stored in the .env file.

Now, head to the end of the settings file and write the following code:

# Bottom of settings.py 
# Twilio SendGrid
EMAIL_HOST = 'smtp.sendgrid.net'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'apikey' # Name for all the SenGrid accounts
EMAIL_HOST_PASSWORD = env('SENDGRID_API_KEY')

# The email you'll be sending emails from
DEFAULT_FROM_EMAIL = env('FROM_EMAIL', default='noreply@gmail.com')
LOGIN_REDIRECT_URL = 'success'

Here, we’re using the env variable to return the values present in the .env file. Let’s see three crucial settings we’ll be using:

  • EMAIL_HOST_PASSWORD: The password which Django will try to use to connect to the EMAIL_HOST (SendGrid), in this case, your private API key
  • DEFAULT_FROM_EMAIL: The email address your recipients will receive mail from
  • LOGIN_REDIRECT_URL: The URL namespace the users will be redirected to once they activate their account with the verification email.

4. Send a testing email from the Django Shell

Finally, let’s test out whether we can send emails with Django and SendGrid. To do so, open a new terminal window, making sure your virtual environment is activated, and enter the Django shell.

(venv) $ python manage.py shell

Since we’re running the shell with manage.py, all of our settings will be imported. Now that you’re in the shell, send an email with the following code, replacing the placeholder "to@receiver.org" address with an email address you have access to:

from django.core.mail import send_mail
from django.conf import settings
send_mail('Testing mail', 'A cool message :)', settings.DEFAULT_FROM_EMAIL, ['to@receiver.org'])

We used the send_mail function to send this message, and we passed the subject, message, from_email, and recipient_list parameters to it.

As you can see, we used the settings object to get the DEFAULT_FROM_EMAIL and used it to send the email.

If you have access to the email address you sent the email to, you should see a message like this arrive in your inbox:

Received test email in inbox

If you run into an error at this step, make sure you have verified your sender identity in the SendGrid console.

Create signup and email activation flow

Now that you can send emails with Django and SendGrid, it’s time to build the email activation process.

Start a users app

From the root of your project, start an app called users which will let us introduce custom features to Django’s built-in authentication system:

python manage.py startapp users

This app isn’t meant to replace Django’s built-in User model, so we’re going to use it to create custom tokens, forms, views, and urls files.

Proceed to install the app in your config/settings.py file:


# config/settings.py
INSTALLED_APPS = [
    # More apps ...
    'django.contrib.staticfiles',

    # Users app
    'users',

]

Lastly, open config/urls.py and modify your project URL patterns to include the users app URL configuration.


from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('users.urls')),
]

Of course, the users.urls file doesn’t exist yet, so let’s create an empty urlpatterns list for the users app. Create a users/urls.py file inside the app, and add an empty list:

urlpatterns = []

This will prevent Django from raising errors in the following steps.

Coding a token generator

We’ll need to use a token generator to create one-time-use tokens for our users to activate their accounts. You can see this exact process on sites like lyricstraining. Fortunately, Django already has some of this functionality, so the only thing we’ll do is customize it a little bit.

To build a one-time token generator, head into the users app you created in the previous step and create a file named token.py. Then add the code below:

# users/token.py

from django.contrib.auth.tokens import PasswordResetTokenGenerator

class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
    def _make_hash_value(self, user, timestamp):
        return (
            str(user.is_active) + str(user.pk) + str(timestamp)
        )

token_generator = AccountActivationTokenGenerator()

This code might seem complex, so let’s see what each piece of code means.

First, we import the PasswordResetTokenGenerator class that generates and checks tokens for password resetting. We took advantage of its make_token() and check_token() methods to create the token generator.

The make_token() method generates a hash value with user-related data like their ID, password (hashed value, Django doesn’t store raw passwords), logging timestamp, and email. This information will change once the token has been used, which means the token won’t be valid.

The default _make_hash_value() of this class returns the following:

    def _make_hash_value(self, user, timestamp):
        login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None)
        email_field = user.get_email_field_name()
        email = getattr(user, email_field, '') or ''
        return f'{user.pk}{user.password}{login_timestamp}{timestamp}{email}'

So, we’re only modifying the method and creating a variable token_generator to instantiate it.

To check out this new class in action, first, migrate your database (SQLite by default) by running the following command in a terminal window:

python manage.py migrate

In the command above, we’re creating the database tables for the users, which we’ll be using next. Now, open the Django shell:

python manage.py shell

Create a new user called user1 and pass it to the token_generator.make_token()method:

from django.contrib.auth import get_user_model
from users.token import token_generator
users = get_user_model()
user1 = users.objects.create(username="test", password="acoolpass124")
token_generator.make_token(user1)

If you run the code above, you should get a token like this:

'ark0dy-97b7b1bccb1367eac4b81634d5d0bf6a'

Now that we have a way to generate one-time tokens for each user, it’s time to continue developing our Django project.

Signup form

We need to create a SignUpForm which inherits from the default UserCreationForm  and adds an email field because we need that to send the activation link to our users.

We’ll also need a custom send_activation_email() method, which, as the name suggests, sends the activation link to the users.

Create a file users/forms.py inside the users app and paste the code below in it:

# users/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm

from django.contrib.sites.shortcuts import get_current_site
from django.contrib.auth import get_user_model
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode

from django.template.loader import render_to_string

from .token import token_generator

user_model = get_user_model()

# Sign Up Form
class SignUpForm(UserCreationForm):

    email = forms.EmailField(
        max_length=254, help_text='Enter a valid email address')

    class Meta:
        model = user_model
        fields = [
            'username',
            'email',
            'password1',
            'password2',
        ]

    # We need the user object, so it's an additional parameter
    def send_activation_email(self, request, user):
        current_site = get_current_site(request)
        subject = 'Activate Your Account'
        message = render_to_string(
            'users/activate_account.html',
            {
                'user': user,
                'domain': current_site.domain,
                'uid': urlsafe_base64_encode(force_bytes(user.pk)),
                'token': token_generator.make_token(user),
            }
        )

        user.email_user(subject, message, html_message=message)

This is a rather huge form, but let’s focus on the send_activation_email(). This method accepts two arguments: the request, and a User object. We use the request to get the current site domain (localhost:8000 in our case), and the user to get its base64 encoded ID and a one-time token.

All of this information is passed as context to a template users/activate_account.html, which we’ll create later.

Finally, we send an email to the user with the respective subject, message, and html message.

We created this method inside the form because views should have the least amount of logic possible.

Views

We’re going to create multiple views, so if at any point you need a guide, check out the source code of the project.

These are the views we’re going to build:

  • SignUpView (Registration view)
  • ActivateView (Activates the user’s account)
  • CheckEmailView (Points users to check their email)
  • SuccessView (Displays success template)
  • CustomLoginView (Customized logging view)

Open the users/views.py file inside the users app and import the following:

# users/views.py
from django.shortcuts import render
from django.views.generic import CreateView, TemplateView, RedirectView
from django.contrib.auth import login
from django.contrib.auth.views import LoginView

from django.urls import reverse_lazy
from django.utils.http import urlsafe_base64_decode
from django.utils.encoding import force_str # force_text on older versions of Django

from .forms import SignUpForm, token_generator, user_model

We’re importing some Django generic views, some utilities, and of course the SignUpForm and token_generator we built previously.

Now, create a SignUpView inheriting from generic CreateView, customize the form_valid() method to set the user status to inactive, and send the activation email:

class SignUpView(CreateView):
    form_class = SignUpForm 
    template_name = 'users/signup.html'
    success_url = reverse_lazy('check_email')

    def form_valid(self, form):
        to_return = super().form_valid(form)
        
        user = form.save()
        user.is_active = False # Turns the user status to inactive
        user.save()

        form.send_activation_email(self.request, user)

        return to_return

Taking into account that we’re using the default User model, we’re setting the is_active field to false, but if you have a custom user model or a one-to-one relationship to a profile model, you can modify the view above to set the corresponding field to false.

If you have a custom user model, you should create a model manager that sets all the users’ status to False at the creation moment.

Then, write an activation view inheriting from the generic RedirectView, because if the get() method runs successfully, we’ll redirect to the success namespace, which we’ll create later:

class ActivateView(RedirectView):

    url = reverse_lazy('success')

    # Custom get method
    def get(self, request, uidb64, token):

        try:
            uid = force_str(urlsafe_base64_decode(uidb64))
            user = user_model.objects.get(pk=uid)
        except (TypeError, ValueError, OverflowError, user_model.DoesNotExist):
            user = None

        if user is not None and token_generator.check_token(user, token):
            user.is_active = True
            user.save()
            login(request, user)
            return super().get(request, uidb64, token)
        else:
            return render(request, 'users/activate_account_invalid.html')

With the new release of Django 4.0, some features were removed or renamed. This is the case for the force_str function which in older versions (Django 3.x), was named force_text.

Finally, write a simple CheckEmailView and SuccessView, which are nothing but TemplateViews:

class CheckEmailView(TemplateView):
    template_name = 'users/check_email.html'

class SuccessView(TemplateView):
    template_name = 'users/success.html'

Don’t worry about templates, since we’ll create them later.

URLs

Next, we’re going to configure the users app URLs. In users/urls.py, import all the views you created previously and write the respective URL patterns, replacing the empty list from the earlier step:

from django.urls import path

from .views import (
    SignUpView,
    ActivateView,
    CheckEmailView,
    SuccessView,
)
# https://docs.djangoproject.com/en/4.0/ref/templates/language/#id1
urlpatterns = [
    path('signup/', SignUpView.as_view(), name="signup"),
    path('activate/<uidb64>/<token>/', ActivateView.as_view(), name="activate"),
    path('check-email/', CheckEmailView.as_view(), name="check_email"),
    path('success/', SuccessView.as_view(), name="success"),
]

We’re almost done, but we need to write all the templates before testing the result of this project.

Building templates

After building all the backend functionality (what the user can’t see), it’s time to build the templates, which are plain HTML files that let us save time using template inheritance and display data dynamically.

For that purpose, we’re going to use the Django template language (DTL) and Bootstrap 5. Using Bootstrap 5 means we won’t be using static files.

We’ve already installed Django Crispy Forms, so the only thing that is left is to install it in the settings file.


# config/settings.py
INSTALLED_APPS = [
    ....
    'django.contrib.staticfiles',

    # Forms
    'crispy_forms',
    'crispy_bootstrap5',

    # Users
    'users',
]

CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"

CRISPY_TEMPLATE_PACK = "bootstrap5"

Now, head into your terminal and create the typical Django template double structure for the users app:

pwd
# /home/daniel/Documents/email-verification/users
mkdir -p templates/users

We’re going to build the following templates:

  • activate_account.html (Template used to send emails)
  • base.html (Base template for all other templates)
  • signup.html (Sign up template form)
  • check_email.html (Simple check email message)
  • success.html (Displayed if the account activation went well)

All the following templates will be located under the users/templates/users directory.

Email activation template

Nowadays, which automated message is sent as plain text?

We’re going to send an HTML message with custom info to the user. In fact, we’ve already implemented that functionality in the SignUpForm so it’s time to create the corresponding template.

Create a file activate_account.html inside the templates folder and paste the following code:

<div>
    <h2>Hey {{ user.username }}</h2>

    <h3>We noticed you just sign up on our website<h3>
    <a href="http://{{ domain }}{% url 'activate' uidb64=uid token=token %}">Please <strong>confirm your email address</strong> and activate your account</a>
</div>

It’s a simple HTML template, that uses DTL variables to get the information passed through the context dictionary (that we wrote in the SignUpForm), displays it as a heading two (h2), and creates a link similar to the following:

http://localhost:8000/activate/MTE/arlr3y-7fb92ddbceb2f3efee7581f45a044135/

We’ll see how this looks when the email is sent just after we finish.

Base template

When working with Django templates, it’s recommended to create a base template that contains a basic HTML skeleton and CDN links. This lets you save code, and of course, time, since you don’t have to copy-paste the same HTML structure for all of your templates.

Let’s do so in this app. Create a base.html file and paste in the following template:

<!DOCTYPE html>
<html lang="en">

    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Django Email Confirmation</title>
        <!-- CSS only -->
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
            integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    </head>

    <body>
        <div class="container">
            {% block body %}

            {% endblock body %}
        </div>
        <!-- JavaScript Bundle with Popper -->
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
            integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous">
        </script>
    </body>

</html>

Yes, we’re including links to Bootstrap 5 and creating a block body which we’ll use later. Basically, all the templates that extend the base template will use this body block to “insert” HTML snippets while using the basic HTML skeleton.

Signup template

The user should be able to sign up with a form from our website, so let’s create the signup template, using the crispy filter.

Create a template signup.html inside the templates folder, and use the code below:

{% extends 'users/base.html' %}
{% load crispy_forms_tags %}
{% block body %}
<div class="mx-auto">
    <div class="form-group">
        <form action="" method="post">
            {% csrf_token %}
            {{ form|crispy }}
            <button type="submit" class="btn btn-danger w-100 my-3">Create account</button>
        </form>
    </div>
</div>
{% endblock body %}

Note how we load the crispy form tags just after we extend from the base template. Also, it is important to remember that every time we set up a form in our templates, it should have a  CSRF token inside of it.

Check email template

Create a check_email.html template and implement the following code:

{% extends 'users/base.html' %}

{% block body %}
<div class="mx-auto text-center">
    <h1>Activate your account</h1>
    <p>Please check your inbox and verify your email address</p>
</div>

{% endblock body %}

Once again, we’re extending the base template and using the body block to display a rather minimalistic message.

Success template

Our final template will show the user a success message that confirms their account has been activated.

Create a success.html template and paste in the following code:

{% extends 'users/base.html' %}

{% block body %}
<div class="mx-auto text-center">
    <h1>Congrats! you just verified your account</h1>
</div>

{% endblock body %}

Testing the results

This is the user interaction when they try to register in this web app. You can follow along with these steps in order to test the application.

First, users go to the signup page (localhost:8000/signup/) and enter their information to create an account.

Signup page with form

Then they’re redirected to the CheckEmailView, which displays this simple message:

Check email view

If they check their inbox, a message similar to the one below should appear:

Message received in inbox

They click on the email verification link and are redirected to the ActivateView. If the token is correct, they’re immediately redirected to the success page.

Message in ActivateView

You can see what’s happening behind the scenes in the local server logs:

Screenshot of server logs

Conclusion

Django is one of the most used web frameworks out there, and it has a vast amount of tools to solve everyday programmer tasks like email activation.

Also, you can integrate it with powerful services such as SendGrid, Twilio Programmable SMS, or nearly any existing web API.

Now that you have completed this tutorial, you know how to:

  • Use SendGrid SMTP service to send emails with Django
  • Create one-time links with Django
  • Register users and activate their accounts via email with Django and SendGrid

Daniel is a self-taught Python Developer, Technical Writer, and lifelong learner. He enjoys creating software from scratch and explaining this process through stunning articles. Feel free to reach him at linkedin.com/in/danidiaztech/, or on Twitter at @DaniDiazTech.