Django REST Framework TokenAuthentication

Romeo Agbor Peter - Sep 16 '21 - - Dev Community

This article assumes you're familiar with Django and the basics of the Django REST framework web framework.

Implementing a token authentication in REST framework is not quite straightforward. After browsing the docs and scanning through blog posts, I found an "easy" way to implement the TokenAuthentication scheme.

Before delving in, it's best to explain just a bit what Django REST framework is, and some of its essential concepts.

Django REST Framework (DRF).

The Django REST framework is python framework for building decoupled and "API-first" web application. It allows you to convert your existing Django models to format that translated to and from JSON.

As the name suggests, DRF is built on top of Django framework. In fact, it was built to enable building REST API using the Django framework.

Key Concepts of DRF

Serializer

"Serializer" in DRF allow the serializing and deserializing Django model instances into different representation, such as json. This is done by creating a serializer.py file in the desired directory followed by the corresponding code responsible for doing the serialization.

View

Just as Django has the views.py file responsible for building logic and presenting data to the client in different forms. DRF builds on top that idea and provide its own views for presenting data. They are two ways to represent views in DRF: function-based views and class-based views.

In this post, I'll be using class-based views that'll inherit from REST framework's APIView.

Why the APIView?

  • It's optimize for content negotiation and setting the correct renderer on the response.
  • It catches all APIException exceptions and returns appropriate responses.
  • Incoming requests are inspected and appropriate permission and/or throttle checks will be run before dispatching the request to the handler method.

NOTE

Some developers prefer the naming the views file api.py. This is to keep separated from Django's views.py file.

Requests and responses

The REST framework provides a Response object that extends Django's HttpRequest. The extension allows it to provide a robust way of parsing requests. The Response object provide a request.data attribute that's similar to Django view's request.POST but its more suitable for building web APIs.

Routing

The REST framework has the routermodule for API URL routing to Django. It provides a consistent way of writing view logic to a set of URLs. To create routes for URLs, you create a router.py file in the desired directory and then include it in the Django URLconf (URL configuration).

The implementation in this post will not be using the REST framwork router, but Django's URLconf.

Token authentication

This is an HTTP authentication scheme that uses token as means for verifying and granting access to clients. Only clients with valid token granted access. A token is passed as a payload to the HTTP Authorization header for every request. The server receives the token and checks it with what it has stored. If the tokens match, then the client is verified and given access.

Installation and Configuration

To set up an isolated environment, you have use a virtual environment.

Installing Django & DRF

If the virtual environment is set up and activated, run pip install django djang-restframework in the terminal to install Django and DRF.

To start a project, run python django-admin startproject [project name] to start a new project. I'll be calling mine authentication.

Installed Apps

Django has to recognize and use the REST framework and the token authentication scheme. Add them to the list of installed apps in the settings.py file.

# Application definition

INSTALLED_APPS = [
    ...
    "rest_framework",
    "rest_framework.authtoken"
    ...

]
Enter fullscreen mode Exit fullscreen mode

Setting Authentication Class & Permission.

Still in the settings.py file, the default authentication scheme will be set to TokenAuthentication and permission to AllowAny. The AllowAny is a general setting that applies to all views of application, it allows the client to have access to all available views of the web app. However, this can be further streamlined to restrict certain views to authenticated users only, which you'll see as you move on.

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.TokenAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.AllowAny",
    ]
}
Enter fullscreen mode Exit fullscreen mode

Migration.

To apply changes that require database integration (Django comes with default SQLite), they have to be migrated. Run python manage.py migrate to apply such changes.

TokenAuthentication.

TokenAuthentication is differs from SessonAuthentication. No state is created on the server when using a token authentication scheme. Only the validity of the token is checked. If there's a match, the user is granted access, or more definitely, is authenticated.

Create User.

A user account is needed to test the implementations. Django's manage.py command utility can be used to create a super user account.

python manage.py createsuperuser --username johndoe --email johndoe@gmail.com
Enter fullscreen mode Exit fullscreen mode

Test View

Let's create a view with permission set only for authenticated users. Requests sent to the view with an invalid token won't be granted access. The view will handle a GET request that return a message of Hello, World! for valid tokens.

# views.py

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated 

class HelloView(APIView):
    permissions_classes = [IsAuthenticated] # <-------- Only authenticated users can access this view

    def get(self, request):
        context = {"message": "Hello, World!"} # <------ Response to the client
        return Response(context)
Enter fullscreen mode Exit fullscreen mode

Add url pattern and view to the URLconf file, urls.py

# urls.py
 from django.urlsconf import path
 from authentication.views import HelloView

urls_patterns = [
    path("/api-token-auth", HelloView.as_view())
]
Enter fullscreen mode Exit fullscreen mode

Let's test the view. A request without token will be sent.

Python has a lightweight HTTP client package called httpie. Install it and follow along.

..authentication> http http://localhost:8000/api-is-authenticated
HTTP/1.1 401 Unauthorized
Allow: GET, HEAD, OPTIONS
...
Content-Type: application/json
...
WWW-Authenticate: Token # <----------- Take note of this
...
{
    "detail": "Authentication credentials were not provided."
}

Enter fullscreen mode Exit fullscreen mode

From the above, the request wasn't authenticated because no [valid] token was associated with the request. Also, if you inspect the header, you'd notice the www-Authentication: Token payload. That is to tell the client that a token is needed to access data on the http://localhost:8000/api-token-auth URL. Later on, we'll test the view again, but with a valid token.

Registration

The registration authentication is responsible for handling registration and creating user account. JSON request from the client will hold the credentials required to create the account. If credentials are valid, the account will be created.

To check if data is valid, it has to cross-check with serialized model data.

Create a serializers.py file and write the following code:


# serializers.py

from django.contrib.auth.models import User

from rest_framework import serializers


class RegistrationSerializer(serializers.ModelSerializer):
     class Meta:
        model = User
        fields = [
            "id", 
            "username", 
            "first_name", 
            "last_name", 
            "email", 
            "password", 
            "is_active", 
            "is_staff"
        ]
        extra_kwargs = {"id": {"read_only": True}, "password": {"write_only": True}}
        read_only_fields

    def create(self, validated_data):

        username = validated_data["username"]
        first_name = validated_data["first_name"]
        last_name = validated_data["last_name"]
        email = validated_data["email"]
        password = validated_data["password"]

        user = User.objects.create_user(
            username=username,
            first_name=first_name,
            last_name=last_name,
            email=email,
            password=password,
        )

        return user
Enter fullscreen mode Exit fullscreen mode

For the view code to handle logic for incoming request. The request will be a POST request.

The view will pass incoming data to the RegistrationSerializer class for validating and creating the user. The serializer will return an object that tells if the data is valid or not. The view will check if data is valid. If valid, it'll save it, generate token, and return response. If not valid, it'll return error response.


# views.py

from django.contrib.auth.models import User

from authentication.serializers import RegistrationSerializer

from rest_framework import serializers
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.authtoken.models import Token
from rest_framework import permissions

class RegistrationView(APIView):
     """Registeration View"""

    def post(self, request, *args, **kwargs):
        """Handles post request logic"""
        registration_serializer  = RegistrationSerializer(data=request.data)

        # Generate tokens for existing users
        for user in User.objects.all():
            if not user:
                break
            else:
                try:
                    Token.objects.get(user_id=user.id)
                except Token.DoesNotExist:
                    Token.objects.create(user=user)

        if registration_class.is_valid():
            user = registration_serializer.save()
            token = Token.objects.create(user=user)

            return Response(
                {
                    "user": {
                        "id": serializer.data["id"],
                        "first_name": serializer.data["first_name"],
                        "last_name": serializer.data["last_name"],
                        "username": serializer.data["username"],
                        "email": serializer.data["email"],
                        "is_active": serializer.data["is_active"],
                        "is_staff": serializer.data["is_staff"],
                    },
                    "status": {
                        "message": "User created",
                        "code": f"{status.HTTP_200_OK} OK",
                    },
                    "token": token.key,
                }
            )
        return Response(
             {
                "error": serializer.errors,
                "status": f"{status.HTTP_203_NON_AUTHORITATIVE_INFORMATION} \ 
                    NON AUTHORITATIVE INFORMATION",
            }
        )
Enter fullscreen mode Exit fullscreen mode

next, create the URL pattern and add the view responsible for handling request to the URL.

# urls.py from django.urlsconf import path from authentication.views import RegistrationViewurls_patterns = [    #...    path("/api-register-auth", RegistrationView.as_view())]
Enter fullscreen mode Exit fullscreen mode

Lastly, test the RegistrationView . It should return the appropriate response with a token if required and valid credentials are provided.

http --json POST http://localhost:8000/api/auth/register username="JamesChe" email="jamesuche@gmail.com" first_name="james" last_name="uche" password="jamesuche123456"{    "status": {        "code": "200 OK",        "message": "User created"    },    "token": "b4784f96c3c65387bc8ea6463d5d4658cb32b0ac",    "user": {        "id": 2,        "first_name": "james",        "last_name": "uche",        "username": "JamesChe",        "email": "jamesuche@gmail.com",        "is_active": true,        "is_staff" true,    }}
Enter fullscreen mode Exit fullscreen mode

Login

Implementing Login authentication using the REST framework is quite straightforward. There are two way to achieve setting up a login view: using built-in view or a custom view.

Using obtain_auth_token view.

REST framework provides the built-in obtain_auth_token view for creating and retrieving tokens for authenticated user.

Clients can obtain token using registered credential. The username and password are what's required. Once the view confirms the credentials, It'll obtain the token if it exists or create a new one.

In the urls.py file, import as follows: from rest_framework.authtoken.views import obtain_auth_token then add the view to the URLconf.

# urls.pyfrom rest_framework.authtoken.views import obtain_auth_tokenurl_patterns + [    path("/api-token-auth", obtain_auth_token.as_views())]
Enter fullscreen mode Exit fullscreen mode

NOTE:

The naming of the URL pattern can be whatever you want.

To get token, the client have to send a post request to the URL. The client must provide the username and password of the registered user.

..authentication> http post http//localhost:800/api-token-auth/ username="johndoe" password=123456HTTP/1.1 200 OKAllow: OPTION, POSTSContent-Type: application/json......{  "token": "bad492c010451bcba6acf7437706b8dd30eb11d5"}
Enter fullscreen mode Exit fullscreen mode

Remember the earlier HelloView view? Let's test it by sending a GET request to its URL, this time with the token we just got.

..authentication> http http://localhost:8000/api-hello "Authorization: Token bad492c010451bcba6acf7437706b8dd30eb11d5"HTTP/1.1 200 OKAllow: GET, HEAD, OPTIONS.........{    "message": "Hello, World!" # <------------ Message because user token is valid.}
Enter fullscreen mode Exit fullscreen mode

And there! We're now able to access the view without restriction. the expected Hello, World! message is returned rather than a restriction message.

Using Custom View

Certain cases require extra data about the authenticated user be sent as response to the client. That "extra" data could be on a database. To retrieve such data, a serializer is needed to convert them to python native code format for easy serializing. The view will handle the necessary related logic and send response to the client.

Two serializer classes are needed for this: one to verify the user credentials and the other to return the necessary response.

# Serializers.pyfrom django.contrib.auth.models import Userfrom rest_framework import serializersclass UserLoginSerializer(serializers.Serializer):    """Login serializer"""        username = serializers.CharField(required=True)    password = serializers.CharField(required=True, read_only=True)class UserLoginResponse(serializers.ModelSerializer):    """Response serializer"""        class Meta:        model = User        fields = "id, username, first_name, last_name, email, password, is_active, is_staff"        read_only_fields = ["id", "password", "is_active", "is_staff"]
Enter fullscreen mode Exit fullscreen mode

Next, in the views.py file, the view has to check if the incoming data from the request is a valid data using the serializer. If it's not it'll return a serializer error. if the data is valid, the credentials will be authenticate and a response message with the generated or obtained token will be sent back to the client.

# views.pyfrom django.contrib.auth.models import Userfrom authentication.serializers import LoginSerializerfrom authentication.serializers import LoginResponseSerializerfrom rest_framework import serializersfrom rest_framework.views import APIViewfrom rest_framework.response import Responsefrom rest_framework import statusfrom rest_framework.authtoken.models import Tokenfrom rest_framework import permissionsfrom django.contrib.auth import authenticateclass LoginView(APIView):    """Login View"""        permissions_classes = [permissions.isAuthenticated]        def post(self, request, *args, **kwargs):        login_serializer = LoginSerializer(data=request.data)                if login_serializer.is_valid():            user = authenticate(request, **serializer.data)                        if user is not None:                response_class = LoginResponseSerializer(user)                token, created_token = Token.objects.get_or_create(user_id=user.id)                                if isinstance(created_token, Token):                    token = created_token.key                                return Response(                    {                        "user": response_serializer.data,                        "status": {                            "message": "User Authenticated",                            "code": f"{status.HTTP_200_OK} OK",                        },                        "token": token.key,                    }                )            else:                raise serializers.ValidationError(                {                    "error": {                        "message": "Invalid Username or Password. Please try again",                        "status": f"{status.HTTP_400_BAD_REQUEST} BAD REQUEST",                    }                }            )                return(            {                "error": serializer.errors,                "status": f"{status.HTTP_403_FORBIDDEN} FORBIDDEN"            }        )
Enter fullscreen mode Exit fullscreen mode

Create URL pattern and add view to URLConf file.

# urls.pyfrom django.urls import pathfrom authentication.views import LoginViewurlpatterns = [    #...    #...    path("/api-login-auth", views.LoginView),]
Enter fullscreen mode Exit fullscreen mode

To test out the custom view, send a POST request with the required user credentials as JSON.

..authentication> http --json post http//localhost:800/api-login-auth username="johndoe" password=123456HTTP/1.1 200 OKAllow: OPTION, POSTS...Content-Type: application/json......{    "status": {        "code": "200 OK",        "message": "User Authenticated"    },    "token": "bad492c010451bcba6acf7437706b8dd30eb11d5",    "user": {        "email": "johndoe@gmail.com",        "first_name": "",        "id": 1,        "is_active": true,        "is_staff": true,        "last_name": "",        "username": "johndoe"    }}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Implementing TokenAuthentication in Django REST framework can be steep at first. But it start to make sense when you understand the concept: Rather having a session on the server, a token is instead created and is used to verify the user on every request.

I recommend using the REST framework APIView classes-bases for easy work. Only when keep repeating yourself should you look into other class-based to reduce repetition.

If all the application needs is just the token, then use the built-in obtain_auth_token view for that.

Customs views may need extra data returned to the user, in this case you should create extra serializer classes to achieve that.

I'll appreciate your feedback on this post. Cheers!

. . . . . . . . . . . . . . . . . . .