Demystifying Token-Based Authentication using Django REST Framework

Authentication is one of those things which have now been considered a rote and repetitive task when doing web development. Most applications you will ever develop almost always need to have some form of user authentication to allow users access the app’s functionality.

Generally speaking, all authentication you will do goes through these steps:

  1. Get the details sent in by the user (Parse JSON or use form data)
  2. Search for the user from the user’s table on the column identified with the unique field value passed in by the user (This is usually either username or e-mail address).
  3. Get the first record from the above query. (This is usually done because we assume there should be no duplicates on the unique field we’re searching on)
  4. Hash the password using the same algorithms to hash passwords at register phase (Done before saving the user object).
  5. Check if the password value matches the password field on the user’s table.
  6. If true, login user (Add to the session or generate a token). Redirect to the main app page.
  7. If false, return the user to the login page. Send error messages.

These must look pretty accurate to any developer who has done this before. Implementing authentication using DRF is done the same way. The approach, however, may differ from what you’re used to especially if you’re coming from a more expressive programming language.

 The User Model

Django provides a User model and a number of methods to help with things like authentication and session management.

To ensure we get our hands as dirty as possible, however, we will be creating our own user model. Our model will be inheriting from the AbstractBaseUser class available in django.contrib.auth.models. We shall then add our extra features ontop of that.

Create a new app called authentication. This app will handle everything that has to do with user authentication and management.

python manage.py createapp authentication

In authentication/models.py, we start by creating the Account model for storing user details and generally working with user objects.

from django.db import models
from django.contrib.auth.models import AbstractBaseUser

class Account(AbstractBaseUser):
    username = models.CharField(unique=True, max_length=50)
    email = models.EmailField(unique=True)

    firstname = models.CharField(max_length=100, blank=True)
    lastname = models.CharField(max_length=100, blank=True)

    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

    is_admin = models.BooleanField(default=False)

    objects = AccountManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username']

What we’ve done here is to create a model to handle all our user management. I’ll summarize what happened in the code snippet above:

As you can see, we linked the objects parameter of this model to AccountManager. This Manager class will have methods such as create_user() and create_superuser() and we shall be overriding the logic of these methods.

We will now add the code for the AccountManager class above the Account model class (This ensures that the model class can find the manager class).

from django.contrib.auth.models import BaseUserManager

class AccountManager(BaseUserManager):
    def create_user(self, email, password=None, **kwargs):
        # Ensure that an email address is set
        if not email:
            raise ValueError('Users must have a valid e-mail address')

        # Ensure that a username is set
        if not kwargs.get('username'):
            raise ValueError('Users must have a valid username')

        account = self.model(
            email=self.normalize_email(email),
            username=kwargs.get('username'),
            firstname=kwargs.get('firstname', None),
            lastname=kwargs.get('lastname', None),
        )

        account.set_password(password)
        account.save()

        return account

    def create_superuser(self, email, password=None, **kwargs):
        account = self.create_user(email, password, kwargs)

        account.is_admin = True
        account.save()

        return account

As you can see above, the AccountManager class inherits from the BaseUserManager class. We implemented two major methods we shall be using later in our code:

We’re making progress!

To ensure Django knows that we want to use a model different from the default User model, we will add this line to settings.py

AUTH_USER_MODEL = 'authentication.Account'

This way, especially for the purpose of our token generation view, we are able to tell Django to use our model whenever anything to do with authentication is required.

A lot of Django can be magical at times and like many other modern frameworks relies on configuration. In our case, by configuring the AUTH_USER_MODEL, USERNAME_FIELD and set_password() logic, Django is able to use this information to perform authenticate and login operations using the right database table.

 Authenticating the User

We will be making use of Django-REST Framework JWT, a Python module that adds JWT authentication support for DRF apps. Using this package, we can easily implement JWT Based Authentication for app without doing so much.

A lot has already been done for us and with little configuration, we are up and running.

Run the following to get the DRF JWT installed:

pip install djangorestframework-jwt

After installing the package, the next thing to do is to add the login route to our urls.py in the authentication app.

from django.conf.urls import url
from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
    url(r'^login/', obtain_jwt_token),
]

The obtain_jwt_token view provided by DRF JWT handles authenticating the user and sending us a token if the user is properly logged. The token is returned in the following format:

{
    token: "SOME_TOKEN_STRING"
}

We can specify different configuration parameters for our Tokens and how they are generated in the settings.py file. Some options we can configure are shown below:

# Default JWT preferences
JWT_AUTH = {
    'JWT_ENCODE_HANDLER':
    'rest_framework_jwt.utils.jwt_encode_handler',

    'JWT_DECODE_HANDLER':
    'rest_framework_jwt.utils.jwt_decode_handler',

    'JWT_PAYLOAD_HANDLER':
    'rest_framework_jwt.utils.jwt_payload_handler',

    'JWT_PAYLOAD_GET_USER_ID_HANDLER':
    'rest_framework_jwt.utils.jwt_get_user_id_from_payload_handler',

    'JWT_RESPONSE_PAYLOAD_HANDLER':
    'rest_framework_jwt.utils.jwt_response_payload_handler',

    'JWT_SECRET_KEY': SECRET_KEY,
    'JWT_ALGORITHM': 'HS256',
    'JWT_VERIFY': True,
    'JWT_VERIFY_EXPIRATION': True,
    'JWT_LEEWAY': 0,
    'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
    'JWT_AUDIENCE': None,
    'JWT_ISSUER': None,

    'JWT_ALLOW_REFRESH': False,
    'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),

    'JWT_AUTH_HEADER_PREFIX': 'JWT',
}

The settings shown are the default provided by the package. We can change things like Token Expiration date, Secret Key or Encryption algorithm using these options. See more details about the different options in the docs.

We will need to link this with the main application urls.py. To do this, we include it with a parent route pattern:

from django.conf.urls import include, url
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    url(r'^api/v1/auth/', include('authentication.urls')),
]

Doing this, all the URLs in authentication.urls will be prefixed with /api/v1/auth. Hence, the login route we created will be accessed using /api/v1/auth/login.

That’s all for the login part of things

 Registering the User

Registering a user is also pretty straightforward. We will need to create the URL and then the view function to handle it.

We will start by creating the serializers we’re going to be using to work with the request data and our models. The serializers perform easy conversion between types and provides an API to help us work with them from a higher level.

The serializer for the Account model will be created using the ModelSerializer class. Using this class, we can automatically create serializers from Django Models. The syntax for doing this is shown below.

from django.contrib.auth import update_session_auth_hash

from rest_framework import serializers

from .models import Account


class AccountSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True, required=True)
    confirm_password = serializers.CharField(write_only=True, required=True)

    class Meta:
        model = Account
        fields = (
            'id', 'email', 'username', 'date_created', 'date_modified',
            'firstname', 'lastname', 'password', 'confirm_password')
        read_only_fields = ('date_created', 'date_modified')

The code snippet does a number of things. Firstly, since we are creating a subclass of serializers.ModelSerializer, we can specify a model within the class. Besides specifying the model, you will also see here that the fields parameter was set to a tuple of fields represented as strings. This does a number of things but I will paint a couple of scenarios to give you a god idea of what is going on.

It is important to note here that, because we are using the ModelSerializer class, the types of the fields specified are set based on their types in the model. Options like required=True on model fields would also set validation conditions when this data is passed from the client.

Examining the code a bit further, we notice that, we have specified some fields as read_only_fields. As you would guess, these fields will be viewable when the model object is being transformed to JSON but will not be required when doing a Create or Update action.

The final thing to note is the manual creation of the password and confirm_password fields. As you can see, they are required but also set as write_only. What this means is that when the data is being converted from model instance to JSON, the fields will not be displayed. On the other hand, when you are doing any write operations, like Create and Update, the fields will be required.

We will add some more logic in the AccountSerializer class to give us a bit more flexibility when working with it.

from django.contrib.auth import update_session_auth_hash

from rest_framework import serializers

from .models import Account


class AccountSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True, required=True)
    confirm_password = serializers.CharField(write_only=True, required=True)

    class Meta:
        model = Account
        fields = (
            'id', 'email', 'username', 'date_created', 'date_modified',
            'firstname', 'lastname', 'password', 'confirm_password')
        read_only_fields = ('date_created', 'date_modified')

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

    def update(self, instance, validated_data):
        instance.email = validated_data.get('email', instance.email)
        instance.username = validated_data.get('username',
                                               instance.username)
        instance.firstname = validated_data.get('firstname',
                                                instance.firstname)
        instance.lastname = validated_data.get('lastname',
                                               instance.lastname)

        password = validated_data.get('password', None)
        confirm_password = validated_data.get('confirm_password', None)

        if password and password == confirm_password:
            instance.set_password(password)

        instance.save()
        return instance

    def validate(self, data):
        '''
        Ensure the passwords are the same
        '''
        if data['password']:
            print "Here"
            if data['password'] != data['confirm_password']:
                raise serializers.ValidationError(
                    "The passwords have to be the same"
                )
        return data

I will briefly explain what each method does.

That’s it for the serializer!

Next up, we create the view logic for the registration process. This would involve getting the data from the user and creating a new account object using that information.

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

from .serializers import AccountSerializer
from .models import Account


class AuthRegister(APIView):
    """
    Register a new user.
    """
    serializer_class = AccountSerializer
    permission_classes = (AllowAny,)

    def post(self, request, format=None):
        serializer = self.serializer_class(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

We have made use of the APIView. Using this we are able to write our view functions as classes. Similar to Django’s CBVs, we have created a post() method to handle POST transactions to the URL. In the post logic, as you can see, we created a serializer object using the form data and then called the is_valid() function to validate the fields against the serialzer. If this data is valid we proceed to save the object and return a success message.

As you would guess, the save() method is automatically calling the serializer create() method.

An error status is returned if there’s an error validating.

We will now be creating the route for this function in the authentication.urls module. Our file should now look like this.

from django.conf.urls import include, url
from .views import AuthLogin, AuthRegister
from rest_framework_jwt.views import obtain_jwt_token, refresh_jwt_token, verify_jwt_token

urlpatterns = [
    url(r'^login/', obtain_jwt_token),
    url(r'^token-refresh/', refresh_jwt_token),
    url(r'^token-verify/', verify_jwt_token),
    url(r'^register/$', AuthRegister.as_view()),
]

I have included some other Rest Framework JWT views for dealing with expired token or verifying tokens. All these URLs will be prefixed with /api/v1/auth/ as expected.

That’s it! We’re done with the authentication system. You can now create and run your migrations to get started. After this, you can then test the API thoroughly using POSTMAN.

Watch out for the next article.

UPDATE
I got a number of complaints about the code snippets in this article not working. I have uploaded a working version of the app which I was using for this demonstration here. Feel free to compare your code with what’s in there.

If you still have questions, feel free to send me an e-mail at chidiebere.nnadi@gmail.com.

Cheers.

 
329
Kudos
 
329
Kudos

Now read this

An aweful much of self improvement

I really do not know what happened to me last year but I happened to be extremely determined to change (and I mean mostly improve) in the new year. I had spent a number of weeks at home during the holidays and I had time to think such as... Continue →