JWT Authentication in Django

This tutorial will give an introduction to JSON Web Tokens (JWT) and how to implement JWT authentication in Django.

What Is JWT?

JWT is an encoded JSON string that is passed in headers to authenticate requests. It is usually obtained by hashing JSON data with a secret key. This means that the server doesn’t need to query the database every time to retrieve the user associated with a given token.

How JSON Web Tokens Work

When a user successfully logs in using their credentials, a JSON Web Token is obtained and saved in local storage. Whenever the user wants to access a protected URL, the token is sent in the header of the request. The server then checks for a valid JWT in the Authorization header, and if found, the user will be allowed access.

A typical content header will look like this:

Authorization:
Bearer eyJhbGciOiJIUzI1NiIsI

Below is a diagram showing this process:

How JSON Web Tokens WorkHow JSON Web Tokens WorkHow JSON Web Tokens Work

The Concept of Authentication and Authorization

Authentication is the process of identifying a logged-in user, while authorization is the process of identifying if a certain user has the right to access a web resource.

API Example

In this tutorial, we are going to build a simple user authentication system in Django JWT as the authentication mechanism.

Requirements

Let’s get started.

Create a directory where you will keep your project and also a virtual environment to install the project dependencies.

1
mkdir myprojects
2
3
cd myprojects
4
5
virtual venv
6

Activate the virtual environment:

1
source venv/bin/activate

Create a Django project.

1
django-admin startproject django_auth
2

Install DRF and django-rest-framework-jwt using pip.

1
pip install djangorestframework
2
pip install djangorestframework-jwt
3
pip install django

Let’s go ahead and add DRF to the list of installed apps in the settings.py file.

Configure the JWT Settings

In order to use a simple JWT, we need to configure django-rest-framework permissions to accept JSON Web Tokens.

In the settings.py file, add the following configurations:

1
REST_FRAMEWORK = {
2
  'DEFAULT_AUTHENTICATION_CLASSES': (
3
    'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
4
  ),
5
}

Create a new app called users which will handle user authentication and management.

1
cd django-auth
2
django-admin.py startapp users 

Add the users application to the list of installed apps in the settings.py file.

Setting Up the Database

We are going to use the PostgreSQL database because it’s more stable and robust.

Create the auth database and assign a user.

Switch over to the Postgres account on your machine by typing:

Access the Postgres prompt and create the database:

1
psql
2
postgres=# CREATE DATABASE auth;

Create a role:

1
postgres=# CREATE ROLE django_auth WITH LOGIN PASSWORD 'asdfgh'; 

Grant database access to the user:

1
postgres=# GRANT ALL PRIVILEGES ON DATABASE auth TO django_auth;

Install the psycopg2 package, which will allow us to use the database we configured:

Edit the currently configured SQLite database and use the Postgres database.

1
DATABASES = {
2
    'default': {
3
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
4
        'NAME': 'auth',
5
        'USER': 'django_auth',
6
        'PASSWORD': 'asdfgh',
7
        'HOST': 'localhost',
8
        'PORT': '',
9
    }
10
}

Creating Models

Django comes with a built-in authentication system which is very elaborate, but sometimes we need to make adjustments, and thus we need to create a custom user authentication system. Our user model will be inheriting from the AbstractBaseUser class provided by django.contrib.auth.models.

In users/models.py, we start by creating the User model to store the user details.

1
# users/models.py
2
from __future__ import unicode_literals
3
from django.db import models
4
from django.utils import timezone
5
from django.contrib.auth.models import (
6
    AbstractBaseUser, PermissionsMixin
7
)
8
9
class User(AbstractBaseUser, PermissionsMixin):
10
    """
11
    An abstract base class implementing a fully featured User model with
12
    admin-compliant permissions.
13

14
    """
15
    email = models.EmailField(max_length=40, unique=True)
16
    first_name = models.CharField(max_length=30, blank=True)
17
    last_name = models.CharField(max_length=30, blank=True)
18
    is_active = models.BooleanField(default=True)
19
    is_staff = models.BooleanField(default=False)
20
    date_joined = models.DateTimeField(default=timezone.now)
21
22
    objects = UserManager()
23
24
    USERNAME_FIELD = 'email'
25
    REQUIRED_FIELDS = ['first_name', 'last_name']
26
27
    def save(self, *args, **kwargs):
28
        super(User, self).save(*args, **kwargs)
29
        return self

REQUIRED_FIELDS contains all required fields on your user model, except the username field and password, as these fields will always be prompted for.

UserManager is the class that defines the create_user and createsuperuser methods. This class should come before the AbstractBaseUser class we defined above. Let’s go ahead and define it.

1
from django.contrib.auth.models import (
2
    AbstractBaseUser, PermissionsMixin, BaseUserManager
3
)
4
5
class UserManager(BaseUserManager):
6
7
    def _create_user(self, email, password, **extra_fields):
8
        """
9
        Creates and saves a User with the given email,and password.
10
        """
11
        if not email:
12
            raise ValueError('The given email must be set')
13
        try:
14
            with transaction.atomic():
15
                user = self.model(email=email, **extra_fields)
16
                user.set_password(password)
17
                user.save(using=self._db)
18
                return user
19
        except:
20
            raise
21
22
    def create_user(self, email, password=None, **extra_fields):
23
        extra_fields.setdefault('is_staff', False)
24
        extra_fields.setdefault('is_superuser', False)
25
        return self._create_user(email, password, **extra_fields)
26
27
    def create_superuser(self, email, password, **extra_fields):
28
        extra_fields.setdefault('is_staff', True)
29
        extra_fields.setdefault('is_superuser', True)
30
31
        return self._create_user(email, password=password, **extra_fields)

Migrations

Migrations provide a way of updating your database schema every time your models change, without losing data.

Create an initial migration for our users model, and sync the database for the first time.

1
python manage.py make migrations users
2

3
python manage.py migrate

Creating a Superuser

Create a superuser by running the following command:

1
python manage.py createsuperuser

Creating New Users

Let’s create an endpoint to enable registration of new users. We will start by serializing the User model fields. Serializers provide a way of changing data to a form that is easier to understand, like JSON or XML. Deserialization does the opposite, which is converting data to a form that can be saved to the database.

Create users/serializers.py and add the following code.

1
# users/serializers.py
2
from rest_framework import serializers
3
from.models import User
4
5
6
class UserSerializer(serializers.ModelSerializer):
7
8
    date_joined = serializers.ReadOnlyField()
9
10
    class Meta(object):
11
        model = User
12
        fields = ('id', 'email', 'first_name', 'last_name',
13
                  'date_joined', 'password')
14
        extra_kwargs = {'password': {'write_only': True}}

CreateUserAPIView

Next, we want to create a view so the client will have a URL for creating new users.

In users.views.py, add the following:

1
# users/views.py
2
class CreateUserAPIView(APIView):
3
    # Allow any user (authenticated or not) to access this url 
4
    permission_classes = (AllowAny,)
5
6
    def post(self, request):
7
        user = request.data
8
        serializer = UserSerializer(data=user)
9
        serializer.is_valid(raise_exception=True)
10
        serializer.save()
11
        return Response(serializer.data, status=status.HTTP_201_CREATED)

We set permission_classes to (AllowAny,) to allow any user (authenticated or not) to access this URL.

Configuring URLs

Create a file users/urls.py and add the URL to match the view we created. Also add the following code.

1
# users/urls.py
2
3
from django.conf.urls import url, patterns
4
from .views import CreateUserAPIView
5
6
urlpatterns = [
7
    url(r'^create/$', CreateUserAPIView.as_view()),
8
]

We also need to import URLs from the users application to the main django_auth/urls.py file. So go ahead and do that. We are using the include function here, so don’t forget to import it.

1
# django_auth/urls.py
2
from django.conf.urls import url, include
3
from django.contrib import admin
4
5
urlpatterns = [
6
    url(r'^admin/', admin.site.urls),
7
    url(r'^user/', include('users.urls', namespace='users')),
8
9
]

Now that we are done creating the endpoint, let’s do a test and see if we are on track. We will use Postman to do the tests. If you are not familiar with Postman, it’s a tool which presents a friendly GUI for constructing requests and reading responses.

Configuring URLsConfiguring URLsConfiguring URLs

As you can see above, the endpoint is working as expected.

Authenticating Users

We will make use of the Django-REST Framework JWT Python module we installed at the beginning of this tutorial. It adds a simple JWT authentication support for Django Rest Framework JWT apps.

But first, let’s define some configuration parameters for our tokens and how they are generated in the settings.py file.

1
# settings.py
2
import datetime
3
JWT_AUTH = {
4
5
    'JWT_VERIFY': True,
6
    'JWT_VERIFY_EXPIRATION': True,
7
    'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=3000),
8
    'JWT_AUTH_HEADER_PREFIX': 'Bearer',
9
10
}
  • JWT_VERIFY: It will raise a jwt.DecodeError if the secret is wrong.
  • JWT_VERIFY_EXPIRATION: Sets the expiration to True, meaning Tokens will expire after a period of time. The default time is five minutes.
  • JWT_AUTH_HEADER_PREFIX: The Authorization header value prefix that is required to be sent together with the token. We have set it as Bearer, and the default is JWT.

In users/views.py, add the following code.

1
@api_view(['POST'])
2
@permission_classes([AllowAny, ])
3
def authenticate_user(request):
4
5
    try:
6
        email = request.data['email']
7
        password = request.data['password']
8
9
        user = User.objects.get(email=email, password=password)
10
        if user:
11
            try:
12
                payload = jwt_payload_handler(user)
13
                token = jwt.encode(payload, settings.SECRET_KEY)
14
                user_details = {}
15
                user_details['name'] = "%s %s" % (
16
                    user.first_name, user.last_name)
17
                user_details['token'] = token
18
                user_logged_in.send(sender=user.__class__,
19
                                    request=request, user=user)
20
                return Response(user_details, status=status.HTTP_200_OK)
21
22
            except Exception as e:
23
                raise e
24
        else:
25
            res = {
26
                'error': 'can not authenticate with the given credentials or the account has been deactivated'}
27
            return Response(res, status=status.HTTP_403_FORBIDDEN)
28
    except KeyError:
29
        res = {'error': 'please provide a email and a password'}
30
        return Response(res)

In the code above, the login view takes username and password as input, and it then creates a token with the user information corresponding to the passed credentials as payload and returns it to the browser. Other user details such as name are also returned to the browser together with the token. This token will be used to authenticate in future requests.

The permission classes are set to allowAny since anyone can access this endpoint.

We also store the last login time of the user with this code.

1
user_logged_in.send(sender=user.__class__,
2
                                    request=request, user=user)

Every time the user wants to make an API request, they have to send the token in Auth Headers in order to authenticate the request.

Let’s test this endpoint with Postman. Open Postman and use the request to authenticate with one of the users you created previously. If the login attempt is successful, the response will look like this:

A sample responseA sample responseA sample response

Retrieving and Updating Users

So far, users can register and authenticate themselves. However, they also need a way to retrieve and update their information. Let’s implement this.

In users.views.py, add the following code.

1
class UserRetrieveUpdateAPIView(RetrieveUpdateAPIView):
2
3
    # Allow only authenticated users to access this url
4
    permission_classes = (IsAuthenticated,)
5
    serializer_class = UserSerializer
6
7
    def get(self, request, *args, **kwargs):
8
        # serializer to handle turning our `User` object into something that
9
        # can be JSONified and sent to the client.
10
        serializer = self.serializer_class(request.user)
11
12
        return Response(serializer.data, status=status.HTTP_200_OK)
13
14
    def put(self, request, *args, **kwargs):
15
        serializer_data = request.data.get('user', {})
16
17
        serializer = UserSerializer(
18
            request.user, data=serializer_data, partial=True
19
        )
20
        serializer.is_valid(raise_exception=True)
21
        serializer.save()
22
23
        return Response(serializer.data, status=status.HTTP_200_OK)

We first define the permission classes and set to IsAuthenticated since this is a protected URL and only authenticated users can access it. 

We then define a get method to retrieve user details. After retrieving user details, an authenticated user will then update their details as desired.

Update your URLs to define the endpoint as follows.

1
users/urls.py
2
from .views import CreateUserAPIView, UserRetrieveUpdateAPIView
3
4
urlpatterns = [
5
    
6
    url(r'^update/$', UserRetrieveUpdateAPIView.as_view()),
7
]

In order for the request to be successful, the headers should contain the JWT token as shown below.

Headers with the JWT TokenHeaders with the JWT TokenHeaders with the JWT Token

If you attempt to request a resource without the authentication header, you will get the following error.

An example errorAn example errorAn example error

If a user stays beyond the time specified in JWT_EXPIRATION_DELTA without making a request, the token will expire and they will have to request another token. This is also demonstrated below.

JWT_EXPIRATION_DELTA ExampleJWT_EXPIRATION_DELTA ExampleJWT_EXPIRATION_DELTA Example

Conclusion

This tutorial has covered what is necessary to successfully build a solid back-end authentication system with JSON Web Tokens. We also covered Django JWT authentication or Django authorization.

Check out more Python and Django JWT articles and tutorials: 

Leave a comment

Your email address will not be published.