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

115

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

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

Quelle est la meilleure pratique?

Quelqu'un ici expérimenté avec Django REST Framework et pourrait suggérer une solution technique?

(la dernière question a une priorité inférieure)

nemesisdesign
la source

Réponses:

101

Il est recommandé que les clients mobiles renouvellent périodiquement leur jeton d'authentification. C'est bien sûr au serveur de faire respecter.

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

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 cadre de repos, afin que le jeton soit actualisé 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 URL:

urlpatterns += patterns(
    '',
    url(r'^users/login/?$', '<path_to_file>.obtain_expiring_auth_token'),
)
odedfos
la source
6
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?
Joar Leth
4
Créer un nouveau jeton a du sens. Vous pouvez également régénérer la valeur de la clé de jetons existante et vous n'aurez pas à supprimer l'ancien jeton.
odedfos
Que faire si je souhaite effacer le jeton à l'expiration? Quand je get_or_create à nouveau, un nouveau jeton est-il généré ou l'horodatage est-il mis à jour?
Sayok88
3
De plus, vous pouvez expirer les jetons de la table en expulsant les anciens périodiquement dans un cronjob (Celery Beat ou similaire), au lieu d'intercepter la validation
BjornW
1
@BjornW Je ferais simplement l'expulsion et, à mon avis, il est de la responsabilité de la personne qui s'intègre à l'API (ou à votre front-end) de faire une demande, ils reçoivent, "Jeton non valide", puis appuyez sur le bouton d'actualisation / créer de nouveaux points de terminaison de jetons
ShibbySham
25

Si quelqu'un est intéressé par cette solution mais souhaite avoir un jeton valide pendant un certain temps, il est alors remplacé par un nouveau jeton, voici la solution complète (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')
)

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 en tant que classe d'authentification au lieu de TokenAuthentication:

REST_FRAMEWORK = {

    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
        #'rest_framework.authentication.TokenAuthentication',
        'yourmodule.authentication.ExpiringTokenAuthentication',
    ),
}
galex
la source
J'obtiens l'erreur 'ObtainExpiringAuthToken' object has no attribute 'serializer_class'lorsque j'essaye d'accéder au point de terminaison de l'API. Je ne sais pas ce qui me manque.
Dharmit
2
Solution intéressante, que je testerai plus tard; pour le moment, votre message m'a aidé à me mettre sur la bonne voie, car j'avais simplement oublié de définir AUTHENTICATION_CLASSES.
normal
2
Arriver en retard à la fête mais j'avais besoin de faire quelques changements subtils pour que cela fonctionne. 1) utc_now = datetime.datetime.utcnow () doit être utc_now = datetime.datetime.utcnow (). Replace (tzinfo = pytz.UTC) 2) Dans la classe ExpiringTokenAuthentication (TokenAuthentication): Vous avez besoin d'un modèle, self.model = self. get_model ()
Ishan Bhatt
5

J'ai essayé la réponse @odedfos mais j'ai eu une erreur trompeuse . Voici la même réponse, fixe et avec des 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)
Benjamin Toueg
la source
4

Je pensais que je donnerais une réponse Django 2.0 en utilisant DRY. Quelqu'un a déjà construit cela pour nous, google Django OAuth ToolKit. Disponible avec pépin, pip install django-oauth-toolkit. Instructions sur l'ajout du token ViewSets avec des routeurs: https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html . C'est similaire au tutoriel officiel.

Donc, fondamentalement, OAuth1.0 était plus la sécurité d'hier qui est ce qu'est TokenAuthentication. Pour obtenir des jetons d'expiration sophistiqués, OAuth2.0 fait fureur de nos jours. Vous obtenez un AccessToken, RefreshToken et une variable de portée pour affiner les autorisations. Vous vous retrouvez avec des crédits comme celui-ci:

{
    "access_token": "<your_access_token>",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "<your_refresh_token>",
    "scope": "read"
}
Ryan Dines
la source
4

L'auteur a demandé

la question est de savoir si l'application doit renouveler / changer le jeton périodiquement et si oui comment? Est-ce que c'est l'application mobile qui nécessite le renouvellement du jeton ou l'application Web doit-elle le faire de manière autonome?

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

Je pense que le changement de jeton périodiquement par jeton n'a pas de sens. Le cadre de repos crée un jeton qui a 40 caractères, si l'attaquant teste 1000 jetons toutes les secondes, il faut des 16**40/1000/3600/24/365=4.6*10^7anné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é votre jeton, la probabilité de deviner votre jeton est la même.

Si vous craignez que les attaquants puissent peut-être obtenir votre jeton, vous le changez donc périodiquement, qu'après que l'attaquant ait obtenu le jeton, il puisse également changer votre jeton, alors l'utilisateur réel est expulsé.

Ce que vous devez vraiment faire est d'empêcher l'attaquant d'obtenir le jeton de votre utilisateur, utilisez https .

Au fait, je dis simplement que le changement de jeton par jeton n'a aucun sens, que le changement de jeton par nom d'utilisateur et mot de passe est parfois signifiant. Peut-être que le jeton est utilisé dans un environnement http (vous devez toujours éviter ce genre de situation) ou un tiers (dans ce cas, vous devez créer un type de jeton différent, utilisez oauth2) et lorsque l'utilisateur fait quelque chose de dangereux comme changer vous devez vous assurer de ne plus utiliser le jeton d'origine car il a peut-être été révélé par l'attaquant à l'aide des outils sniffer ou tcpdump.

Ramwin
la source
Oui, d'accord, vous devriez obtenir un nouveau jeton d'accès par d'autres moyens (qu'un ancien jeton d'accès). Comme avec un jeton d'actualisation (ou l'ancienne façon de forcer une nouvelle connexion avec un mot de passe au moins).
BjornW
1

Si vous remarquez qu'un jeton est comme un cookie de session, vous pouvez vous en tenir à la durée de vie par défaut des cookies de session dans Django: https://docs.djangoproject.com/en/1.4/ref/settings/#session-cookie-age .

Je ne sais pas si Django Rest Framework gère cela automatiquement, mais vous pouvez toujours écrire un court script qui filtre les anciens et les marque comme expirés.

Tomasz Zieliński
la source
1
L'authentification par jeton n'utilise pas de cookies
s29
0

Je pensais juste que j'ajouterais le mien car cela m'a été utile. J'utilise généralement la méthode JWT, mais parfois quelque chose comme ça est mieux. J'ai mis à jour la réponse acceptée pour django 2.1 avec les importations appropriées.

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
la source
0

juste pour continuer à ajouter à la réponse @odedfos, je pense qu'il y a eu quelques changements dans la syntaxe, donc le code de ExpiringTokenAuthentication doit être ajusté:

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

N'oubliez pas non plus de l'ajouter à DEFAULT_AUTHENTICATION_CLASSES au lieu de rest_framework.authentication.TokenAuthentication

Luis Rodriguez-Moldes
la source