Over the past couple of months, I have been building a web application in my spare time as a way to familiarize myself with the Django, Django REST Framework, and React stack. While working on the app, which I have called Oddjobbox, I came across some unique obstacles; specifically when it came to user management and authentication with JWT. Let’s discuss that.

Tech Stack

Before I dive deep into the code, I wanted to give some background information on the technology that I will be using. On the backend, I chose Django and the Django Rest Framework for developing a RESTful API that would be at the core of my app. On the frontend, I have decided to use React to make calls to the API. I will also be using Docker and Docker Compose for development. Finally, I will be using Postman to run tests and monitor the API as I go. This walkthrough moves very quickly and assumes some familiarity with Python, Django, and Docker.

Requirements

Before starting, you should have the following python dependencies installed:

django~=2.2
djangorestframework~=3.9
pyjwt~=1.7
gunicorn~=19.9 # Our WSGI server
psycopg2~=2.7 # Only if using Postgresql
sqlparse~=0.3
pytz~=2019.1

Since I am using Docker, you can get up and running with the following Dockerfile:

FROM python:3.7-alpine

WORKDIR /app

COPY requirements.txt .

RUN apk add --no-cache --virtual .build-deps \
  build-base postgresql-dev jpeg-dev zlib-dev \
    && pip install -r requirements.txt \
    && find /usr/local \
        \( -type d -a -name test -o -name tests \) \
        -o \( -type f -a -name '*.pyc' -o -name '*.pyo' \) \
        -exec rm -rf '{}' + \
    && runDeps="$( \
        scanelf --needed --nobanner --recursive /usr/local \
                | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \
                | sort -u \
                | xargs -r apk info --installed \
                | sort -u \
    )" \
    && apk add --virtual .rundeps $runDeps \
    && apk del .build-deps

COPY . .

CMD ["python3", "manage.py", "runserver", "0:8000"]

If you have a solution for making this image any smaller, go for it! In the meantime, this will suffice. Moving on…

Creating a User model

The Django user model is pretty straight forward. We will be inheriting from the AbstractBaseUser and the PermissionsMixin classes to create our model.

import jwt
from django.conf import settings
from django.core.validators import (validate_email, RegexValidator)
from django.contrib.auth.models import (AbstractBaseUser, PermissionsMixin, BaseUserManager)
from django.db import models
from django.utils import timezone


class User(AbstractBaseUser, PermissionsMixin):
    """
    Defines our custom user class.
    Username, email and password are required.
    """

    username = models.CharField(
        validators=[
            RegexValidator(
                regex=r"[\w]{6,25}",
                message=
                'Usernames must be between 6 and 25 characters long and can only contain letters, numbers, and underscores.'
            )
        ],
        db_index=True,
        max_length=25,
        unique=True,
    )

    email = models.EmailField(validators=[validate_email],
                              max_length=128,
                              unique=True)

    is_staff = models.BooleanField(default=False)

    is_active = models.BooleanField(default=True)

    # The `USERNAME_FIELD` property tells us which field we will use to log in.
    USERNAME_FIELD = 'username'
    EMAIL_FIELD = 'email'
    REQUIRED_FIELDS = ['email']

    # Tells Django that the UserManager class defined above should manage
    # objects of this type.
    objects = UserManager()

    def __str__(self):
        """
        Returns a string representation of this `User`.
        This string is used when a `User` is printed in the console.
        """
        return self.username

    @property
    def token(self):
        """
        Allows us to get a user's token by calling `user.token` instead of
        `user.generate_jwt_token().

        The `@property` decorator above makes this possible. `token` is called
        a "dynamic property".
        """
        return self._generate_jwt_token()

    def get_full_name(self):
        """
        This method is required by Django for things like handling emails.
        Typically this would be the user's first and last name. Since we do
        not store the user's real name, we return their username instead.
        """
        return self.username

    def get_short_name(self):
        """
        This method is required by Django for things like handling emails.
        Typically, this would be the user's first name. Since we do not store
        the user's real name, we return their username instead.
        """
        return self.username

    def _generate_jwt_token(self):
        """
        Generates a JSON Web Token that stores this user's ID and has an expiry
        date set to 60 days into the future.
        """
        exp = timezone.now() + timezone.timedelta(days=60)
        iat = timezone.now()
        token = jwt.encode(
            {
                "iat": int(iat.strftime("%s")),
                "id": self.pk,
                "exp": int(exp.strftime("%s")),
            },
            settings.SECRET_KEY,
            algorithm="HS256",
        )

        return token.decode("utf-8")

As you can see, the token is created dynamically with the @property decorator with an expiry of 60 days. The UserManager that will be called when creating a new user is as follows:

class UserManager(BaseUserManager):
    """
    Django requires that custom users define their own Manager class. By
    inheriting from `BaseUserManager`, we get a lot of the same code used by
    Django to create a `User`.

    All we have to do is override the `create_user` function which we will use
    to create `User` objects.
    """

    def _create_user(self, username, email, password=None, **extra_fields):
        if not username:
            raise ValueError('The given username must be set')

        if not email:
            raise ValueError('The given email must be set')

        email = self.normalize_email(email)
        user = self.model(username=username, email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)

        return user

    def create_user(self, username, email, password=None, **extra_fields):
        """
        Create and return a `User` with an email, username and password.
        """
        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)

        return self._create_user(username, email, password, **extra_fields)

    def create_superuser(self, username, email, password, **extra_fields):
        """
        Create and return a `User` with superuser (admin) permissions.
        """
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')

        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')

        return self._create_user(username, email, password, **extra_fields)

There is a lot of code that was dumped out just now but my goal is to get you up and running as soon as possible. I recommend reading through the code line by line to make sure you understand what is going on (this is generally a good idea whenever you are copying code from the internet!). The User class and UserManager are all you need to create a custom user in Django! Just don’t forget to let Django know that these models exist by declaring your app in the settings.py file:

INSTALLED_APPS = [
    ...
    'rest_framework',
    'authentication', # My'authentication` app
    ...
]

AUTH_USER_MODEL = 'authentication.User'

Authentication Backend

By default, Django does not know how to authenticate your JWTs. To fix this, we must the create the following backends.py file:

import jwt
from django.conf import settings
from rest_framework import (authentication, exceptions)
from .models import User


class JWTAuthentication(authentication.BaseAuthentication):
    authentication_header_prefix = 'Bearer'

    def authenticate(self, request):
        """
        The `authenticate` method is called on every request regardless of
        whether the endpoint requires authentication.

        `authenticate` has two possible return values:

        1) `None` - We return `None` if we do not wish to authenticate. Usually
                    this means we know authentication will fail. An example of
                    this is when the request does not include a token in the
                    headers.

        2) `(user, token)` - We return a user/token combination when
                             authentication is successful.

                            If neither case is met, that means there's an error
                            and we do not return anything.
                            We simple raise the `AuthenticationFailed`
                            exception and let Django REST Framework
                            handle the rest.
        """
        request.user = None

        # `auth_header` should be an array with two elements: 1) the name of
        # the authentication header (in this case, "Token") and 2) the JWT
        # that we should authenticate against.
        auth_header = authentication.get_authorization_header(request).split()
        auth_header_prefix = self.authentication_header_prefix.lower()

        if not auth_header:
            return None

        if len(auth_header) == 1:
            # Invalid token header. No credentials provided. Do not attempt to
            # authenticate.
            return None

        elif len(auth_header) > 2:
            # Invalid token header. The Token string should not contain spaces.
            # Do not attempt to authenticate.
            return None

        # The JWT library we're using can't handle the `byte` type, which is
        # commonly used by standard libraries in Python 3. To get around this,
        # we simply have to decode `prefix` and `token`. This does not make for
        # clean code, but it is a good decision because we would get an error
        # if we didn't decode these values.
        prefix = auth_header[0].decode('utf-8')
        token = auth_header[1].decode('utf-8')

        if prefix.lower() != auth_header_prefix:
            # The auth header prefix is not what we expected. Do not attempt to
            # authenticate.
            return None

        # By now, we are sure there is a *chance* that authentication will
        # succeed. We delegate the actual credentials authentication to the
        # method below.
        return self._authenticate_credentials(request, token)

    def _authenticate_credentials(self, request, token):
        """
        Try to authenticate the given credentials. If authentication is
        successful, return the user and token. If not, throw an error.
        """
        try:
            payload = jwt.decode(token, settings.SECRET_KEY)
        except:
            msg = 'Invalid authentication. Could not decode token.'
            raise exceptions.AuthenticationFailed(msg)

        try:
            user = User.objects.get(pk=payload['id'])
        except User.DoesNotExist:
            msg = 'No user matching this token was found.'
            raise exceptions.AuthenticationFailed(msg)

        if not user.is_active:
            msg = 'This user has been deactivated.'
            raise exceptions.AuthenticationFailed(msg)

        return (user, token)

Again, this is a lot of code being thrown out, but I like to think that it is fairly straight forward if you have some experience with Python and Django.

We must also remember to update our settings.py file to tell Django where to find our custom authentication backend:

...
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
        ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'authentication.backends.JWTAuthentication',
        )
}
...

By now you have created a custom User model and UserManager model, and created a custom JWTAuthentication class to authenticate your user tokens. The last piece missing is to setup your user views for DRF to handle.

DRF Serializers

There are a couple of views that need to be serialized to finally get up and running. The first one is the RegistrationSerializer

from rest_framework import serializers
from rest_framework.validators import UniqueValidator
from django.contrib.auth import authenticate
from django.contrib.auth.password_validation import validate_password
from .models import User

class RegistrationSerializer(serializers.Serializer):
    """
    Creates a new user.
    Email, username, and password are required.
    Returns a JSON web token.
    """

    # The password must be validated and should not be read by the client
    password = serializers.CharField(min_length=8,
                                     max_length=128,
                                     write_only=True)

    # The client should not be able to send a token along with a registration
    # request. Making `token` read-only handles that for us.
    token = serializers.CharField(read_only=True)

    # The unique validator enforces the unique contraint on our user model.
    username = serializers.CharField(
        min_length=6,
        max_length=25,
        write_only=True,
        validators=[
            UniqueValidator(
                queryset=User.objects.all(),
                message='This username is already associated with an account.')
        ])
    email = serializers.EmailField(
        max_length=128,
        write_only=True,
        validators=[
            UniqueValidator(
                queryset=User.objects.all(),
                message='This email is already associated with an account.')
        ])

    def validate(self, data):
        password = data.get('password', None)

        if password is not None:
            validate_password(password)
        else:
            raise serializers.ValidationError('A password is required.')

        return data

    def create(self, validated_data):
        return User.objects.create_user(**validated_data)

This serializer will receive a username, email, and password and will return a user token if authentication is successful. Next we need a way of logging existing users in. We will create a LoginSerializer for this:

class LoginSerializer(serializers.Serializer):
    """
    Authenticates an existing user.
    username and password are required.
    Returns a JSON web token.
    """
    username = serializers.CharField(write_only=True)
    password = serializers.CharField(write_only=True)
    token = serializers.CharField(read_only=True)

    def validate(self, data):
        """
        Validates user data.
        """
        email = data.get('email', None)
        password = data.get('password', None)

        if email is None:
            raise serializers.ValidationError(
                'An email address is required to log in.')

        if password is None:
            raise serializers.ValidationError(
                'A password is required to log in.')

        user = authenticate(username=email, password=password)

        if user is None:
            raise serializers.ValidationError(
                'A user with this email and password was not found.')

        if not user.is_active:
            raise serializers.ValidationError(
                'This user has been deactivated.')

        data['token'] = user.token

        return data

DRF Views

The login process will also return a user token, but only if the user has already been created. With these two serializers in place, we can move on to our views.py file. We simply need to include a view for registering and for logging in.

from rest_framework import status
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework import viewsets
from .serializers import (LoginSerializer, RegistrationSerializer)


class UserViewSet(viewsets.ViewSet):
    """
    The `auth` endpoint allows only specific actions including login, registration, etc.
    """

    @action(detail=False, methods=['post'], permission_classes=[AllowAny])
    def login(self, request):
        """
        Login existing `User`.
        """
        serializer = LoginSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        return Response(serializer.data, status=status.HTTP_200_OK)

    @action(detail=False, methods=['post'], permission_classes=[AllowAny])
    def registration(self, request):
        """
        Register a new `User` account.
        """
        serializer = RegistrationSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response(serializer.data, status=status.HTTP_201_CREATED)

The final step is to set up our urls.py file to map our views to a url.

from django.urls import re_path, include
from rest_framework.routers import DefaultRouter
from authentication.views import UserViewSet

router = DefaultRouter()
router.register('auth', UserViewSet, 'user')

urlpatterns = [
    re_path(r'', include(router.urls)),
]

Summary

With all these files created, we are now able to register and log in users using our custom Django models and successfully authenticate our users with JSON Web Tokens. While most of this information has been dumped on this page, I hope it has been helpful to those looking to do something similar.