Django DRF – Create multiple Token per user


In this post I will show you how to create multiple token for users connecting to your django backend.

Django Rest Framework (DRF) Token authentication uses a simple token-based HTTP Authentication scheme. It is appropriate for client-server application architecture, where the backend is accessed from various clients such as desktop and mobile clients.

If you are reading this you probably already found out that DRF Token Authentication, out of the box, does not allow a single user to have multiple token.

In case you are wondering why this feature is even useful, here is a scenario; I started exploring this option when working on an IOT application that requires token authentication for mobile and Desktop clients, as well as allow manual creation of auth tokens for integrating IOT devices and third party applications via RESTAPIs.

The authentication token used by mobile and desktop clients are set to expire every 24hrs which is not suitable for an IOT device that requires a persistent connection to a service hosted by the platform. I also wanted to grant users the ability to manually create/delete tokens used to integrate third party apps and devices.

With that out of the way, let’s dive into the technical nitty-gritty.

The assumption here is that you are already familiar with Django Rest Framework and Token Authentication. If you are not, you can find more about DRF Token authentication here: http://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication

In this tutorial we will be creating two Authentication classes;

  • Non Expiring Token used for manually creating tokens for clients that require persistent non-expiring authentication tokens, such as third party Apps.
  • Expiring Token used for creating tokens that expire every 24 hours. This is appropriate for Desktop and Mobile clients.
  •  

    1. Remove the ‘rest_framework.authtoken’ from ‘INSTALLED_APPS’ in your settings.py

    We will be creating our own custom authtoken class instead of using the default from rest_framework.

    2. Create a new model for the custom Token overriding the default DRF Token. The Default Token model has a ‘One-to-One’ relation field to the user. We will be replacing that with ‘Foreign Key’ which allows ‘Many-to-One’ relationship.

    # models.py
    
    from django.utils.encoding import python_2_unicode_compatible
    from django.utils.translation import ugettext_lazy as _
    import rest_framework.authtoken.models
    
    
    @python_2_unicode_compatible
    class Token(rest_framework.authtoken.models.Token):
        '''
        create multi token per user - override default rest_framework Token class
        replace model one-to-one relationship with foreign key
        '''
        key = models.CharField(_("Key"), max_length=40, db_index=True, unique=True)
        #Foreign key relationship to user for many-to-one relationship
        user = models.ForeignKey(
            settings.AUTH_USER_MODEL, related_name='auth_token',
            on_delete=models.CASCADE, verbose_name=_("User")
        )
        name = models.CharField(_("Name"), max_length=64)
    
        class Meta:
            # ensure user and name are unique
            unique_together = (('user', 'name'),)
    
    

    Note that we’ve added a field called ‘name’. This is used for assigning name used in identifying each token. The ‘name’ and ‘user’ fields are used in the ‘unique_together’ validator to enforce ‘unique_together’ constraints on the model instance.
    Also, compared to the base class, the ‘key’ field is no longer a primary key but still indexed and unique.

    3. In ‘authentication.py’ add below code. This will serve as the default authentication class for desktop and mobile clients.

    # authentication.py
    
    
    from rest_framework.authentication import TokenAuthentication
    from .models import Token
    from rest_framework.exceptions import AuthenticationFailed
    
    
    class ExpiringTokenAuthentication(TokenAuthentication):
        '''
        Expiring token for mobile and desktop clients.
        It expires every 24hrs requiring client to supply valid username 
        and password for new one to be created.
        '''
        def authenticate_credentials(self, key, request=None):
            models = self.get_model()
            try:
                token = models.objects.select_related('user').get(key=key)
            except models.DoesNotExist:
                raise AuthenticationFailed({'error':'Invalid or Inactive Token', 'is_authenticated': False})
    
            if not token.user.is_active:
                raise AuthenticationFailed({'error':'Invalid user', 'is_authenticated': False})
    
            utc_now = timezone.now()
            utc_now = utc_now.replace(tzinfo=pytz.utc)
    
            if token.created < utc_now - datetime.timedelta(hours=24):
                raise AuthenticationFailed({'error':'Token has expired', 'is_authenticated': False})
            return token.user, token
    

    4. Now in the view, add codes for generating both ‘expiring’ and ‘persistent’ auth tokens. We will be overriding the default DRF ObtainAuthToken class to include retrieving and adding the ‘name’ parameter supplied by the client.

    
    # views.py
    
    from rest_framework.authtoken.views import ObtainAuthToken
    from rest_framework.response import Response
    from rest_framework import status
    import datetime
    from django.utils import timezone
    from .models import Token
    
    class ObtainMultiAuthToken(ObtainAuthToken):
        '''
        create persistent tokens that user can manually create for connecting devices
        '''
    
        def post(self, request):
            serializer = self.serializer_class(data=request.data)
            if serializer.is_valid():
                user = serializer.validated_data['user']
                name = serializer.validated_data['token_name']
                token, created = Token.objects.get_or_create(user=user, name=name)
                return Response({'id_token': token.key, 'token_name': name, 'user': user.email, 'is_admin': user.is_staff})
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST, )
    
    obtain_persistent_auth_token = ObtainMultiAuthToken.as_view()
    
    
    
    class ObtainExpiringAuthToken(ObtainAuthToken):
        '''
        Create token that expires every 24hrs
        Used for login in users from mobile and desktop clients
        '''
    
        def post(self, request):
            serializer = self.serializer_class(data=request.data)
            if serializer.is_valid():
                user = serializer.validated_data['user']
                token, created = Token.objects.get_or_create(user=user, name='auth-token')
                utc_now = timezone.now()
                if not created and token.created < utc_now - datetime.timedelta(hours=24):
                    token.delete()
                    token = Token.objects.create(user=user)
                    token.created = datetime.datetime.utcnow()
                    token.save()
                return Response({'id_token': token.key, 'user': user.email, 'is_admin': user.is_staff})
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST, )
    
    
    obtain_expiring_auth_token = ObtainExpiringAuthToken.as_view()
    
    
    

    5. Add auth API endpoints to urls.py

    # urls.py
    
    from django.urls import path
    from . import views
    
    
    urlpatterns = [
    ...
        path('non-expiring-token/', views.obtain_persistent_auth_token),
        path('token/', views.obtain_expiring_auth_token), 
    ...
    ]
    

    6. If you want a Token to be created for every authenticated user, you can simply catch the User’s post_save signal.

    from django.conf import settings
    from django.db.models.signals import post_save
    from django.dispatch import receiver
    from .models import Token # your custom token class
    
    @receiver(post_save, sender=settings.AUTH_USER_MODEL)
    def create_auth_token(sender, instance=None, created=False, **kwargs):
        if created:
            Token.objects.create(user=instance)
    
    

    7. In settings.py, set your default authentication class. You could also use per view authentication, visit here for more information.

    # settings.py
    
    REST_FRAMEWORK = {
     
        'DEFAULT_AUTHENTICATION_CLASSES': (
            'pb_auth.authentication.ObtainExpiringAuthToken',
        )
    }
    
    

    8. To Logout clients, delete the token. A new token is automatically created whenever a valid username and password are provided.

    
    # views.py
    
    class Logout(APIView):
    
        def get(self, request):
            # delete token
            request.auth.delete()
            return Response("User successfully logged out")
    
    
    logout = Logout.as_view()
    
    

    9. Add the Logout endpoint to your urls.py

    
    
    urlpatterns = [
    ...
        path('logout/', views.logout),
    ...
    ]
    


    About Matthias 33 Articles
    I am a Software Engineer from Houston, TX who love to write codes that brings great ideas to live. In my professional life, I have created software for different industries including Oil & Gas, Finance, Service Provider, Cloud Computing and Embedded Systems. When not writting codes, i enjoy travelling, good music and photography. You can reach me at me@matthiasomisore.com.

    3 Comments

    1. You can use below folder structure. note that all the token related codes are held in a separate app called ‘auth_app’:

      project_base:
      app_project:
         settings.py
      auth_app:
           __init__.py
           admin.py
           authentication.py
           views.py
           signals.py
      
    2. I am getting request.user is Anonymous user even when i got the token (by logging in). I think there is some issue with setting up the middlewares. Can you share your MIDDLEWARE_SETTINGS ?

    Leave a Reply

    Your email address will not be published.


    *