API Authentication Django and Android Apps
Client and User API Authetication for Django, Android and iOS apps
While working on a recent project, I found myself in a situation where me and my team had to implement the security authentication layer on the server side to make sure only the authenticated client applications (android and iOS mobile apps in our case) and authenticated users have access to the data. As anybody else would, we started to research on the best practices and how other people in the industry have implemented such authentication.
We had used Django Rest Framework to implement our API resources and as it is one of the most known/used packages to implement APIs with Django Framework, I thought we would easily find the information we are looking for. But to my surprise all our google searches ended up in blogs and stackoverflow answers that showed how to implement authentication, which we already had an idea about.
We could not find a single example that showed us how these authentication would work with different types of clients e.g. web
, android
& ios
. Once we failed in gathering the information we required, we decided to identify our core requirements and come-up with the best possible solution for us.
We had only 2 major requirements:
- Client Authentication – Only allow our own clients (web and mobile apps) to access the APIs.
- User Authentication – Only allow authenticated users to access the APIs.
Client Authentication
We wanted to make sure that only our own mobile apps are allowed to use our API end points and most of the examples in our research just showed implementation of authentication for users we knew we’d need to work more on this one. Even Django Rest Framework had built in classes to be used for user authentication and that’s what you can find in the documentation, we knew we’d need to implement customised models and logic for client authentication.
So we started off with defining a custom APIClient model as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
from django.db import models from utils.general import generate_hash class APIClient(models.Model): ## Token related fields access_token = models.CharField(max_length=255, blank=True) expires_on = models.DateTimeField(null=True) expired = models.BooleanField(default=False) ## Client related fields name = models.CharField(max_length=255) key = models.CharField(max_length=255) secret = models.CharField(max_length=255) def generate_key_and_secret(self): """ Generates a random key and secret combo. """ key = generate_hash() secret = generate_hash() return (key, secret) def set_key_and_secret(self, key=None, sec=None): """ Associates the given key and secret to the instance of self. If no key and secret was passed, they will be automatically generated. """ nkey, nsec = self.generate_key_and_secret() key = key if key else nkey sec = sec if sec else nsec # Associate key and secret with self. self.key = key self.secret = sec self.save() def save(self, *args, **kwargs): super(APIClient, self).save(*args, **kwargs) if not (self.key or self.secret): self.set_key_and_secret() |
So whenever a new APIClient object is created, it will automatically generate a key
and a secret
and add it as an attribute to the object.
So For example, we’ll create a new APIClient for our android app as follows:
1 |
client = APIClient.objects.create(name='Android 5.2') |
and then we provide client.key
and client.secret
to our android development team as they will need then while requesting an access token for their app.
The idea behind is, that we provide different clients with credentials (key and secret) just like a user has a username and a password. Then just like user authentication they give us the key and the secret and on the backend we decide whether this APIClient should have access to our API’s or not.
If the key and secret exist, match, are valid and are not expired yet, we provide the client with an encrypted access_token
which they have to include in the request headers to access the API resources.
In order to achieve the above we’d now start off with implementing an API resource that authenticate the given key and secret and provide the clients with an access token if their request is valid.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
from rest_framework import serializers from rest_framework.generics import CreateAPIView import utils ## Serializer class ClientAccessTokenSerializer(serializers.Serializer): def create(self, data): """ Handles the post data. """ key = data.get('key') secret = data.get('secret') try: client = APIClient.objects.get(key=key, secret=secret) access_token, expires_on = utils.create_client_access_token( client.key, client.secret) data['access_token'] = access_token data['expires_on'] = expires_on return data except Exception as err: raise serializers.ValidationError(str(err)) ## View class GetClientAccessToken(CreateAPIView): authentication_classes = [] permission_classes = [] serializer_class = ClientAccessTokenSerializer |
The above view will return a valid access token to the client if they provide valid credentials (key and secret) in their request data. Once they have an access token they can include it in the request headers to access the data through the API resources.
Now we will implement the logic that determines whether the given access_token in the request is valid or not and if we should give the request access to our data. To do so we will create a custom authentication class that extends Django rest framework’s base authentication class.
1 2 3 4 5 6 7 8 9 10 |
from rest_framework import authentication from utils import client_access_token_valid class ClientAuthentication(authentication.BaseAuthentication): def authenticate(self, request): token = request.META.get('HTTP_CTOKEN') if (token) and (client_access_token_valid(token)): return True, True raise exceptions.AuthenticationFailed("Client Authentication Failed") |
The above class now can be used with the API views where we want to implement the client authentication and in case the request contains a bad access_token
we will return 401 Unauthorized
response to the client.
The above implementation can only be used for requests that require just the client authentication. But there are a lot of views where we would want to authenticate both the client and the user. And as rest framework would pass the authentication if any one of the authentication passes, We’d need to implement another authentication that authenticates both the client and the user for such views.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
from rest_framework import authentication from utils import client_access_token_valid class ClientAndUserAuthentication(authentication.BaseAuthentication): def authenticate_client(self): ... def authenticate_user(self): ... def authenticate(self, request): user = self.authenticate_user() authenticated = self.authenticate_client() if user and authenticated: return user, authenticated raise exceptions.AuthenticationFailed("Authentication Failed") |
For endpoints where we required both the client and the user authentication we’d simply use the above class in the authentication_classes
property of the APIView and for public endpoints where we do not require user authentication we’d use ClientAuthentication
class.
If you are using this document as a reference to your work, Please do note the following points:
- This is just a basic authentication implementation on the ORM end and you can additionally secure your data by implementing a security layer on the server end.
- Make sure you use the above implementation with the SSL protocol i.e. over https as the communication over this protocol is encrypted and thus secure. Using such implementation over http is as good as having no authentication implementation’
If you have any questions or suggestion, Please do comment below.
Update:
Following is the requested utils package used in the examples below. Some of the code might have changed in the following package over time but this will give you a basic idea.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
import datetime import time import uuid from django.conf import settings from django.contrib.auth import authenticate from django.core.cache import get_cache from scripts.krypt import AESCipher conn = get_cache('authentication') def generate_hash(): """ Generates a random hash using the uuid4 module """ return str(uuid.uuid4().get_hex()) def create_client_access_token(key, secret, expires_after_days=30): """ Creates an access_token for an API client based on the key, secret and timestamp. Encryption Algo used from krypt script. """ ## First we try to get the token from the redis cache ## if the token exists we directly return the token tdata = conn.get('%s%s%s' % (key, settings.DEFAULT_SEP, secret)) if tdata: token = tdata.get('token') expires_on = tdata.get('expires_on') return token, expires_on ## If token does not exist in the cache create a new token ## and then put it in the cache now = datetime.datetime.now() expires_on = now + datetime.timedelta(expires_after_days) cache_expire = int((expires_on - now).total_seconds()) expires_on = expires_on.strftime(settings.DATE_FORMAT) raw_token_key = '%s%s%s%s%s' % (key, settings.DEFAULT_SEP, secret, settings.DEFAULT_SEP, expires_on) cipher = AESCipher() token = cipher.encrypt(raw_token_key) ## Save the newly created token in the cache ## and expire the entry on expires_on value cache_map = {'token': token, 'expires_on': expires_on} conn.set('%s%s%s' % (key, settings.DEFAULT_SEP, secret), cache_map, timeout=cache_expire) return token, expires_on def client_access_token_valid(access_token): """ Returns a boolean value of whether or not a given client access token is valid """ cipher = AESCipher() try: raw_token = cipher.decrypt(access_token) except (TypeError, ValueError, Exception) as err: raw_token = "" raw_split = raw_token.split(settings.DEFAULT_SEP) if len(raw_split) == 3: key, secret, expires_on = raw_split tdata = conn.get('%s%s%s' % (key, settings.DEFAULT_SEP, secret)) if tdata: return tdata.get('token') == access_token return False |
Tags In
-
Pranab
-
geekunlimited
-
-
punit dama
-
geekunlimited
-
-
Raja Sudhan
-
geekunlimited
-
-
Mia
-
geekunlimited
-
Mia
-
-
-
India Info
-
Sundaram
-
Amna Sheikh
-
Mirco Grillo