web-dev-qa-db-fra.com

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

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

En tant que mécanisme d'authentification, nous avons choisi "Authentification par jeton" et je l'ai déjà implémenté après la documentation de Django-REST-Framework. La question est de savoir si l'application doit renouveler/changer le jeton périodiquement et si oui, comment? Devrait-ce être l'application mobile qui nécessite le renouvellement du jeton ou l'application Web devrait le faire de manière autonome?

Quelle est la meilleure pratique?

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

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

92
nemesisdesign

Il est recommandé de demander aux clients mobiles de renouveler périodiquement leur jeton d'authentification. Ceci est bien sûr à la charge du serveur.

La classe TokenAuthentication par défaut ne prend pas cela en charge, mais vous pouvez l'étendre pour atteindre 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 au cadre de repos par défaut 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'),
)
81
odedfos

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')),

votremodule/authentification.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',
    ),
}
20
galex

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

Vous pouvez utiliser http://getblimp.github.io/Django-rest-framework-jwt

Cette bibliothèque peut générer un jeton ayant une date d'expiration.

Pour comprendre la différence entre le jeton DRF par défaut et le jeton fourni par le DRF, consultez:

Comment créer une authentification Django REST JWT Authentication avec plusieurs serveurs Web?

3
Angky William

Si vous remarquez qu'un jeton ressemble à 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/fr/1.4/ref/settings/#session-cookie- âge .

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

1
Tomasz Zielinski

Je pensais donner une réponse Django 2.0 en utilisant DRY. Quelqu'un a déjà construit ça pour nous, google Django OAuth ToolKit. Disponible avec pip, pip install Django-oauth-toolkit. Instructions sur l'ajout du jeton ViewSets avec les routeurs: https://Django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html . C'est semblable au tutoriel officiel.

Donc, fondamentalement, OAuth1.0 était plus la sécurité d'hier et c'est ce que TokenAuthentication est OAuth2.0 fait fureur ces jours-ci. Vous obtenez une variable AccessToken, RefreshToken et scope pour affiner les autorisations. Vous vous retrouvez avec des créations comme celle-ci:

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

Je pensais juste que j'ajouterais le mien car cela m'a été utile. Je vais généralement avec 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)
0
wdfc

L'auteur a demandé

la question est de savoir si l’application doit renouveler/changer le jeton périodiquement et si oui, comment? Devrait-ce être l'application mobile qui nécessite le renouvellement du jeton ou l'application Web devrait 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 le changement de jeton périodique par jeton n'a pas de sens. La structure restante crée un jeton de 40 caractères. Si l'attaquant teste 1 000 jetons toutes les secondes, il faut 16**40/1000/3600/24/365=4.6*10^7 années pour obtenir le jeton. Vous ne devriez pas craindre que l'attaquant teste votre jeton un à un. Même si vous avez changé votre jeton, la probabilité de le deviner est la même.

Si vous craignez que les attaquants puissent vous obtenir un jeton, vous devez le changer périodiquement puis, une fois que l'attaquant a récupéré le jeton, il peut également le modifier, puis expulser le véritable utilisateur. 

Ce que vous devez réellement faire est d’empêcher l’attaquant de récupérer le jeton de votre utilisateur, utilisez https .

En passant, je dis simplement que changer de jeton par jeton n'a pas de sens, changer de jeton par nom d'utilisateur et mot de passe est parfois méchant. Peut-être que le jeton est utilisé dans un environnement http (vous devriez 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 en liant une boîte aux lettres ou en supprimant un compte, vous devez vous assurer que vous n'utiliserez plus le jeton Origin car il peut avoir été révélé par l'attaquant à l'aide d'outils de détection (sniffer) ou de tcpdump.

0
ramwin