Menu
Menu

Token based authentication using Django Rest Framework

Friday, May 27, 2016

In this post we'll be creating login and signup APIs using Django Rest Framework (DRF) which will provide token based authentication to the client.

Those who are new to token based authentication can read about it here.

For the client end we'll be using an Android app. The Android app uses the Google sign in to get the user's details like email etc and allow the user to enter the app. We do not have the traditional username and password sign in but you can go ahead and integrate that too.

Assuming you already have a Django project, let's start setting up the DRF.

First install DRF and add 'rest_framework', 'rest_framework.authtoken' to your INSTALLED_APPS in settings.py -

pip install djangorestframework

Then in your root urls.py file add -

url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))

That's it. Now lets create the models. We are going to create just one model named 'Location' which will save the user's locations. The user model will be provided by Django. This is the models.py -

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

class Location(models.Model):
    owner = models.ForeignKey(User, related_name='locations')
    latitude = models.CharField(max_length=20)
    longitude = models.CharField(max_length=20)
    address = models.CharField(max_length=500)
    created = models.DateTimeField()
    updated = models.DateTimeField()
    retrieved = models.BooleanField(default=False)

    class Meta:
        ordering = ('created',)

Then we have the app level urls.py -

from django.conf.urls import url
from snippets import views
from rest_framework.urlpatterns import format_suffix_patterns

urlpatterns = [
    url(r'^users/$', views.UserList.as_view()),
    url(r'^users/(?P<pk>[0-9]+)/$', views.UserDetail.as_view()),
    url(r'^locations/$', views.LocationList.as_view()),
    url(r'^login/$', views.Login.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)

The top two routes are for listing all the users or get a single user using the user's id respectively. The third route is for listing the a particular user's location. You might notice that we don't provide the user in the url whose location we need to get. That will be handled by token authentication as explained later. Lastly we also have a route for the signup/login to register new users or authenticate existing users.

In DRF we use serializers to convert our data to json format. We create a new file called serializers.py in the same directory as above mentioned urls.py -

from rest_framework import serializers
from .models import Location
from django.contrib.auth.models import User

class LocationSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')

    class Meta:
        model = Location
        fields = ('id', 'owner', 'latitude', 'longitude', 'address', 'created', 'updated', 'retrieved')

class UserSerializer(serializers.ModelSerializer):
    locations = serializers.PrimaryKeyRelatedField(many=True, queryset=Location.objects.all())

    class Meta:
        model = User
        fields = ('id', 'username', 'email', 'locations')

Finally this is our views.py -

from django.shortcuts import render
from django.conf import settings
from snippets.models import Location
from django.contrib.auth.models import User
from snippets.serializers import LocationSerializer,UserSerializer
from rest_framework.authentication import SessionAuthentication, BasicAuthentication, TokenAuthentication
from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveAPIView, RetrieveUpdateDestroyAPIView
from rest_framework.authtoken.models import Token
from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_201_CREATED, HTTP_202_ACCEPTED
from oauth2client import client, crypt

class UserList(ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer


class UserDetail(RetrieveAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer


class LocationList(ListCreateAPIView):
    queryset = Location.objects.all()
    serializer_class = LocationSerializer
    permission_classes = (IsAuthenticated,)
    authentication_classes = (TokenAuthentication,)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

    def get_queryset(self):
        user = self.request.user
        return Location.objects.filter(owner=user)


class Login(APIView):
    def verifyGoogleToken(self, idToken):
        isIdTokenValid = True
        try:
            idinfo = client.verify_id_token(idToken, settings.CLIENT_ID)
            if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
                raise crypt.AppIdentityError("Wrong issuer.")
        except crypt.AppIdentityError:
            # Invalid token
            isIdTokenValid = False
            idinfo = {
                'Response' : 'Invalid Google Token!',
            }
        return (isIdTokenValid, idinfo)

    def post(self, request, format=None):
        isIdTokenValid, googleResponse = self.verifyGoogleToken(request.data.get('id_token'))
        if isIdTokenValid:
            try:
                user = User.objects.get(email=googleResponse.get('email'))
                token = Token.objects.get(user=user)
                statusCode = HTTP_202_ACCEPTED
            except User.DoesNotExist:
                user = User.objects.create(username=googleResponse.get('name'), email=googleResponse.get('email'))
                token = Token.objects.create(user=user)
                statusCode = HTTP_201_CREATED
            additionalContent = {
                'token' : token.key,
                }
            googleResponse.update(additionalContent)
            return Response(googleResponse, statusCode)
        else:
            return Response(googleResponse, status=HTTP_400_BAD_REQUEST)

First you need to install the google library to verify the google ID Token received from the client -

pip install oauth2client

The UserList and UserDetail API are pretty straightforward. We just serialize the data and return it. No authentication classes have been added so anyone can make a HTTP GET call to this API.

Coming to LocationList API. You may remember that we don't provide the user in the url '/locations' because the user is recognized by token authentication. DRF handles everything for us automatically. We have added permission_classes and authentication_classes in this API. That means the request to this API needs to be authenticated using tokens. The HTTP GET request must have the token sent along with it in the Header of the request with the following format - 'Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' where the number is the user token available on the client end. If we don't provide this token then the request will fail authentication and no data will be returned.

Now the last part is understanding how to generate unique token for each and every user and associate it with the user object.

Let's have a look at the Login API. It receives a HTTP POST request which contains the Google 'ID Token' generated at the client end. This token is verified using Google's oauth2client library. If the token is valid then we get the email address of the user along with some other details.

Using the email address received we check if the user already exists or not. If the user exists then we simply return the credentials of the user including the authentication token. If the user does not exist then we first create the new user using the email address and associate a new authentication token with the user and then return the credentials.

That's it! All the future requests which need to be authenticated, just need to include the authentication classes.

Source Code available at Github.