Django
Building a Production Ready Django JSON API
  •  

Setting up JWT Authentication

Outline

Django comes with a session-based authentication system that works out of the box. It includes all of the models, views, and templates you need to let users log in and create a new account. Here's the rub though: Django's authentication only works with the traditional HTML request-response cycle.

What do we mean by "the traditional HTML request-response cycle"? Historically, when a user wanted to perform some action (such as creating a new account), the user would fill out a form in their web browser. When they clicked the "Submit" button, the browser would make a request — which included the data the user had typed into the registration form — to the server, the server would process that request, and it would respond with HTML or redirect the browser to a new page. This is what we mean when we talk about doing a "full page refresh."

Why is knowing that Django's built-in authentication only works with the traditional HTML request-response cycle important? Because the client we're building this API for does not adhere to this cycle. Instead, the client expects the server to return JSON instead of HTML. By returning JSON, we can let the client decide what it should do next instead of letting the server decide. With a JSON request-response cycle, the server receives data, processes it, and returns a response (just like in the HTML request-response cycle), but the response does not control the browser's behavior. It just tells us the result of the request.

Luckily, the team behind Django realized that the trend of web development was moving in this direction. They also knew that some projects might not want to use the built-in models, views, and templates. They may choose to use custom versions instead. To make sure all of the efforts that went into building Django's built-in authentication system wasn't wasted, they decided to make it possible to use the most important parts while maintaining the ability to customize the end result.

We will dive into this later in the chapter. For now, here's what you want to know:

  1. We will be creating our own User model to replace Django's.
  2. We will have to write our views to support returning JSON instead of HTML.
  3. Because we won't be using HTML, we have no need for Django's built-in login and register templates.

If you're wondering what's left for us to use, that's a fair question. This goes back to what we talked about earlier about Django making it possible to use the core parts of authentication without using the default authentication system.

Read Using the Django authentication system to learn more about how Django's default authentication works.
Read Customizing authentication in Django to get a better idea about what lies ahead in the rest of this chapter. We will return to this page later.

Session-based authentication

By default, Django uses sessions for authentication. Before going further, we should talk about what this means, why it's important, what token-based authentication and JSON Web Tokens (JWTs for short) are, and which one we'll be using in this course.

In Django, sessions are stored as cookies. These sessions, along with some built-in middleware and request objects, ensure that there is a user available on every request. The user can be accessed as request.user. When the user is logged in, request.user is an instance of the User class. When they're logged out, request.user is an instance of the AnonymousUser class. Whether the user is authenticated or not, request.user will always exist.

What's the difference? Put simply, anytime you want to know if the current user is authentication, you can use request.user.is_authenticated() which will return True if the user is authenticated and False if they aren't. If request.user is an instance of AnonymousUser, request.user.is_authenticated() will always return False. This allows the developer (you!) to turn if request.user is not None and request.user.is_authenticated(): into if request.user.is_authenticated():. Less typing is a good thing in this case!

In our case, the client and the server will be running at different locations. The server will be running at http://localhost:3000/ and the client will be at http://localhost:5000/. The browser considers these two locations to be on different domains, similar to running the server on http://www.server.com and running the client on http://www.client.com. We will not be allowing external domains access to our cookies, so we have to find another alternative solution to using sessions.

If you're wondering why we won't be allowing access to our cookies, you should check out the articles on Cross-Origin Resource Sharing (CORS), and Cross-Site Request Forgery (CSRF) linked below. If you just want to start coding, check the boxes and move on.

Read HTTP access control (CORS) to learn more about Cross-Origin Resource Sharing (CORS).
Read Cross Site Request Forgery protection to learn more about how Django protects against CSRF attacks.

Token-based authentication

The most common alternative to session-based authentication is token-based authentication, and we will be using a specific form of token-based authentication to secure our application.

With token-based auth, the server provides the client with a token upon a successful login request. This token is unique to the user logging in and is stored in the database along with the user's ID. The client is expected to send the token with future requests so the server can identify the user. The server does this by searching the database table containing all of the tokens that have been created. If a matching token is found, the server goes on to verify that the token is still valid. If no matching token is found, then we say the user is not authenticated.

Because tokens are stored in the database and not in cookies, token-based authentication will suit our needs.

Verifying tokens

We always have the option of storing more than just the user's ID with their token. We can also store things such as a date on which the token will expire. In this example we would need to make sure that this expiration has not passed. If it has, then the token is not valid. So we delete it from the database and ask the user to log in again.

JSON Web Tokens

JSON Web Token (JWT for short) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between two parties. You can think of JWTs as authentication tokens on steroids.

Remember when I said we'll be using a specific form of token-based authentication? JWTs are what I was referring to.

Read Introduction to JSON Web Tokens to learn more about what JWTs are and how they work.

Why are JSON Web Tokens better than regular tokens?

There are a few benefits we get when going with JWTs over regular tokens:

  1. JWT is an open standard. That means that all implementations of JWT should be fairly similar, which is a benefit when working with different languages and technologies. Regular tokens are more free-form, allowing the developer to decide how best to implement the tokens.
  2. JWTs can contain all of the information about the user, which is convenient for the client.
  3. Libraries handle the heavy lifting here. Rolling out your own authentication is dangerous, so we leave the important stuff to battle-tested libraries that we can trust.

Creating the User model

How about we get started?

The file conduit/apps/authentication/models.py stores the models we will use for authentication. If you cloned the repository earlier in the course, you'd notice that the directory conduit/apps/authentication/ already exists. However, the file models.py does not. You'll want to create this file yourself.

Our explanations of the code throughout this course is intentionally short. Between the comments in the code and the resources we link to, we're confident that you can find the information you need.

Create conduit/apps/authentication/models.py.

We will need the following imports to create the User and UserManager classes, so go ahead and add the following to the top of the file:

import jwt

from datetime import datetime, timedelta

from django.conf import settings
from django.contrib.auth.models import (
    AbstractBaseUser, BaseUserManager, PermissionsMixin
)
from django.db import models

When customizing authentication in Django, one requirement is that you specify a custom Manager class with two methods: create_user and create_superuser. To learn about custom authentication in Django, read Substituting a custom User model.

Let's start by creating the UserManager class.

Type the code for the UserManager class into conduit/apps/authentication/models.py and take note of the comments:

Read Managers to learn what a Manager class does.

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):
        """Create and return a `User` with an email, username and password."""
        if username is None:
            raise TypeError('Users must have a username.')

        if email is None:
            raise TypeError('Users must have an email address.')

        user = self.model(username=username, email=self.normalize_email(email))
        user.set_password(password)
        user.save()

        return user

    def create_superuser(self, username, email, password):
        """
        Create and return a `User` with superuser (admin) permissions.
        """
        if password is None:
            raise TypeError('Superusers must have a password.')

        user = self.create_user(username, email, password)
        user.is_superuser = True
        user.is_staff = True
        user.save()

        return user

Now that we have the manager class, we can create the User model.

Add the User model to the bottom of conduit/apps/authentication/models.py.
class User(AbstractBaseUser, PermissionsMixin):
    # Each `User` needs a human-readable unique identifier that we can use to
    # represent the `User` in the UI. We want to index this column in the
    # database to improve lookup performance.
    username = models.CharField(db_index=True, max_length=255, unique=True)

    # We also need a way to contact the user and a way for the user to identify
    # themselves when logging in. Since we need an email address for contacting
    # the user anyways, we will also use the email for logging in because it is
    # the most common form of login credential at the time of writing.
    email = models.EmailField(db_index=True, unique=True)

    # When a user no longer wishes to use our platform, they may try to delete
    # their account. That's a problem for us because the data we collect is
    # valuable to us and we don't want to delete it. We
    # will simply offer users a way to deactivate their account instead of
    # letting them delete it. That way they won't show up on the site anymore,
    # but we can still analyze the data.
    is_active = models.BooleanField(default=True)

    # The `is_staff` flag is expected by Django to determine who can and cannot
    # log into the Django admin site. For most users this flag will always be
    # false.
    is_staff = models.BooleanField(default=False)

    # A timestamp representing when this object was created.
    created_at = models.DateTimeField(auto_now_add=True)

    # A timestamp reprensenting when this object was last updated.
    updated_at = models.DateTimeField(auto_now=True)

    # More fields required by Django when specifying a custom user model.

    # The `USERNAME_FIELD` property tells us which field we will use to log in.
    # In this case we want it to be the email field.
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username']

    # 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.email

    @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.
        """
        dt = datetime.now() + timedelta(days=60)

        token = jwt.encode({
            'id': self.pk,
            'exp': int(dt.strftime('%s'))
        }, settings.SECRET_KEY, algorithm='HS256')

        return token.decode('utf-8')

Glance over the above snippet for a few minutes and then do the following:

If you want to know more about custom authentication in Django, the docs are a great place to learn. The following links are optional, but we recommend you check them out if interested:

Specifying the AUTH_USER_MODEL setting

By default, Django assumes that the user model is django.contrib.auth.models.User. We want to use our own custom User though. Since we've created the User class, the next thing we need to do is to tell Django to use our User model instead of using its own.

It is highly recommended that you take a second to read Substituing a customer User model; especially the "Warning" sections.

If you've already migrated your database before specifying the custom User model, you may need to delete your database and re-run your migrations.

Tell Django about our User model by specifying the AUTH_USER_MODEL setting in conduit/settings.py.

To set the AUTH_USER_MODEL setting, type the following at the bottom of conduit/settings.py:

# Tell Django about the custom `User` model we created. The string
# `authentication.User` tells Django we are referring to the `User` model in
# the `authentication` module. This module is registered above in a setting
# called `INSTALLED_APPS`.
AUTH_USER_MODEL = 'authentication.User'

Creating and running migrations

As we add new models and change existing models, we will need to update the database to reflect these changes. Migrations are what Django uses to tell the database that something has changed, and ours will tell the database that we need to add a new table for our custom User model.

**Note: **If you have run python manage.py makemigrations or python manage.py migrate, you'll need to delete your database before continuing. If you're using SQLite as your database, then all you need to do is delete the file called db.sqlite3 in the root directory of your project.

If you're using PostgreSQL, then follow the instructions below:

If you have run python manage.py makemigrations or python manage.py migrate already, please delete the db.sqlite3 file from the root directory of your project. Django gets unhappy if you change AUTH_USER_MODEL after creating the database and it's best to just drop the database and start anew.

Now you're ready to create and apply migrations. After that, we can create our first user.

To create migrations, you'll need to run the following command in your console to create migrations:

python manage.py makemigrations

This creates the default migrations for our new Django project. However, it will not create migrations for new apps inside of our project. The first time we want to create migrations for a new app, we have to be more explicit about it.

To create a set of migrations for the authentication app, run the following:

python manage.py makemigrations authentication

This will create the initial migration for the authentication app. In the future, whenever you want to generate migrations for the authentication app, you only need to run python manage.py makemigrations.

Make the default migrations by running python manage.py makemigrations.
Create the initial migrations for the authentication app by running python manage.py makemigrations authentication.

We can now apply them by running the following command:

python manage.py migrate

Unlike the makemigrations command, you never need to specify the app to be migrated when running the migrate command.

Apply the newly create migrations by running make migrate.

Our first user

We've created our User model, and our database is up and running. The next thing to do is to create our first User object. We'll make this user a superuser since we'll be using it to test our site.

Create your first user by running the following command:

python manage.py createsuperuser

Django will ask you a few questions such as your email, username, and password. Once answered, your new user will be created. Congratulations!

Create your first user by running python manage.py createsuperuser.

To test that the user has been created, let's start by opening a Django shell from the command line:

python manage.py shell_plus

If you've used Django before, you may be familiar with the shell command, but not with shell_plus. shell_plus is provided by a library called django-extensions, which has been included in the boilerplate project you cloned before starting this course. It's useful because it automatically imports the models for each app in the INSTALLED_APPS setting. It can also be set up to import other utilities automatically.

Once the shell is open, run the following:

NOTE: Only type what appears on lines preceded by >>>. Lines that are not preceded by >>> are output from the previous command.

>>> user = User.objects.first()
>>> user.username
‘james'
>>> user.token
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0Njk0MDY2OTksImlkIjoxfQ.qSnwWVD4PJKhKxgLxY0H5mkTE51QnMWv_kqNJVau1go'

If all went well, you should see output similar to the above.

Registering new users

At the moment a user can't do anything interesting. Our next task is to create an endpoint for registering new users.

RegistrationSerializer

Create conduit/apps/authentication/serializers.py.

Start by creating conduit/apps/authentication/serializers.py and typing the following code:

from rest_framework import serializers

from .models import User


class RegistrationSerializer(serializers.ModelSerializer):
    """Serializers registration requests and creates a new user."""

    # Ensure passwords are at least 8 characters long, no longer than 128
    # characters, and can not be read by the client.
    password = serializers.CharField(
        max_length=128,
        min_length=8,
        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(max_length=255, read_only=True)

    class Meta:
        model = User
        # List all of the fields that could possibly be included in a request
        # or response, including fields specified explicitly above.
        fields = ['email', 'username', 'password', 'token']

    def create(self, validated_data):
        # Use the `create_user` method we wrote earlier to create a new user.
        return User.objects.create_user(**validated_data)

Read through the code, paying special attention to the comments, and then move on when you're done.

Read about ModelSerializer.

In the code above, we created a class RegistrationSerializer that inherits from serializers.ModelSerializer. serializers.ModelSerializer is just an abstraction on top of serializers.Serializer, which you probably remember from the Django REST Framework (DRF) tutorial. ModelSerializer handles a few things relevant to serializing Django models for us, so we don't have to.

Another thing to point out is that it allows you to specify two methods: create and update. In the above example we wrote our own create method using User.objects.create_user, but we did not specify the update method. In this case, DRF will use it's own default update method to update a user.

RegistrationAPIView

We can now serialize requests and responses for registering a user. Next, we want to create a view to use as an endpoint, so the client will have a URL to hit to create a new user.

Create conduit/apps/authentication/views.py and type the following:

from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView

from .serializers import RegistrationSerializer


class RegistrationAPIView(APIView):
    # Allow any user (authenticated or not) to hit this endpoint.
    permission_classes = (AllowAny,)
    serializer_class = RegistrationSerializer

    def post(self, request):
        user = request.data.get('user', {})

        # The create serializer, validate serializer, save serializer pattern
        # below is common and you will see it a lot throughout this course and
        # your own work later on. Get familiar with it.
        serializer = self.serializer_class(data=user)
        serializer.is_valid(raise_exception=True)
        serializer.save()

        return Response(serializer.data, status=status.HTTP_201_CREATED)

Let's talk about a couple of new things in this snippet:

  1. The permission_classes property is how we decide who can use this endpoint. We can restrict this to authenticated users or admin users. We can also allow users who are authenticated or not based on whether this endpoint they're hitting is "safe" — that means the endpoint is a GET, HEAD, or OPTIONS request. For this course, you only need to know about GET requests. We will talk more about permissions_classes later on.
  2. The create serializer, validate serializer, save serializer pattern you see inside the post method is very common when using DRF. You'll want to familiarize yourself with this pattern as you will be using it a lot.
Create conduit/apps/authentication/views.py.
Read up on Django Rest Framework's (DRF) Permissions.

Now we need to get the routing for our project set up. There were some changes introduced from Django 1.x => 2.x switching from url to path. There are quite a few questions on adapting the old regular expression url to the new way of doing things in a path. Considerate Code created a URL to Path Cheatsheet and write-up on the change. Some unique circumstances might necessitate the need for using re_path and using regular expressions.

Create conduit/authentication/urls.py and place the following into the file to handle our routes for this application:

from django.urls import path

from .views import RegistrationAPIView

app_name = 'authentication'
urlpatterns = [
    path('users/', RegistrationAPIView.as_view()),
]

If you're coming from another framework like Rails, it is common to put all of your routes in a single file. While you can do this in Django, it is considered best practice to create modular app specific paths. This forces you to think about your app design and keeping them self contained and reusable. That's what we've done here. We've also specified app_name = 'authentication' so that we can use an include and keep our application modular. Now let's include the above file in our global URLs file.

Create conduit/apps/authentication/urls.py.

Open conduit/urls.py and you'll see the following line near the top of the file:

from django.urls import path

The first thing we want to do is import a method called include from django.urls:

-from django.urls import path
+from django.urls import include, path

The include method let's include another urls.py file without having to do a bunch of work like importing and then re-registering the routes in this file.

Further down you'll see the following:

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

Let's update this to include our new urls.py file:

urlpatterns = [
    path('admin/', admin.site.urls),
+   path('api/', include('conduit.apps.authentication.urls', namespace='authentication')),
]
Include the URLs from the authentication app in the main urls.py file.

Registering a user with Postman

Now that we've created the User model and added an endpoint for registering new users, we're going to run a quick sanity check to make sure we're on track. To do this, we're going to use a tool called Postman with a pre-made collection of endpoints.

If you've never used Postman before, check out our Testing Conduit Apps Using Postman guide.

Open Postman and use the "Register" request inside the "Auth" folder to create a new user.

Make your first Postman request.

Awesome! We're making some real progress now!

There is one thing we need to fix though. Notice how the response from the "Register" request has all of the user's information at the root level. Our client expects this information to be namespaced under "user." To do that, we'll need to create a custom DRF renderer.

Rendering User objects

Create a file called conduit/apps/authentication/renderers.py and type the following content:

import json

from rest_framework.renderers import JSONRenderer


class UserJSONRenderer(JSONRenderer):
    charset = 'utf-8'

    def render(self, data, media_type=None, renderer_context=None):
        # If we receive a `token` key as part of the response, it will be a
        # byte object. Byte objects don't serialize well, so we need to
        # decode it before rendering the User object.
        token = data.get('token', None)

        if token is not None and isinstance(token, bytes):
            # Also as mentioned above, we will decode `token` if it is of type
            # bytes.
            data['token'] = token.decode('utf-8')

        # Finally, we can render our data under the "user" namespace.
        return json.dumps({
            'user': data
        })

Create conduit/apps/authentication/renderers.py.

There's nothing new or interesting happening here, so just read through the comments in the snippet and then we can move on.

Now open conduit/apps/authentication/views.py and import UserJSONRenderer by adding the following line to the top of your file:

from .renderers import UserJSONRenderer

You'll also need to set the renderer_classes property of the RegistrationAPIView class like so:

class RegistrationAPIView(APIView):
    permission_classes = (AllowAny,)
+    renderer_classes = (UserJSONRenderer,)
    serializer_class = RegistrationSerializer

    # … Other things
Update RegistrationAPIView to use the UserJSONRenderer renderer class.

With UserJSONRenderer in place, go ahead and use the "Register" Postman request to create a new user. Notice how this time the response is inside the "user" namespace.

Register a new user with Postman.

Logging users in

Since users can now register for Conduit, we need to build a way for them to log into their account. In this lesson, we will add the serializer and view needed for users to log in. We will also start looking at how our API should handle errors.

LoginSerializer

Open conduit/apps/authentication/serializers.py and add the following import to the top of the file:

from django.contrib.auth import authenticate

After that, create the following serializer in the same file:

class LoginSerializer(serializers.Serializer):
    email = serializers.CharField(max_length=255)
    username = serializers.CharField(max_length=255, read_only=True)
    password = serializers.CharField(max_length=128, write_only=True)
    token = serializers.CharField(max_length=255, read_only=True)

    def validate(self, data):
        # The `validate` method is where we make sure that the current
        # instance of `LoginSerializer` has "valid". In the case of logging a
        # user in, this means validating that they've provided an email
        # and password and that this combination matches one of the users in
        # our database.
        email = data.get('email', None)
        password = data.get('password', None)

        # Raise an exception if an
        # email is not provided.
        if email is None:
            raise serializers.ValidationError(
                'An email address is required to log in.'
            )

        # Raise an exception if a
        # password is not provided.
        if password is None:
            raise serializers.ValidationError(
                'A password is required to log in.'
            )

        # The `authenticate` method is provided by Django and handles checking
        # for a user that matches this email/password combination. Notice how
        # we pass `email` as the `username` value since in our User
        # model we set `USERNAME_FIELD` as `email`.
        user = authenticate(username=email, password=password)

        # If no user was found matching this email/password combination then
        # `authenticate` will return `None`. Raise an exception in this case.
        if user is None:
            raise serializers.ValidationError(
                'A user with this email and password was not found.'
            )

        # Django provides a flag on our `User` model called `is_active`. The
        # purpose of this flag is to tell us whether the user has been banned
        # or deactivated. This will almost never be the case, but
        # it is worth checking. Raise an exception in this case.
        if not user.is_active:
            raise serializers.ValidationError(
                'This user has been deactivated.'
            )

        # The `validate` method should return a dictionary of validated data.
        # This is the data that is passed to the `create` and `update` methods
        # that we will see later on.
        return {
            'email': user.email,
            'username': user.username,
            'token': user.token
        }
Add the new LoginSerializer class to conduit/apps/authentication/serializers.py.

With the serializer in place, let's move on to creating the view.

LoginAPIView

Open conduit/apps/authentication/views.py and update the following import:

-from .serializers import RegistrationSerializer
+from .serializers import (
+    LoginSerializer, RegistrationSerializer
+)

Then add the new login view:

class LoginAPIView(APIView):
    permission_classes = (AllowAny,)
    renderer_classes = (UserJSONRenderer,)
    serializer_class = LoginSerializer

    def post(self, request):
        user = request.data.get('user', {})

        # Notice here that we do not call `serializer.save()` like we did for
        # the registration endpoint. This is because we don't  have
        # anything to save. Instead, the `validate` method on our serializer
        # handles everything we need.
        serializer = self.serializer_class(data=user)
        serializer.is_valid(raise_exception=True)

        return Response(serializer.data, status=status.HTTP_200_OK)
Add the new LoginAPIView class to conduit/apps/authentication/views.py.

Open conduit/apps/authentication/urls.py and update the following import:

-from .views import RegistrationAPIView
+from .views import LoginAPIView, RegistrationAPIView

And add a new rule to the urlpatterns list:

urlpatterns = [
    url(r'^users/?$', RegistrationAPIView.as_view()),
+    url(r'^users/login/?$', LoginAPIView.as_view()),
]
Add a URL pattern for LoginAPIView to conduit/apps/authentication/urls.py.

Logging a user in with Postman

At this point, a user should be able to log in by hitting the new login endpoint. Let's test this out. Open Postman and use the "Login" request to log in with one of the users you created previously. If the login attempt was successful, the response will include a token that can be used in the future when making requests that require the user be authenticated.

Log in a user using Postman.

There is something else we need to handle here. Try using the "Login" request to log in with an invalid email/password combination. Notice the error response. There are two problems with this.

First of all, non_field_errors sounds strange. Usually, this key corresponds to whatever field caused the serializer to fail validation. Since we overrode the validate method, instead of a field-specific method such as validate_email, the Django REST Framework didn't know what field to attribute the error. The default is non_field_errors, and since our client will be using this key to display errors, we're going to change this to say error.

Secondly, the client expects any errors to be namespaced under the errors key in a JSON response, similar to how we namespaced the login and register requests under the user key. We will accomplish this by overriding Django REST Framework's (DRF) default error handling.

Overriding EXCEPTION_HANDLER and NON_FIELD_ERRORS_KEY

One of DRF's settings is called EXCEPTION_HANDLER that returns a dictionary of errors. We want our errors namespaced under the errors key, so we're going to have to override EXCEPTION_HANDLER. We will also override NON_FIELD_ERRORS_KEY as mentioned earlier.

Let's start by creating conduit/apps/core/exceptions.py and adding the following snippet:

from rest_framework.views import exception_handler

def core_exception_handler(exc, context):
    # If an exception is thrown that we don't explicitly handle here, we want
    # to delegate to the default exception handler offered by DRF. If we do
    # handle this exception type, we will still want access to the response
    # generated by DRF, so we get that response up front.
    response = exception_handler(exc, context)
    handlers = {
        'ValidationError': _handle_generic_error
    }
    # This is how we identify the type of the current exception. We will use
    # this in a moment to see whether we should handle this exception or let
    # Django REST Framework do its thing.
    exception_class = exc.__class__.__name__

    if exception_class in handlers:
        # If this exception is one that we can handle, handle it. Otherwise,
        # return the response generated earlier by the default exception 
        # handler.
        return handlers[exception_class](exc, context, response)

    return response

def _handle_generic_error(exc, context, response):
    # This is the most straightforward exception handler we can create.
    # We take the response generated by DRF and wrap it in the `errors` key.
    response.data = {
        'errors': response.data
    }

    return response
Create conduit/apps/core/exceptions.py with the above code.

With that taken care of, open up conduit/settings.py and add a new setting to the bottom of the file called REST_FRAMEWORK, like so:

REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'conduit.apps.core.exceptions.core_exception_handler',
    'NON_FIELD_ERRORS_KEY': 'error',
}
Add the REST_FRAMEWORK dict to conduit/settings.py with the EXCEPTION_HANDLER and NON_FIELD_ERRORS_KEY keys as above.

This is how we override settings in DRF. We will add one more settings in a bit when we start writing views that require the user to be authenticated.

Let's try sending another login request using Postman. Be sure to use an email/password combination that is invalid.

Use Postman to log in with an invalid username and password. Make sure the response is formatted as we discussed earlier.

Updating UserJSONRenderer

Uh oh! Still not quite what we want. We've got the errors key now, but everything is namespaced under the user key. That's not good.

Let's update UserJSONRenderer to check for the errors key and do things a bit differently. Open up conduit/apps/authentication/renderers.py and make these changes:

class UserJSONRenderer(JSONRenderer):
    charset = 'utf-8'

    def render(self, data, media_type=None, renderer_context=None):
+        # If the view throws an error (such as the user can't be authenticated
+        # or something similar), `data` will contain an `errors` key. We want
+        # the default JSONRenderer to handle rendering errors, so we need to
+        # check for this case.
+        errors = data.get('errors', None)

        # If we receive a `token` key in the response, it will be a
        # byte object. Byte objects don't serializer well, so we need to
        # decode it before rendering the User object.
        token = data.get('token', None)

+        if errors is not None:
+            # As mentioned above, we will let the default JSONRenderer handle
+            # rendering errors.
+            return super(UserJSONRenderer, self).render(data)

        if token is not None and isinstance(token, bytes):
            # We will decode `token` if it is of type
            # bytes.
            data['token'] = token.decode('utf-8')

        # Finally, we can render our data under the "user" namespace.
        return json.dumps({
            'user': data
        })
Update UserJSONRenderer in conduit/apps/authentication/renderers.py to handle the case where data contains an errors key.

Now send the login request with Postman one more time and all should be well.

Use Postman to log in with an invalid username and password. Make sure the response has a users key instead of an errors key.

Retrieving and updating users

Users can register new accounts and log into those accounts. Now users will need a way to retrieve and update their information. Let's implement this before we move on to creating user profiles.

UserSerializer

We're going to create one more serializer for the profile. We've got serializers for login and register requests, but we need to be able to serializer user objects too.

Open conduit/apps/authentication/serializers.py and add the following:

class UserSerializer(serializers.ModelSerializer):
    """Handles serialization and deserialization of User objects."""

    # Passwords must be at least 8 characters, but no more than 128 
    # characters. These values are the default provided by Django. We could
    # change them, but that would create extra work while introducing no real
    # benefit, so lets just stick with the defaults.
    password = serializers.CharField(
        max_length=128,
        min_length=8,
        write_only=True
    )

    class Meta:
        model = User
        fields = ('email', 'username', 'password', 'token',)

        # The `read_only_fields` option is an alternative for explicitly
        # specifying the field with `read_only=True` like we did for password
        # above. The reason we want to use `read_only_fields` here is that
        # we don't need to specify anything else about the field. The
        # password field needed the `min_length` and 
        # `max_length` properties, but that isn't the case for the token
        # field.
        read_only_fields = ('token',)


    def update(self, instance, validated_data):
        """Performs an update on a User."""

        # Passwords should not be handled with `setattr`, unlike other fields.
        # Django provides a function that handles hashing and
        # salting passwords. That means
        # we need to remove the password field from the
        # `validated_data` dictionary before iterating over it.
        password = validated_data.pop('password', None)

        for (key, value) in validated_data.items():
            # For the keys remaining in `validated_data`, we will set them on
            # the current `User` instance one at a time.
            setattr(instance, key, value)

        if password is not None:
            # `.set_password()`  handles all
            # of the security stuff that we shouldn't be concerned with.
            instance.set_password(password)

        # After everything has been updated we must explicitly save
        # the model. It's worth pointing out that `.set_password()` does not
        # save the model.
        instance.save()

        return instance
Add the new UserSerializer class to conduit/apps/authentication/serializers.py.

It's worth pointing out that we do not explicitly define the create method in the serializer since DRF provides a default create method for all instances of serializers.ModelSerializer. It's possible to create a user with this serializer, but we want the creation of a User to be handled by RegistrationSerializer.

UserRetrieveUpdateAPIView

Open up conduit/apps/authentication/views.py and update the imports like so:

from rest_framework import status
+from rest_framework.generics import RetrieveUpdateAPIView
-from rest_framework.permissions import AllowAny
+from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

from .renderers import UserJSONRenderer
from .serializers import (
-   LoginSerializer, RegistraitonSerializer
+    LoginSerializer, RegistrationSerializer, UserSerializer,
)

Below the imports, create a new view called UserRetrieveUpdateAPIView:

class UserRetrieveUpdateAPIView(RetrieveUpdateAPIView):
    permission_classes = (IsAuthenticated,)
    renderer_classes = (UserJSONRenderer,)
    serializer_class = UserSerializer

    def retrieve(self, request, *args, **kwargs):
        # There is nothing to validate or save here. Instead, we just want the
        # serializer to handle turning our `User` object into something that
        # can be JSONified and sent to the client.
        serializer = self.serializer_class(request.user)

        return Response(serializer.data, status=status.HTTP_200_OK)

    def update(self, request, *args, **kwargs):
        serializer_data = request.data.get('user', {})

        # Here is that serialize, validate, save pattern we talked about
        # before.
        serializer = self.serializer_class(
            request.user, data=serializer_data, partial=True
        )
        serializer.is_valid(raise_exception=True)
        serializer.save()

        return Response(serializer.data, status=status.HTTP_200_OK)
Add the new UserRetrieveUpdateAPIView class to conduit/apps/authentication/views.py.

Now go over to conduit/apps/authentication/urls.py and update the imports to include UserRetrieveUpdateAPIView:

-from .views import LoginAPIView, RegistrationAPIView
+from .views import (
+    LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView
+)

And add a new route to urlpatterns:

urlpatterns = [
+    path('user', UserRetrieveUpdateAPIView.as_view()),
    path('users', RegistrationAPIView.as_view()),
    path('users/login', LoginAPIView.as_view()),
]
Add a URL pattern for UserRetrieveUpdateAPIView to conduit/apps/authentication/urls.py.

Open Postman again and send the "Current User" request. You should get an error with a response that looks like this:

{
  "user": {
    "detail": "Authentication credentials were not provided."
  }
}
Use Postman to request data on the current user. You should get an error saying Authentication credenetials were not provided.

Authenticating Users

Django has this idea of authentication backends. Without going into too much detail, a backend is essentially a plan for deciding whether a user is authenticated. We'll need to create a custom backend to support JWT since this isn't supported by Django nor Django REST Framework(DRF) by default.

Create and open conduit/apps/authentication/backends.py and add the following code:

import jwt

from django.conf import settings

from rest_framework import authentication, exceptions

from .models import User


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

    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)
Create conduit/apps/authentication/backends.py

There is a lot of logic and exceptions being thrown in this file, but the code is pretty straight forward. All we've done is list conditions where the user would not be authenticated and throw an exception if any of those conditions are true.

There isn't any extra reading to do here, but feel free to check out the docs for the PyJWT library if you're interested.

Telling DRF about our authentication backend

We must explicitly tell Django REST Framework which authentication backend we want to use, similar to how we told Django to use our custom User model.

Open up conduit/settings.py and update the REST_FRAMEWORK dict with a new key:

REST_FRAMEWORK = {
     [...]
+
+    'DEFAULT_AUTHENTICATION_CLASSES': (
+        'conduit.apps.authentication.backends.JWTAuthentication',
+    ),
}
Register our JWT authentication backend with Django REST Framework by adding DEFAULT_AUTHENTICATION_CLASSES to the REST_FRAMEWORK setting in conduit/settings.py.

Retrieving and updating users with Postman

Now that our new authentication backend is in place, the authentication error we saw a while ago should be gone. Test this by opening Postman and sending another "Current User" request. The request should be successful, and you should see the information about your user in the response.

Use Postman to request the current user. This time the request should be successful.

Remember that we created an update endpoint at the same time we created the retrieve endpoint. Let's test this one too. Send the request labeled "Update User" in the "Auth" folder in Postman. If you used the default body, then the email of your user should have changed. Feel free to play around with these requests to make changes to your user.

Use Postman to update the current user. Play around with this request to make changes to your user.

On to better things

That's all there is for this chapter. We've created a user model and serialized users in three different ways. There are four shiny new endpoints that let users register, login, and retrieve and update their information. We're off to a strong start here!

Next up we're going to create profiles for our users. You may have noticed that the User model is pretty bare-bones. We only included things essential for authentication. Other information such as a biography and avatar URL will go in the Profile model that we'll build out in the next chapter.

 

I finished! On to the next chapter