Hire the author: Maina K

All relevant code for this tutorial can be found here

Recently I was working on an API, and the foundational parts had already been done, a key part being authentication. I, however, noticed how creating an account allowed any email to be used.


Wanting to create a credible user base I decided to implement a check that required emails to be verified before login could be accepted. Following a series of “how-to” searches on Google, I settled on SendGrid due to its popularity and extensive email plans.

This is how I was able to pull it off.

Some words/phrases to keep in mind

  • GraphQL => GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data
  • SendGrid => SendGrid is a customer communication platform for transactional and marketing email
  • Django => Django is a Python-based free and open-source web framework, which follows the model-template-view architectural pattern
  • Graphene-Python => A library for building GraphQL APIs in Python.
  • Graphene-Django => Built on top of Graphene, the package provides some additional abstractions that make it easy to add GraphQL functionality to a Django project
  • Django Template Language(DTL) => Django shipped built-in template system for dynamic HTML generation.

Prerequisites

You will need the following to follow along:

  • A working device, preferably a computer.
  • Python 3+
  • A working understanding of both Python and GraphQL
  • A functional terminal/command-line application (because how else would you appear techie, right?)
  • A SendGrid account of course. You can use this link to create one for free.

SendGrid API Key and Django-Graphene

On creating the SendGrid account navigate to the settings and create an API Key. Copy this key and keep it safe, SendGrid will not allow you to view it again, we’ll need it later.

Now we set up our django-graphql project, we will be using Graphene-Django which provides additional abstractions that make it easy to add GraphQL functionality to our Django project. You can check its docs here.

Setting up the project

Now we set up our django-graphql project, we will be using Graphene-Django which provides some additional abstractions that make it easy to add GraphQL functionality to our Django project. You can check its docs here.

In your terminal (I am using a unix based shell) type the following commands to setup the project.

mkdir django-sendgrid && cd django-sendgrid && pipenv shell

The above command does three things: creates a folder to host our project, moves to the folder, and creates a virtual environment using the pipenv shell command.

NB: Using pipenv is a personal preference, the more common virtualenv command would serve the same purpose. The choice, however, is not simply aesthetic, for more on the functional benefits of pipenv check out its docs here 

We then proceed to install Django using the pipenv command. While at it, we might as well install other packages we will require for the project. Type this in your bash.

pipenv install django sendgrid graphene-django django-graphql-jwt

The above command aside from Django installs the django-graphene package we mentioned earlier as well as the sendgrid python package that will aid in helping us integrate the Sendgrid API into our project. django-graphql-jwt will help in generating JsonWebTokens for authentication purposes.

We set up the Django project as is wont by running the following command while in our project directory:

django-admin startproject SendgridDjangoDemo .

We will then proceed to create an app where all our authentication code will reside. First we cd into the created folder (SendgridDjangoDemo) and run the following command

django-admin startapp authentication

The .env factor

All through the project, we will deal with some sensitive information that best resides in an environment file and not in our actual code. It is good practice to always create a .env file to avoid accidentally adding sensitive information to version control.

We will create a .env file in our root folder and it should contain the following information:

export SECRET='thisisasecretkey' #move your secret key from settings to here
export SENDGRID_API_KEY= 'your sendgrid api key i.e SG.XXXXXXX'
export DOMAIN='http://127.0.0.1:8000'
export EMAIL_HOST='smtp.sendgrid.net'
export EMAIL_HOST_USER='apikey'#this is required as is and is a SendGrid default.
export EMAIL_PORT=587
export EMAIL_USE_TLS=True

Most of the above information is linked to the SendGrid setup. Most of the above variables are SendGrid defaults, i.e EMAIL_HOST, EMAIL_HOST_USER and EMAIL_PORT. The SENDGRID_API_KEY is the long string that we generated after setting up a SendGrid account. The DOMAIN variable is to be used as a base URL for your API.

We can then proceed to load up our environment variables by running

source .env

Configuring the settings

Before we start coding we need to make some edits to the settings.py file to accommodate the app we have created as well as some of the packages we installed earlier. After the additions your INSTALLED_APPS list should resemble:

INSTALLED_APPS = [
   'django.contrib.admin',
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.messages',
   'django.contrib.staticfiles',
   'graphene_django',
   'SendgridDjangoDemo.authentication'
]

We also need to make edits to the graphene and Django’s AUTHENTICATION_BACKENDS (to accommodate graphql_jwt) configurations.. Add the following to your settings file.

# GraphQl settings
GRAPHENE = {
    'SCHEMA': 'SendgridDjangoDemo.schema.schema',
    'MIDDLEWARE': [
        'graphql_jwt.middleware.JSONWebTokenMiddleware',
    ],
}
# ON TOP  Add settings for authentication with graphql_jwt
AUTHENTICATION_BACKENDS = [
    'graphql_jwt.backends.JSONWebTokenBackend',
    'django.contrib.auth.backends.ModelBackend',
]

So a couple of things are going on here, first, we set up the GraphQL settings using the GRAPHENE config dictionary. We provide the position of the main schema file in our project as well as configuring graphql_jwt middleware.

We also include the graphql_jwt in our authentication backends for authentication purposes.

Setting up our models

We can now proceed to define our models which will define the data to be used in the API. In your authentication/models.py file add the following code.

from django.db import models
from django.contrib.auth.models import AbstractUser
# Create your models here.
class User(AbstractUser):
   email = models.EmailField(max_length=100, unique=True)
   password = models.CharField(max_length=100)
   is_verified = models.BooleanField(default=False)
   USERNAME_FIELD = 'email'
   REQUIRED_FIELDS = []

In the code snippet above we inherit from the AbstractUser class this enables us to override some default fields and introduce some of our own to the AbstractUser class. We are particularly concerned with the is_verified field which will be used as a validity flag for registered emails.

We will also need to use the email for authentication as opposed to the default username, thus we require the email field to be unique.

We can now proceed to run migrations to create our SQLite DB and set up our database columns, but before we do that we need to make an extra addition to the settings file.

Since we have altered Django’s default user model, we need the AUTH_USER_MODEL variable in settings to point to our models class, lest we get errors. Add the following to your settings file.

AUTH_USER_MODEL = 'authentication.User'

We can now proceed to run our migrations.

./manage.py makemigrations
./manage.py migrate

Setting up the registration mutation

We will now proceed to code the registration mutation that will be pertinent in creating our users. Create a schema.py file in the authentication folder. In the authentication/schema.py file add the following code

import graphene
from graphene_django import DjangoObjectType
from SendgridDjangoDemo.authentication.models import User
class UserType(DjangoObjectType):
   class Meta:
       model = User
class CreateUser(graphene.Mutation):
   message = graphene.String()
   user = graphene.Field(UserType)
   class Arguments:
       username = graphene.String()
       email = graphene.String(required=True)
       password = graphene.String(required=True)
   def mutate(self, info, **kwargs):
       user = User.objects.create_user(
           email=kwargs.get('email'),
           username=kwargs.get('username'),
       )
       user.set_password(kwargs.get('password'))
       user.save()
       return CreateUser(user=user, message="Successfully created user, {}".format(user.username)
)
class Mutation(graphene.ObjectType):
   create_user = CreateUser.Field()

I will not delve into the details of GraphQL implementations since this requires an article of its own, think of the above if not familiar with graphene functionalities as an abstracted registration system.


I’ll, however, draw your attention towards the mutate method this is
where the registration takes place and most of the functionality should look familiar.We will then proceed to define the outward schema file that we referenced in the GRAPHENE configs in the settings.py file. It should look like:

# SendgridDjangoDemo/schema.py
import graphene
from SendgridDjangoDemo.authentication.schema import  Mutation as AuthMutation
class Mutation(AuthMutation, graphene.ObjectType):
   pass
schema = graphene.Schema(mutation=Mutation)

And finally the Graphql single endpoint in SendgridDjangoDemo/urls.py:

from django.contrib import admin
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView
urlpatterns = [
    path('admin/', admin.site.urls),
    path('sendgriddemo/', csrf_exempt(GraphQLView.as_view(graphiql=True)))
]

Testing our application at this point we should be able to create a user successfully.

Graphiql

Linking the SendGrid API

All we have done so far enables us to create a user but we are still to implement sending an email on registration and that’s what we’ll do next. We start off by adding some SendGrid email configs to the settings.py file.

# sendgrid settings
SENDGRID_API_KEY = os.getenv('SENDGRID_API_KEY')
EMAIL_HOST = os.getenv('EMAIL_HOST')
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = SENDGRID_API_KEY
EMAIL_PORT = os.getenv('EMAIL_PORT')
EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS')
DOMAIN = os.getenv("DOMAIN")

As you may have noticed the settings above are one of the reasons we set up the .env file earlier.

Creating the send_email function

We will create a file in our authentication folder named send_email.py. It will contain our send_confirmation_email function. Later on, we will use Django template language (DTL) for ease in passing variables as well as adding styling.
In authentication/send_email.py add:

# authentication/send_email.py
import jwt
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
from django.template.loader import render_to_string
from SendgridDjangoDemo.settings import SECRET_KEY, DOMAIN, SENDGRID_API_KEY
def send_confirmation_email(email, username):
    token = jwt.encode({'user': username}, SECRET_KEY,
                       algorithm='HS256').decode('utf-8')
    context = {
        'small_text_detail': 'Thank you for '
                             'creating an account. '
                             'Please verify your email '
                             'address to set up your account.',
        'email': email,
        'domain': DOMAIN,
        'token': token,
    }
    # locates our email.html in the templates folder
    msg_html = render_to_string('email.html', context)
    message = Mail(
        # the email that sends the confirmation email
        from_email='confirm_email@test.com',
        to_emails=[email],  # list of email receivers
        subject='Account activation',  # subject of your email
        html_content=msg_html)
    try:
        sg = SendGridAPIClient(SENDGRID_API_KEY)
        sg.send(message)
    except Exception as e:
        return str(e)

Having set up the function that will be sending our confirmation email we need to draw up the email template that our end-users will see.
Create a templates folder in the authentication folder proceed to add an HTML file therein named email.html, in it add:

authentication/templates/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>Email Verification</title>
   <style>
       .card {
           font-family: "Helvetica Neue";
           text-align: center;
           margin: auto;
           width: 40%;
       }
       .btn {
           padding: 7px;
           margin-bottom: 10px;
           background: #993CF3;
           color: #fff;
           border-radius: 5px;
       }
   </style>
</head>
<body>
    <div class="card">
        <div class="card-body">
            <p class="card-text text-center"><strong>{{ small_text_detail }}</strong></p>
            <p class="text-center">{{ email }}</p>
            <a class="text-center verify-button" href=" {{ domain }}{% url "activate"  token=token %}">
                <button type="button" class="btn">Verify Account</button>
            </a>
        </div>
    </div>
</body>
</html>

GraphQL But…

We put it some basic HTML to structure our email, we have also added some styling with CSS for presentability. Since we are using DTL we have access to variables in the HTML.

You will also notice that our link tag has a redirecting URL link, this will compel us to deviate a little from GraphQL in dealing with the redirection link from the email.

Due to GraphQl’s single endpoint feature, we will have to include some REST functionality to handle our email activation. We will start by defining the activation function which we will create in the views.py file in our authentication directory.

Add the code below in authentication/views.py.

import jwt
from django.shortcuts import redirect, render
from SendgridDjangoDemo.authentication.models import User
from SendgridDjangoDemo.settings import SECRET_KEY, DOMAIN
def activate_account(request, token):
    username = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])["user"]
    user = User.objects.get(username=username)
    if username and not user.is_verified:
        user.is_verified = True
        user.save()
        return redirect(f'{DOMAIN}/sendgriddemo/')

We should now update the registration mutation with the send_confirmation_email function. Our authentication/schema.py should resemble the code below

import graphene
from django.contrib.auth import authenticate
from graphene_django import DjangoObjectType
from graphql_jwt.utils import jwt_encode, jwt_payload
from SendgridDjangoDemo.authentication.models import User
from SendgridDjangoDemo.authentication.send_email import send_confirmation_email
#existing code remains
def mutate(self, info, **kwargs):
        user = User.objects.create_user(
            email=kwargs.get('email'),
            username=kwargs.get('username'),
        )
        user.set_password(kwargs.get('password'))
        user.save()
        send_confirmation_email(email=user.email, username=user.username)
        return CreateUser(user=user, message="Successfully created user, {}".format(user.username)

We can proceed to test whether everything we have covered so far works. I like carrying out intermittent testing of my work, just in case I slip along the way the recovery journey doesn’t become too horrifying.

Creating a user now should also send a verification email to the email used in registration. One more addition before we test. We need to add the activation link to the urls.py file:

from django.contrib import admin
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView
from SendgridDjangoDemo.authentication.views import activate_account
urlpatterns = [
    path('admin/', admin.site.urls),
    path('sendgriddemo/activate/<token>', activate_account, name='activate'),
    path('sendgriddemo/', csrf_exempt(GraphQLView.as_view(graphiql=True)))
]

On creating a new account you should receive below email to the email you used to register with:

verification email

The Final Touch

Now for the final touch to our API, we will add the login mutation that checks whether the email has been confirmed by checking the is_verified flag. Let’s get to it add additional code to the schema file inside the authentication folder to have the file looking like below:

import graphene
from django.contrib.auth import authenticate
from graphene_django import DjangoObjectType
from graphql_jwt.utils import jwt_encode, jwt_payload
from SendgridDjangoDemo.authentication.models import User
from SendgridDjangoDemo.authentication.send_email import send_confirmation_email
class UserType(DjangoObjectType):
    class Meta:
        model = User
class CreateUser(graphene.Mutation):
    message = graphene.String()
    user = graphene.Field(UserType)
    class Arguments:
        username = graphene.String()
        email = graphene.String(required=True)
        password = graphene.String(required=True)
    def mutate(self, info, **kwargs):
        user = User.objects.create_user(
            email=kwargs.get('email'),
            username=kwargs.get('username'),
        )
        user.set_password(kwargs.get('password'))
        user.save()
        send_confirmation_email(email=user.email, username=user.username)
        return CreateUser(user=user, message="Successfully created user, {}".format(user.username))
class LoginUser(graphene.Mutation):
    user = graphene.Field(UserType)
    message = graphene.String()
    token = graphene.String()
    verification_prompt = graphene.String()
    class Arguments:
        email = graphene.String()
        password = graphene.String()
    def mutate(self, info, **kwargs):
        email = kwargs.get('email')
        password = kwargs.get('password')
        user = authenticate(username=email, password=password)
        error_message = 'Invalid login credentials'
        success_message = "You logged in successfully."
        verification_error = 'Your email is not verified'
        if user:
            if user.is_verified:
                payload = jwt_payload(user)
                token = jwt_encode(payload)
                return LoginUser(token=token, message=success_message)
            return LoginUser(message=verification_error)
        return LoginUser(message=error_message)
class Mutation(graphene.ObjectType):
    create_user = CreateUser.Field()
    login_user = LoginUser.Field()

Let’s try logging in with a user who’s email is unverified. We get the error below

unverified error

Now let’s verify our email by clicking on the verify button and try again:

Login successful

You are successfully logged in and provided with a token for authentication

How about we write some minimal tests for our api authentication functionality?

In the tests.py file in authentication add:

import json
from django.test import TestCase
from django.core import mail
class AuthTestCase(TestCase):
    def query(self, query: str = None):
        # Method to run all queries and mutations for tests.
        body = dict()
        body['query'] = query
        response = self.client.post(
            "/sendgriddemo/", json.dumps(body),
            content_type='application/json')
        json_response = json.loads(response.content.decode())
        return json_response
    def test_user_can_register_for_account(self):
        register_user_query = '''
        mutation {{
          createUser(username:"{username}" email: "{email}", password: "{password}"){{
            user {{
              email
            }}
            message
          }}
        }}
        '''
        response = self.query(register_user_query.format(
            username="testuser",
            email="user@testuser.com",
            password="password123"
        ))
        data = response.get('data')
        self.assertEqual(data["createUser"]["message"],
                         "Successfully created user, testuser")
    def test_send_email(self):
        mail.send_mail(
            'Account Activation', 'Click below button to confirm your email',
            'confirm_test@test.com', ['user@testuser.com'],
            fail_silently=False,
        )
        # Test that one message has been sent.
        self.assertEqual(len(mail.outbox), 1)
        # Verify that the subject of the first message is correct.
        self.assertEqual(mail.outbox[0].subject, 'Account Activation')

Learning Strategy

I am a practitioner of doing it first and researching later. I however decided to implement a research-first methodology for this project.

Before I started working on this task I had no idea how to go about it. It took me close to an hour to settle for SendGrid and an hour more to research the best SendGrid Python/Django integration.

I got stuck a little on integrating the SendGrid API to my project and this particular package was very helpful. When it came to setting up the sendgrid email settings this article saved my life.

I drew up an implementation plan on how I would execute the implementation, I used pseudo-code and flow diagrams where necessary.

In retrospect….

Theoretical conciseness in planning for a project is key. I learnt this by deciding to take a research first methodology. It’s through the planning phase that I discovered I had to deviate from the GraphQL architecture to efficiently deal with the email confirmation URL we discussed earlier. This exposed the elasticity of GraphQL APIs, their ability to integrate with REST architecture in designing hybrid APIs.

The ability to use two architectures hand in hand was something I wasn’t sure was possible but now have a working understanding of, at least as far as REST and GraphQL are concerned. I firmly believe that mastery of this skill will make transitions from legacy architectures a painless endeavour. It also opens up the possibility of hybrid API designs that take advantage of the best of both worlds.

In conclusion

This particular project took longer than i had predicted. Having run a conservative estimate of 10 hours it took twice the time for completion.

I hope this short example helps somebody stuck as I was when I began. Understanding how to use SendGrid was very eye opening. This example can be built upon since the authentication layer of the API is all set up it can easily be integrated into a fully-fledged API.


The model we have defined could also be extended to include password reset emails as well as disseminating marketing emails for your web app.

Tools for further study

If you are looking to build on top of what we’ve already gone through, the repository is available here

Citations:

The Featured Image is courtesy of pixabay.com and can be found here

Hire the author: Maina K