128 votes

Authentification par jeton pour les API RESTful : le jeton doit-il être changé périodiquement ?

Je construis une API RESTful avec Django et django-rest-framework .

Comme mécanisme d'authentification nous avons choisi "Token Authentication" et je l'ai déjà implémenté en suivant la documentation de Django-REST-Framework, la question est, est-ce que l'application doit renouveler / changer le Token périodiquement et si oui comment ? Est-ce l'application mobile qui doit demander le renouvellement du jeton ou l'application web qui doit le faire de manière autonome ?

Quelle est la meilleure pratique ?

Quelqu'un ici a une expérience du cadre REST de Django et pourrait suggérer une solution technique ?

(la dernière question est moins prioritaire)

115voto

odedfos Points 537

Une bonne pratique consiste à demander aux clients mobiles de renouveler périodiquement leur jeton d'authentification. C'est bien sûr au serveur de faire respecter cette règle.

La classe TokenAuthentication par défaut ne prend pas en charge cette fonctionnalité, mais vous pouvez l'étendre pour la réaliser.

Par exemple :

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

Il est également nécessaire de remplacer la vue de connexion par défaut du rest framework, afin que le jeton soit rafraîchi à chaque fois qu'une connexion est effectuée :

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()

Et n'oubliez pas de modifier les urls :

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

8 votes

Ne voudriez-vous pas créer un nouveau jeton dans ObtainExpiringAuthToken s'il a expiré, plutôt que de simplement mettre à jour l'horodatage de l'ancien jeton ?

4 votes

La création d'un nouveau jeton est logique. Vous pourriez également régénérer la valeur de la clé du jeton existant, ce qui vous éviterait de devoir supprimer l'ancien jeton.

0 votes

Que faire si je veux effacer le jeton à l'expiration ? Lorsque j'utilise à nouveau get_or_create, un nouveau jeton sera-t-il généré ou l'horodatage sera-t-il mis à jour ?

29voto

galex Points 1381

Si quelqu'un est intéressé par cette solution mais souhaite disposer d'un jeton valable pendant un certain temps, il obtient alors remplacé par un nouveau jeton Voici la solution complète (Django 1.6) :

votremodule/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()

votremodule/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')
)

votre projet urls.py (dans le tableau urlpatterns) :

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)

Dans vos paramètres REST_FRAMEWORK, ajoutez ExpiringTokenAuthentication comme classe d'authentification au lieu de TokenAuthentication :

REST_FRAMEWORK = {

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

0 votes

Je reçois l'erreur 'ObtainExpiringAuthToken' object has no attribute 'serializer_class' lorsque j'essaie d'accéder au point de terminaison de l'api. Je ne sais pas ce qui me manque.

2 votes

Solution intéressante, que je testerai plus tard ; pour l'instant, votre message m'a aidé à me mettre sur la bonne voie car j'avais simplement oublié de définir les AUTHENTICATION_CLASSES.

4 votes

Je suis arrivé tard à la fête, mais j'ai dû faire quelques changements subtils pour que ça marche. 1) utc_now = datetime.datetime.utcnow() devrait être utc_now = datetime.datetime.utcnow().replace(tzinfo=pytz.UTC) 2) Dans la classe ExpiringTokenAuthentication(TokenAuthentication) : Vous avez besoin du modèle, self.model = self.get_model()

7voto

Ryan Dines Points 412

J'ai pensé donner une réponse Django 2.0 en utilisant DRY. Quelqu'un a déjà construit cela pour nous, google Django OAuth ToolKit. Disponible avec pip, pip install django-oauth-toolkit . Instructions pour ajouter les ViewSets à jeton avec les routeurs : https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html . Il est similaire au tutoriel officiel.

Donc, en gros, OAuth1.0 était plus la sécurité d'hier, ce qu'est TokenAuthentication. Pour obtenir des jetons d'expiration fantaisistes, OAuth2.0 fait fureur ces jours-ci. Vous obtenez un AccessToken, un RefreshToken, et une variable scope pour affiner les permissions. Vous vous retrouvez avec des credos comme celui-ci :

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

5voto

ramwin Points 849

L'auteur a demandé

La question est de savoir si l'application doit renouveler / changer le Token périodiquement et si oui, comment ? Est-ce l'application mobile qui doit demander le renouvellement du jeton ou l'application web qui doit le faire de manière autonome ?

Mais toutes les réponses sont écrites sur la façon de changer automatiquement le jeton.

Je pense que changer périodiquement de jeton n'a pas de sens. Le cadre de repos crée un jeton de 40 caractères. Si l'attaquant teste 1000 jetons chaque seconde, il lui faut 16**40/1000/3600/24/365=4.6*10^7 années pour obtenir le jeton. Vous ne devez pas vous inquiéter que l'attaquant teste votre jeton un par un. Même si vous avez changé de jeton, la probabilité de deviner votre jeton est la même.

Si vous craignez que les attaquants puissent obtenir votre jeton, vous le changez périodiquement, mais une fois que l'attaquant a obtenu le jeton, il peut également changer votre jeton, et l'utilisateur réel est expulsé.

Ce que vous devez faire, c'est empêcher un attaquant d'obtenir le jeton de l'utilisateur, utiliser https .

Au fait, je dis juste que le changement de jeton par jeton n'a pas de sens, le changement de jeton par nom d'utilisateur et mot de passe a parfois un sens. Peut-être que le jeton est utilisé dans un environnement http (il faut toujours éviter ce genre de situation) ou par un tiers (dans ce cas, il faut créer un autre type de jeton, utiliser oauth2) et quand l'utilisateur fait quelque chose de dangereux comme changer la boîte aux lettres de liaison ou supprimer le compte, il faut s'assurer que vous n'utiliserez plus le jeton d'origine parce qu'il peut avoir été révélé par l'attaquant en utilisant des outils sniffer ou tcpdump.

5voto

Benjamin Toueg Points 3040

J'ai essayé la réponse de @odedfos mais J'ai eu une erreur trompeuse . Voici la même réponse, corrigée et avec les importations appropriées.

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)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X