Autenticación de token para la API RESTful: ¿se debe cambiar el token periódicamente?

115

Estoy construyendo una API RESTful con Django y django-rest-framework .

Como mecanismo de autenticación hemos elegido "Autenticación de Token" y ya lo he implementado siguiendo la documentación de Django-REST-Framework, la pregunta es, ¿la aplicación debe renovar / cambiar el Token periódicamente y en caso afirmativo cómo? ¿Debería ser la aplicación móvil la que requiera la renovación del token o la aplicación web debería hacerlo de forma autónoma?

cual es la mejor practica?

¿Alguien aquí tiene experiencia con Django REST Framework y podría sugerir una solución técnica?

(la última pregunta tiene menor prioridad)

diseño de némesis
fuente

Respuestas:

101

Es una buena práctica que los clientes móviles renueven periódicamente su token de autenticación. Por supuesto, esto depende del servidor para hacer cumplir.

La clase TokenAuthentication predeterminada no admite esto, sin embargo, puede ampliarla para lograr esta funcionalidad.

Por ejemplo:

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

También es necesario anular la vista de inicio de sesión del marco de descanso predeterminado, de modo que el token se actualice cada vez que se realiza un inicio de sesión:

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

Y no olvide modificar las URL:

urlpatterns += patterns(
    '',
    url(r'^users/login/?$', '<path_to_file>.obtain_expiring_auth_token'),
)
odedfos
fuente
6
Sin embargo, ¿no le gustaría crear un nuevo token en ObtainExpiringAuthToken si está vencido, en lugar de simplemente actualizar la marca de tiempo del anterior?
Joar Leth
4
Crear un nuevo token tiene sentido. También podría volver a generar el valor de la clave de tokens existente y luego no tendría que eliminar el token anterior.
odedfos
¿Qué pasa si quiero borrar el token al vencimiento? Cuando vuelva a get_or_create, ¿se generará un nuevo token o se actualizará la marca de tiempo?
Sayok88
3
Además, puede caducar los tokens de la tabla desalojando los antiguos periódicamente en un cronjob (Celery Beat o similar), en lugar de interceptar la validación
BjornW
1
@BjornW Solo haría el desalojo y, en mi opinión, es responsabilidad de la persona que se integra con la API (o su interfaz) hacer una solicitud, reciben, "Token no válido", y luego presionan el botón actualizar / crear nuevos puntos finales de tokens
ShibbySham
25

Si alguien está interesado en esa solución pero quiere tener un token que sea válido por un tiempo determinado, entonces es reemplazado por un nuevo token, aquí está la solución completa (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')
)

su proyecto urls.py (en la matriz 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)

En la configuración de REST_FRAMEWORK, agregue ExpiringTokenAuthentication como una clase de autenticación en lugar de TokenAuthentication:

REST_FRAMEWORK = {

    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
        #'rest_framework.authentication.TokenAuthentication',
        'yourmodule.authentication.ExpiringTokenAuthentication',
    ),
}
Galex
fuente
Recibo el error 'ObtainExpiringAuthToken' object has no attribute 'serializer_class'cuando intento acceder al punto final de la API. No estoy seguro de lo que me estoy perdiendo.
Dharmit
2
Solución interesante, que probaré más tarde; en este momento, tu publicación me ayudó a seguir el camino correcto, ya que simplemente me olvidé de configurar las AUTHENTICATION_CLASSES.
normic
2
Llegué tarde a la fiesta, pero necesitaba hacer algunos cambios sutiles para que funcionara. 1) utc_now = datetime.datetime.utcnow () debe ser utc_now = datetime.datetime.utcnow (). Replace (tzinfo = pytz.UTC) 2) En la clase ExpiringTokenAuthentication (TokenAuthentication): Necesita modelo, self.model = self. get_model ()
Ishan Bhatt
5

Probé la respuesta de @odedfos pero tuve un error engañoso . Aquí está la misma respuesta, arreglada y con las importaciones adecuadas.

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)
Benjamín Toueg
fuente
4

Pensé que daría una respuesta de Django 2.0 usando DRY. Alguien ya creó esto para nosotros, Google Django OAuth ToolKit. Disponible con PIP, pip install django-oauth-toolkit. Instrucciones sobre cómo agregar los ViewSets de token con enrutadores: https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html . Es similar al tutorial oficial.

Entonces, básicamente, OAuth1.0 era más la seguridad de ayer, que es lo que es TokenAuthentication. Para obtener tokens elegantes con vencimiento, OAuth2.0 está de moda en estos días. Obtiene un AccessToken, RefreshToken y una variable de alcance para ajustar los permisos. Terminas con credenciales como esta:

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

El autor preguntó

la pregunta es, ¿la aplicación debe renovar / cambiar el token periódicamente y, en caso afirmativo, cómo? ¿Debería ser la aplicación móvil la que requiera la renovación del token o la aplicación web debería hacerlo de forma autónoma?

Pero todas las respuestas están escribiendo sobre cómo cambiar automáticamente el token.

Creo que cambiar token periódicamente por token no tiene sentido. El resto del marco crea un token que tiene 40 caracteres, si el atacante prueba 1000 token por segundo, se necesitan 16**40/1000/3600/24/365=4.6*10^7años para obtener el token. No debe preocuparse de que el atacante pruebe su token uno por uno. Incluso si cambió su ficha, la probabilidad de adivinar su ficha es la misma.

Si le preocupa que tal vez los atacantes puedan obtener su token, entonces lo cambia periódicamente, después de que el atacante obtenga el token, también puede cambiar su token, entonces el usuario real es expulsado.

Lo que realmente debe hacer es evitar que el atacante obtenga el token de su usuario, use https .

Por cierto, solo digo que cambiar token por token no tiene sentido, cambiar token por nombre de usuario y contraseña a veces es significativo. Tal vez el token se use en algún entorno http (siempre debe evitar este tipo de situación) o en algún tercero (en este caso, debe crear un tipo diferente de token, use oauth2) y cuando el usuario esté haciendo algo peligroso como cambiar vincular el buzón o eliminar la cuenta, debe asegurarse de que no volverá a usar el token de origen porque puede haber sido revelado por el atacante usando herramientas sniffer o tcpdump.

ramwin
fuente
Sí, de acuerdo, debería obtener un nuevo token de acceso por algún otro medio (que no sea un antiguo token de acceso). Como con un token de actualización (o la antigua forma de forzar un nuevo inicio de sesión con contraseña al menos).
BjornW
1

Si nota que un token es como una cookie de sesión, puede ceñirse a la duración predeterminada de las cookies de sesión en Django: https://docs.djangoproject.com/en/1.4/ref/settings/#session-cookie-age .

No sé si Django Rest Framework maneja eso automáticamente, pero siempre puede escribir un script corto que filtre los desactualizados y los marque como vencidos.

Tomasz Zieliński
fuente
1
La autenticación de token no utiliza cookies
s29
0

Solo pensé en agregar el mío, ya que esto fue útil para mí. Yo suelo usar el método JWT, pero a veces algo como esto es mejor. Actualicé la respuesta aceptada para django 2.1 con las importaciones adecuadas.

autenticación.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
fuente
0

solo para seguir agregando a la respuesta de @odedfos, creo que ha habido algunos cambios en la sintaxis, por lo que el código de ExpiringTokenAuthentication necesita algunos ajustes:

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

Además, no olvide agregarlo a DEFAULT_AUTHENTICATION_CLASSES en lugar de rest_framework.authentication.TokenAuthentication

Luis Rodríguez-Moldes
fuente