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

后端 未结 10 1258
庸人自扰
庸人自扰 2020-12-02 03:20

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

相关标签:
10条回答
  • 2020-12-02 03:55

    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)
    
    0 讨论(0)
  • 2020-12-02 03:58

    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"
    }
    
    0 讨论(0)
  • 2020-12-02 04:00

    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'),
    )
    
    0 讨论(0)
  • 2020-12-02 04:00

    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.

    0 讨论(0)
  • 2020-12-02 04:01

    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?

    0 讨论(0)
  • 2020-12-02 04:01

    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)
    
    0 讨论(0)
提交回复
热议问题