Blog (or) Scribble Pad

Customising Django Rest Framework (DRF) Token Based Authentication

In this project we are going to generate REST API’s for a mobile client which are protected by token based authentication. Unlike normal token based authentication provided by DRF, we are not going to use the django default User model for generating and authenticating the tokens. Here, I don’t know why but our client wants to generate tokens based on the device mac address and authenticate them by the same. So, in order to achieve this authentication we are going to use the authtoken app in DRF which is responsible for token based authentication.

Ok, lets start a project,

$ django-admin startproject projectname

Download the Django Rest Framework source code from Github

$ git clone https://github.com/tomchristie/django-rest-framework/

Now copy the authtoken app from django-rest-framework/rest_framwork/ to your project folder

$ cp -r django-rest-framework/rest_framwork/authtoken /path/to/my/django/project/

Lets create an app called device which is the main app,

$ python manage.py startapp device

then create the basic model for the device,

# device/models.py
from django.db import models
# for token based auth, creating tokens automatically for all users
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from authtoken.models import Token

class Device(models.Model):
    """
       Device model - device app

       fields: mac_address, device_description

       table to store the device details
    """
    mac_address = models.CharField(max_length=45)
    device_description = models.CharField(max_length=64)

    def __unicode__(self):
        return u"%s" % (self.mac_address)

@receiver(post_save, sender=Device)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if created:
        Token.objects.create(device=instance)

Here the create_auth_token function will create an authentication token for every device that will be created, automatically by using signals.

Now, I’m going to customise the Token based authentication to use device details (Device model) for token generation and validation instead of User model.

(myproject/authtoken)$ vim models.py
# authtoken/models.py
import binascii
import os
from django.utils import timezone

from django.conf import settings
from django.db import models
from django.utils.encoding import python_2_unicode_compatible

@python_2_unicode_compatible
class Token(models.Model):
    """
    The authorization token model based on Mac Address (not user)
    """
    key = models.CharField(max_length=40, primary_key=True)
    device = models.OneToOneField('device.Device', related_name='auth_token')
    created = models.DateTimeField(auto_now_add=True)

    def save(self, *args, **kwargs):
        if not self.key:
            self.key = self.generate_key()
        return super(Token, self).save(*args, **kwargs)

    def generate_key(self):
        return binascii.hexlify(os.urandom(20)).decode()

    def __str__(self):
        return self.key

Now we need to change the DRF authtoken views and serializer to obtain token based on device mac_address instead of the username and password combination used by default.

# authtoken/views.py
from rest_framework import parsers, renderers
from .models import Token
from .serializers import AuthTokenSerializer
from rest_framework.response import Response
from rest_framework.views import APIView

class ObtainAuthToken(APIView):
    throttle_classes = ()
    permission_classes = ()
    parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
    renderer_classes = (renderers.JSONRenderer,)
    serializer_class = AuthTokenSerializer

    def post(self, request):
        serializer = self.serializer_class(data=request.data)
        serializer.is_valid(raise_exception=True)
        device = serializer.validated_data['device']
        token, created = Token.objects.get_or_create(device=device)
        return Response({'token': token.key})
# authtoken/serializers.py
from django.contrib.auth import authenticate
from django.utils.translation import ugettext_lazy as _

from rest_framework import serializers

from device.models import Device

class AuthTokenSerializer(serializers.Serializer):
    mac_address = serializers.CharField()

    def validate(self, attrs):
        mac_address = attrs.get('mac_address')

        if Device.objects.filter(mac_address=mac_address).exists():
            device = Device.objects.get(mac_address=mac_address)
        else:
            msg = _('Device not registered')
            raise serializers.ValidationError(msg)

        attrs['device'] = device
        return attrs

and finally the admin.py file,

# authtoken/admin.py
from django.contrib import admin

from .models import Token


class TokenAdmin(admin.ModelAdmin):
    list_display = ('key', 'device', 'created')
    fields = ('device',)
    ordering = ('-created',)

admin.site.register(Token, TokenAdmin)

Now we are ready to roll, but before that we need to

  • add these authtoken and device apps to installed apps in settings file
  • add url for obtaining tokens
  • run makemigrations and migrate
# projectname/settings.py
INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework', # if you haven't installed DRF, do: pip install djangorestframework
    'device',
    'authtoken',
)
# projectname/urls.py
from django.conf.urls import include, url
from django.contrib import admin
from authtoken.views import ObtainAuthToken

urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    url(r'^token/', ObtainAuthToken.as_view(), name='token'),
]

set up the database,

$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py createsuperuser
$ python manage.py runserver

Now, go to admin and add a new device entry and verify if the tokens are autogenerated and if it did,

$ http post http://127.0.0.1:8000/token/ mac_address=mac_address 
{
    "token": "bc3b1b99c7a62753428c6169400617c9a539b7b4"
}
# if you don't have httpie, install it by `sudo pip install httpie`

So, we’ve setup Token generation for our application, but we haven’t done anything for token authentication part,

We have to have our own permission class, here I’m going to use our own IsAuthenticated permission class. so create a permissions.py file in authtoken app, with the below piece of code.

# authtoken/permissions.py
from rest_framework.permissions import BasePermission

class IsAuthenticated(BasePermission):
    """
    Allows access only to authenticated devices.
    """

    def has_permission(self, request, view):
        try:
        	return request.user and request.user.mac_address
        except:
        	return False

lets add a view or API endpoint or whatever you call it,

# device/views.py
from django.http import JsonResponse

# DRF
from rest_framework.views import APIView

from authtoken.authentication import TokenAuthentication
from authtoken.permissions import IsAuthenticated

class HelloView(APIView):
    """
        Returns a list of Categories available
    """
    authentication_classes = (TokenAuthentication,)
    permission_classes = (IsAuthenticated,)

    def get(self, request):
        return JsonResponse({'Hello':'View'})

add our new view to main urls file,

# projectname/urls.py
from django.conf.urls import include, url
from django.contrib import admin
from authtoken.views import ObtainAuthToken
from device.views import HelloView

urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    url(r'^token/', ObtainAuthToken.as_view(), name='token'),
    url(r'^helloview/', HelloView.as_view(), name='hello_view'),
]

Now, try accessing the added url and check it with and without token and see the authentication works

$ http http://127.0.0.1:8000/helloview/ "Authorization: Token bc3b1b99c7a62753428c6169400617c9a539b7b4"
{
    "Hello": "View"
}

What’s next!

We could make our token to expire at a certain time delta. For this, we could use django-rest-framework-expiring-tokens project directly or bring those features into your own authtoken app if you like.