Token Authentication for RESTful API: should the token be periodically changed?

DjangoRestRestful AuthenticationDjango Rest-Framework

Django Problem Overview


I'm building a RESTful API with Django and django-rest-framework.

As authentication mechanism we have chosen "Token Authentication" and I have already implemented it following Django-REST-Framework's documentation, the question is, should the application renew / change the Token periodically and if yes how? Should it be the mobile app that requires the token to be renewed or the web-app should do it autonomously?

What is the best practice?

Anybody here experienced with Django REST Framework and could suggest a technical solution?

(the last question has lower priority)

Django Solutions


Solution 1 - Django

It is good practice to have mobile clients periodically renew their authentication token. This of course is up to the server to enforce.

The default TokenAuthentication class does not support this, however you can extend it to achieve this functionality.

For example:

from rest_framework.authentication import TokenAuthentication, get_authorization_header
from rest_framework.exceptions import AuthenticationFailed

class ExpiringTokenAuthentication(TokenAuthentication):
    def authenticate_credentials(self, key):
        try:
            token = self.model.objects.get(key=key)
        except self.model.DoesNotExist:
            raise exceptions.AuthenticationFailed('Invalid token')

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed('User inactive or deleted')

        # This is required for the time comparison
        utc_now = datetime.utcnow()
        utc_now = utc_now.replace(tzinfo=pytz.utc)

        if token.created < utc_now - timedelta(hours=24):
            raise exceptions.AuthenticationFailed('Token has expired')

        return token.user, token

It is also required to override the default rest framework login view, so that the token is refreshed whenever a login is done:

class ObtainExpiringAuthToken(ObtainAuthToken):
    def post(self, request):
        serializer = self.serializer_class(data=request.data)
        if serializer.is_valid():
            token, created =  Token.objects.get_or_create(user=serializer.validated_data['user'])

            if not created:
                # update the created time of the token to keep it valid
                token.created = datetime.datetime.utcnow()
                token.save()

            return Response({'token': token.key})
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

obtain_expiring_auth_token = ObtainExpiringAuthToken.as_view()

And don't forget to modify the urls:

urlpatterns += patterns(
    '',
    url(r'^users/login/?$', '<path_to_file>.obtain_expiring_auth_token'),
)

Solution 2 - Django

If someone is interested by that solution but wants to have a token that is valid for a certain time then gets replaced by a new token here's the complete solution (Django 1.6):

yourmodule/views.py:

import datetime
from django.utils.timezone import utc
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
from django.http import HttpResponse
import json

class ObtainExpiringAuthToken(ObtainAuthToken):
    def post(self, request):
        serializer = self.serializer_class(data=request.DATA)
        if serializer.is_valid():
            token, created =  Token.objects.get_or_create(user=serializer.object['user'])

            utc_now = datetime.datetime.utcnow()    
            if not created and token.created < utc_now - datetime.timedelta(hours=24):
                token.delete()
                token = Token.objects.create(user=serializer.object['user'])
                token.created = datetime.datetime.utcnow()
                token.save()

            #return Response({'token': token.key})
            response_data = {'token': token.key}
            return HttpResponse(json.dumps(response_data), content_type="application/json")
        
        return HttpResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

obtain_expiring_auth_token = ObtainExpiringAuthToken.as_view()

yourmodule/urls.py:

from django.conf.urls import patterns, include, url
from weights import views

urlpatterns = patterns('',
    url(r'^token/', 'yourmodule.views.obtain_expiring_auth_token')
)

your project urls.py (in the urlpatterns array):

url(r'^', include('yourmodule.urls')),

yourmodule/authentication.py:

import datetime
from django.utils.timezone import utc
from rest_framework.authentication import TokenAuthentication
from rest_framework import exceptions

class ExpiringTokenAuthentication(TokenAuthentication):
    def authenticate_credentials(self, key):

        try:
            token = self.model.objects.get(key=key)
        except self.model.DoesNotExist:
            raise exceptions.AuthenticationFailed('Invalid token')

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed('User inactive or deleted')

        utc_now = datetime.datetime.utcnow()
        
        if token.created < utc_now - datetime.timedelta(hours=24):
            raise exceptions.AuthenticationFailed('Token has expired')

        return (token.user, token)

In your REST_FRAMEWORK settings add ExpiringTokenAuthentication as an Authentification class instead of TokenAuthentication:

REST_FRAMEWORK = {

    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
        #'rest_framework.authentication.TokenAuthentication',
        'yourmodule.authentication.ExpiringTokenAuthentication',
    ),
}

Solution 3 - Django

Thought I'd give a Django 2.0 answer using DRY. Somebody already built this out for us, google Django OAuth ToolKit. Available with pip, pip install django-oauth-toolkit. Instructions on adding the token ViewSets with routers: https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html. It's similar to the official tutorial.

So basically OAuth1.0 was more yesterday's security which is what TokenAuthentication is. To get fancy expiring tokens, OAuth2.0 is all the rage these days. You get an AccessToken, RefreshToken, and scope variable to fine tune the permissions. You end up with creds like this:

{
    "access_token": "<your_access_token>",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "<your_refresh_token>",
    "scope": "read"
}

Solution 4 - Django

I've tried @odedfos answer but I had misleading error. Here is the same answer, fixed and with proper imports.

views.py

from django.utils import timezone
from rest_framework import status
from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import ObtainAuthToken

class ObtainExpiringAuthToken(ObtainAuthToken):
    def post(self, request):
        serializer = self.serializer_class(data=request.DATA)
        if serializer.is_valid():
            token, created =  Token.objects.get_or_create(user=serializer.object['user'])

            if not created:
                # update the created time of the token to keep it valid
                token.created = datetime.datetime.utcnow().replace(tzinfo=utc)
                token.save()

            return Response({'token': token.key})
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

authentication.py

from datetime import timedelta
from django.conf import settings
from django.utils import timezone
from rest_framework.authentication import TokenAuthentication
from rest_framework import exceptions

EXPIRE_HOURS = getattr(settings, 'REST_FRAMEWORK_TOKEN_EXPIRE_HOURS', 24)

class ExpiringTokenAuthentication(TokenAuthentication):
    def authenticate_credentials(self, key):
        try:
            token = self.model.objects.get(key=key)
        except self.model.DoesNotExist:
            raise exceptions.AuthenticationFailed('Invalid token')

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed('User inactive or deleted')

        if token.created < timezone.now() - timedelta(hours=EXPIRE_HOURS):
            raise exceptions.AuthenticationFailed('Token has expired')

        return (token.user, token)

Solution 5 - Django

The author asked

> the question is, should the application renew / change the Token periodically and if yes how? Should it be the mobile app that requires the token to be renewed or the web-app should do it autonomously?

But all of the answers are writing about how to automatically change the token.

I think change token periodically by token is meaningless. The rest framework create a token that has 40 characters, if the attacker tests 1000 token every second, it requires 16**40/1000/3600/24/365=4.6*10^7 years to get the token. You should not worried that the attacker will test your token one by one. Even you changed your token, the probability of guess you token is the same.

If you are worried that maybe the attackers can get you token, so you change it periodically, than after the attacker get the token, he can also change you token, than the real user is kicked out.

What you should really do is to prevent tha attacker from getting your user's token, use https.

By the way, I'm just saying change token by token is meaningless, change token by username and password is sometimes meanful. Maybe the token is used in some http environment (you should always avoid this kind of situation) or some third party (in this case, you should create different kind of token, use oauth2) and when the user is doing some dangerous thing like changing binding mailbox or delete account, you should make sure you will not use the origin token anymore because it may has been revealed by the attacker using sniffer or tcpdump tools.

Solution 6 - Django

You can leverage http://getblimp.github.io/django-rest-framework-jwt

This library is able generate token that has an expiration date

To understand the difference between DRF default token and the token provide by the DRF take a look at:

https://stackoverflow.com/questions/26126622/how-to-make-django-rest-jwt-authentication-scale-with-mulitple-webservers/30136310#30136310

Solution 7 - Django

It's a good practice to set an expiration mechanism on your app whether for mobile client or web client. There are two common solutions:

  1. system expires token (after specific time) and user has to login again to gain new valid token.

  2. system automatically expires old token (after specific time) and replaces it with new one (change token).

Common things in both of solutions:

Changes in settings.py
DEFAULT_AUTHENTICATION_CLASSES = [
# you replace right path of 'ExpiringTokenAuthentication' class
'accounts.token_utils.ExpiringTokenAuthentication'
]

TOKEN_EXPIRED_AFTER_MINUTES = 300
Create token_utils.py
from django.conf import settings
from datetime import timedelta

from django.conf import settings
from django.utils import timezone
from rest_framework.authentication import TokenAuthentication
from rest_framework.authtoken.models import Token
from rest_framework.exceptions import AuthenticationFailed


def expires_in(token: Token):
elapsed_time = timezone.now() - token.created
return timedelta(minutes=settings.TOKEN_EXPIRED_AFTER_MINUTES) - elapsed_time

def is_token_expired(token):
return expires_in(token) < timedelta(seconds=0)
Changes in your views:
@api_view(['GET'])
@authentication_classes([ExpiringTokenAuthentication])
@permission_classes([IsAuthenticated])
def test(request):
    ...
return Response(response, stat_code)

If using option 1, add these lines to token_utils.py

def handle_token_expired(token):
Token.objects.filter(key=token).delete()


class ExpiringTokenAuthentication(TokenAuthentication):

    def authenticate_credentials(self, key):
        try:
            token = Token.objects.get(key=key)
        except Token.DoesNotExist:
            raise AuthenticationFailed("Invalid Token!")

        if not token.user.is_active:
            raise AuthenticationFailed("User inactive or deleted")

        if is_token_expired(token):
            handle_token_expired(token)
            msg = "The token is expired!, user have to login again." 
            response = {"msg": msg}
            raise AuthenticationFailed(response)

    return token.user, token

If using option 2, add these lines to token_utils.py

def handle_token_expired(token):
    is_expired = is_token_expired(token)
    if is_expired:
        token.delete()
        token = Token.objects.create(user = token.user)
    return is_expired, token


class ExpiringTokenAuthentication(TokenAuthentication):
    """
    when token is expired, it will be removed
    and new one will be created
    """
    def authenticate_credentials(self, key):
        try:
            token = Token.objects.get(key = key)
        except Token.DoesNotExist:
            raise AuthenticationFailed("Invalid Token")
    
        if not token.user.is_active:
            raise AuthenticationFailed("User is not active")

        is_expired, token = handle_token_expired(token)
        if is_expired:
            raise AuthenticationFailed("The Token is expired")
    
        return (token.user, token)

Solution 8 - Django

If you notice that a token is like a session cookie then you could stick to the default lifetime of session cookies in Django: https://docs.djangoproject.com/en/1.4/ref/settings/#session-cookie-age.

I don't know if Django Rest Framework handles that automatically but you can always write a short script which filters out the outdated ones and marks them as expired.

Solution 9 - Django

Just thought I would add mine as this was helpful for me. I usually go with the JWT method but sometimes something like this is better. I updated the accepted answer for django 2.1 with proper imports..

authentication.py

from datetime import timedelta
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
from rest_framework.authentication import TokenAuthentication
from rest_framework import exceptions

EXPIRE_HOURS = getattr(settings, 'REST_FRAMEWORK_TOKEN_EXPIRE_HOURS', 24)


class ExpiringTokenAuthentication(TokenAuthentication):
    def authenticate_credentials(self, key):
        try:
            token = self.get_model().objects.get(key=key)
        except ObjectDoesNotExist:
            raise exceptions.AuthenticationFailed('Invalid token')

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed('User inactive or deleted')

        if token.created < timezone.now() - timedelta(hours=EXPIRE_HOURS):
            raise exceptions.AuthenticationFailed('Token has expired')

    return token.user, token

views.py

import datetime
from pytz import utc
from rest_framework import status
from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.serializers import AuthTokenSerializer


class ObtainExpiringAuthToken(ObtainAuthToken):
    def post(self, request, **kwargs):
        serializer = AuthTokenSerializer(data=request.data)

        if serializer.is_valid():
            token, created = Token.objects.get_or_create(user=serializer.validated_data['user'])
            if not created:
                # update the created time of the token to keep it valid
                token.created = datetime.datetime.utcnow().replace(tzinfo=utc)
                token.save()

            return Response({'token': token.key})
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Solution 10 - Django

just to keep adding to @odedfos answer, I think there have been some changes to the syntax so the code of ExpiringTokenAuthentication needs some adjusting:

from rest_framework.authentication import TokenAuthentication
from datetime import timedelta
from datetime import datetime
import datetime as dtime
import pytz

class ExpiringTokenAuthentication(TokenAuthentication):

    def authenticate_credentials(self, key):
        model = self.get_model()
        try:
            token = model.objects.get(key=key)
        except model.DoesNotExist:
            raise exceptions.AuthenticationFailed('Invalid token')

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed('User inactive or deleted')

        # This is required for the time comparison
        utc_now = datetime.now(dtime.timezone.utc)
        utc_now = utc_now.replace(tzinfo=pytz.utc)

        if token.created < utc_now - timedelta(hours=24):
            raise exceptions.AuthenticationFailed('Token has expired')

        return token.user, token

Also, don't forget to add it to DEFAULT_AUTHENTICATION_CLASSES instead of rest_framework.authentication.TokenAuthentication

Solution 11 - Django

If anyone wants to expire the token after certain time of inactivity, below answer would help. I am tweaking one of the answers given here. I have added comments to the code I added

from rest_framework.authentication import TokenAuthentication
from datetime import timedelta
from datetime import datetime
import datetime as dtime
import pytz

class ExpiringTokenAuthentication(TokenAuthentication):

    def authenticate_credentials(self, key):
        model = self.get_model()
        try:
            token = model.objects.get(key=key)
        except model.DoesNotExist:
            raise exceptions.AuthenticationFailed('Invalid token')

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed('User inactive or deleted')

        # This is required for the time comparison
        utc_now = datetime.now(dtime.timezone.utc)
        utc_now = utc_now.replace(tzinfo=pytz.utc)

        if token.created < utc_now - timedelta(minutes=15):  # TOKEN WILL EXPIRE AFTER 15 MINUTES OF INACTIVITY
			token.delete() # ADDED THIS LINE SO THAT EXPIRED TOKEN IS DELETED
			raise exceptions.AuthenticationFailed('Token has expired')
		else: 
			token.created = utc_now #THIS WILL SET THE token.created TO CURRENT TIME WITH EVERY REQUEST
			token.save() #SAVE THE TOKEN

        return token.user, token

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionnemesisdesignView Question on Stackoverflow
Solution 1 - DjangoodedfosView Answer on Stackoverflow
Solution 2 - DjangogalexView Answer on Stackoverflow
Solution 3 - DjangoRyan DinesView Answer on Stackoverflow
Solution 4 - DjangoBenjamin TouegView Answer on Stackoverflow
Solution 5 - DjangoramwinView Answer on Stackoverflow
Solution 6 - DjangoAngky WilliamView Answer on Stackoverflow
Solution 7 - DjangoSeyed Mostafa SeyedAshoorView Answer on Stackoverflow
Solution 8 - DjangoTomasz ZieliƄskiView Answer on Stackoverflow
Solution 9 - DjangowdfcView Answer on Stackoverflow
Solution 10 - DjangoLuis Rodriguez-MoldesView Answer on Stackoverflow
Solution 11 - DjangoWhite CastleView Answer on Stackoverflow