[Django]-Token Authentication for RESTful API: should the token be periodically changed?

127๐Ÿ‘

โœ…

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'),
)
๐Ÿ‘คodedfos

34๐Ÿ‘

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',
    ),
}
๐Ÿ‘คgalex

7๐Ÿ‘

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"
}
๐Ÿ‘คRyan Dines

6๐Ÿ‘

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)
๐Ÿ‘คBenjamin Toueg

6๐Ÿ‘

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.

๐Ÿ‘คramwin

4๐Ÿ‘

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:

How to make Django REST JWT Authentication scale with mulitple webservers?

๐Ÿ‘คAngky William

2๐Ÿ‘

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)

0๐Ÿ‘

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.

0๐Ÿ‘

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)
๐Ÿ‘คwdfc

0๐Ÿ‘

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

0๐Ÿ‘

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
๐Ÿ‘คWhite Castle

0๐Ÿ‘

According to DRFโ€™s documentaion:

For an implementation which allows more than one token per user, has some tighter security implementation details, and supports token expiry, please see the Django REST Knox third party package

(https://www.django-rest-framework.org/api-guide/authentication/)

๐Ÿ‘คEhsan88

Leave a comment