Introduction on Securing Django APIs

December 16, 2020

In this tutorial, we will secure our TODO API endpoints that we previously created in this article. We will start by implementing Token-based authentication and then implement Javascript web tokens (JWT).

Token-based authentication

Token-based authentication works by getting a token for the correct username and password used to perform subsequent requests to the server.

Code setup

Add 'rest_framework.authtoken' to the apps list in django_todo settings.py file.

# ./django_todo/settings.py
...

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'todo',
    'rest_framework',
    'coreapi',
]

Then add 'rest_framework.authentication.TokenAuthentication' to the REST_FRAMEWORK dictionary in the django_todo project settings.py.

REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ],
}

Run the command ./manage.py migrate to create the table that will store the authentication tokens.

 $ ./manage.py migrate

We need to create a user and test the token generation if it works as expected. To create a user for testing, we use a manage.py command-line utility.

Run the command:

python manage.py createsuperuser --username paul --email paul@gmail.com

Project setup

Since this is a continuation of our previous article, we will be using the Django_todo application that we created there.

If you don’t have the application we created in the previous article, you can clone it from here.

To secure our endpoint using a token, we will add permission_classes to our view classes in views.py file in the todo app directory.

from rest_framework.generics import CreateAPIView
from rest_framework.generics import DestroyAPIView
from rest_framework.generics import ListAPIView
from rest_framework.generics import UpdateAPIView
from rest_framework.permissions import IsAuthenticated # new import

from todo.models import Todo
from todo.serializers import TodoSerializer


# Create your views here.
class ListTodoAPIView(ListAPIView):
    """This endpoint list all of the available todos from the database"""
    permission_classes = (IsAuthenticated,) #permission classes
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer


class CreateTodoAPIView(CreateAPIView):
    """This endpoint allows for creation of a todo"""
    permission_classes = (IsAuthenticated,)#permission classes
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer


class UpdateTodoAPIView(UpdateAPIView):
    """This endpoint allows for updating a specific todo by passing in the id of the todo to update"""
    permission_classes = (IsAuthenticated,)#permission classes
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer


class DeleteTodoAPIView(DestroyAPIView):
    """This endpoint allows for deletion of a specific Todo from the database"""
    permission_classes = (IsAuthenticated,)#permission classes
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer

Adding permission classes to our view classes will make our API endpoints more secure. When we visit http://127.0.0.1:8000/api/v1/todo/ we get HTTP 403 forbidden error.

Implementing the token authentication

In the settings.py file in our django_todo projects directory add rest_framework.authtoken to the INSTALLED_APPS list and also include TokenAuthentication to the REST_FRAMEWORK dictionary.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'todo',
    'rest_framework',
    'rest_framework.authtoken',# <-- authtoken
    'coreapi',
]

REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',  # <-- TokenAuthentication
    ],
}

Now we should run the command ./manage.py migrate to create the table that will store the authentication tokens.

$ ./manage.py migrate

To create an authentication token, we need a user account that we can create through the command line.

Run the command below to create a user account:

$ ./manage.py createsuperuser --username paul --email paul@domain.com

We can also generate the authentication token through the command line. Run the command python manage.py drf_create_token paul to generate the authentication token.

$ ./manage.py drf_create_token paul
Generated token 342b58233e5fdeb2446bcaae60b6e51e953f7a17 for user paul

We will be using the generated token 342b58233e5fdeb2446bcaae60b6e51e953f7a17 to authenticate our requests to the server.

Let’s make a request to http://127.0.0.1:8000/api/v1/todo/ adding our token as an authorization header Authorization: Token 342b58233e5fdeb2446bcaae60b6e51e953f7a17.

GET Request with token

User token endpoint

The Django rest framework comes with an endpoint that users can use to generate their authentication tokens by providing their valid username and password.

In the django_todo project directory add the API endpoint for token generation in the urls.py file.

from django.contrib import admin
from django.urls import path
from django.urls import include
from rest_framework.authtoken.views import obtain_auth_token
from rest_framework.documentation import include_docs_urls
from rest_framework_simplejwt import views as jwt_views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/todo/', include("todo.urls")),
    path('docs/', include_docs_urls(title='Todo Api')),
    path('api/token', obtain_auth_token, name="auth_token")
]

Making a POST request to http://127.0.0.1:8000/api/token/ with a valid username and password returns an authentication token in the response body that can be used to authenticate subsequent requests.

POST Request

Implementing the JSON web token JWT authentication

How JWT works

JWT is an access token acquired by passing in username and password for a refresh token and access token. Access token has a short lifespan (usually 5 minutes) while refresh token has a longer lifespan (usually 24 hours).

Sample JWT:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTQzODI4NDMxLCJqdGkiOiI3ZjU5OTdiNzE1MGQ0NjU3OWRjMmI0OTE2NzA5N2U3YiIsInVzZXJfaWQiOjF9.Ju70kdcaHKn1Qaz8H42zrOYk0Jx9kIckTn9Xx7vhikY

JWT consists of 3 parts:

header.payload.signature

In the JWT above, we have:

header = eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
payload = eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTQzODI4NDMxLCJqdGkiOiI3ZjU5OTdiNzE1MGQ0NjU3OWRjMmI0OTE2NzA5N2U3YiIsInVzZXJfaWQiOjF9
signature = Ju70kdcaHKn1Qaz8H42zrOYk0Jx9kIckTn9Xx7vhikY

The information above is encoded using a Base64 encoder.

After decoding the information above, we get:

Header

{
  "typ": "JWT",
  "alg": "HS256"
}

Payload

{
  "token_type": "access",
  "exp": 1543828431,
  "jti": "7f5997b7150d46579dc2b49167097e7b",
  "user_id": 5
}

Signature

JWT provides the signature. The signature is verified whenever a request is made to the server. If the client’s information in the header or payload is tempered, then the signature will be invalidated. We will be using djangorestframework_simplejwt to implement JWT authenticate.

We will be using djangorestframework_simplejwt to implement JWT authenticate.

To install run the command:

pip install djangorestframework_simplejwt

In the settings.py file in the django_todo applications directory, add rest_framework_simplejwt.authentication.JWTAuthentication to the DEFAULT_AUTHENTICATION_CLASSES in the REST_FRAMEWORK dictionary.

REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
    'DEFAULT_AUTHENTICATION_CLASSES': [
        # 'rest_framework.authentication.TokenAuthentication',  # <-- Token Authentication
        'rest_framework_simplejwt.authentication.JWTAuthentication',  # <-- JWT Authentication
    ],
}

In the urls.py file in the django_todo directory add the URL endpoints below to obtain the refresh and access tokens.

from django.contrib import admin
from django.urls import path
from django.urls import include
from rest_framework.authtoken.views import obtain_auth_token
from rest_framework.documentation import include_docs_urls
from rest_framework_simplejwt import views as jwt_views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/todo/', include("todo.urls")),
    path('docs/', include_docs_urls(title='Todo Api')),
    path('api/token', obtain_auth_token, name="auth_token"),
    path('api/jwt/token/', jwt_views.TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/jwt/token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'),
]

Obtaining Token To get the access and refresh tokens, make a POST request to http://127.0.0.1:8000/api/jwt/token/ passing in username and password.

POST JWT Request

We get a refresh and access token as the response.

{
    "access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTQ1MjI0MjU5LCJqdGkiOiIyYmQ1NjI3MmIzYjI0YjNmOGI1MjJlNThjMzdjMTdlMSIsInVzZXJfaWQiOjF9.D92tTuVi_YcNkJtiLGHtcn6tBcxLCBxz9FKD3qzhUg8",
    "refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU0NTMxMDM1OSwianRpIjoiMjk2ZDc1ZDA3Nzc2NDE0ZjkxYjhiOTY4MzI4NGRmOTUiLCJ1c2VyX2lkIjoxfQ.rA-mnGRg71NEW_ga0sJoaMODS5ABjE5HnxJDb0F8xAo"
}

To access the protected endpoints in our backend, we should include the access token in the header of all of our requests.

Postman image

We can use the access token within 5 minutes before it expires. After that we will need to obtain another access token using the refresh token we got from the previous API request.

When we try to make requests to protected endpoints, we will get the error below.

GET Request

To get a new access token, we will make a post request to http://127.0.0.1:8000/api/jwt/token/refresh/ posting the refresh token.

POST Request

The refresh token is valid for 24 hours, after that a user is required to reauthenticate in order to obtain a new refresh and access token.

The access token is shortlived because it’s sent through the HTTP header, which might get compromised; therefore, it’s only valid for a short while.

For additional customization, visit Django simple jwt

Happy Coding!


Peer Review Contributions by: Lalithnarayan C


About the author

Odhiambo Paul

Odhiambo Paul is a second-year undergraduate student who develops Python, Java and Android applications. Paul has a great passion for writing clean and optimized code.

This article was contributed by a student member of Section's Engineering Education Program. Please report any errors or innaccuracies to enged@section.io.