Autenticazione token per API RESTful: il token dovrebbe essere cambiato periodicamente?


115

Sto costruendo un'API RESTful con Django e django-rest-framework .

Come meccanismo di autenticazione abbiamo scelto "Token Authentication" e l'ho già implementato seguendo la documentazione di Django-REST-Framework, la domanda è, l'applicazione dovrebbe rinnovare / cambiare periodicamente il Token e se sì come? Dovrebbe essere l'app mobile a richiedere il rinnovo del token o la web-app dovrebbe farlo in autonomia?

Qual è la migliore pratica?

Qualcuno qui ha esperienza con Django REST Framework e potrebbe suggerire una soluzione tecnica?

(l'ultima domanda ha una priorità inferiore)

Risposte:


101

È buona norma che i client mobili rinnovino periodicamente il token di autenticazione. Questo ovviamente spetta al server applicarlo.

La classe TokenAuthentication predefinita non lo supporta, tuttavia è possibile estenderla per ottenere questa funzionalità.

Per esempio:

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

È inoltre necessario sovrascrivere la visualizzazione di accesso del framework rest predefinita, in modo che il token venga aggiornato ogni volta che viene eseguito un accesso:

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

E non dimenticare di modificare gli URL:

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

6
Non vorresti creare un nuovo token in ObtainExpiringAuthToken se è scaduto, invece di aggiornare semplicemente il timestamp per quello vecchio?
Joar Leth,

4
La creazione di un nuovo token ha senso. Potresti anche rigenerare il valore della chiave del token esistente e quindi non dovresti eliminare il vecchio token.
odedfos

E se volessi cancellare il token alla scadenza? Quando get_or_create di nuovo verrà generato un nuovo token o verrà aggiornato il timestamp?
Sayok88

3
Inoltre, potresti far scadere i gettoni dal tavolo sfrattando periodicamente quelli vecchi in un cronjob (Celery Beat o simile), invece di intercettare la convalida
BjornW

1
@ BjornW Vorrei solo eseguire lo sfratto e, secondo me, è responsabilità della persona che si integra con l'API (o il tuo front-end) fare una richiesta, riceve "token non valido", quindi premi il pulsante di aggiornamento / creare nuovi endpoint di token
ShibbySham

25

Se qualcuno è interessato a quella soluzione ma desidera avere un token valido per un certo periodo di tempo, viene sostituito da un nuovo token, ecco la soluzione completa (Django 1.6):

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

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

il tuo progetto urls.py (nell'array urlpatterns):

url(r'^', include('yourmodule.urls')),

nomemodulo / 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)

Nelle impostazioni REST_FRAMEWORK aggiungi ExpiringTokenAuthentication come classe di autenticazione invece di TokenAuthentication:

REST_FRAMEWORK = {

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

Ricevo l'errore 'ObtainExpiringAuthToken' object has no attribute 'serializer_class'quando provo ad accedere all'endpoint api. Non sono sicuro di cosa mi sto perdendo.
Dharmit

2
Soluzione interessante, che proverò più tardi; al momento il tuo post mi ha aiutato a prendere la strada giusta perché mi ero semplicemente dimenticato di impostare le AUTHENTICATION_CLASSES.
normic

2
Sono arrivato tardi alla festa ma avevo bisogno di apportare alcune sottili modifiche per farlo funzionare. 1) utc_now = datetime.datetime.utcnow () dovrebbe essere utc_now = datetime.datetime.utcnow (). Replace (tzinfo = pytz.UTC) 2) Nella classe ExpiringTokenAuthentication (TokenAuthentication): Hai bisogno di modello, self.model = self. get_model ()
Ishan Bhatt

5

Ho provato la risposta @odedfos ma ho riscontrato un errore fuorviante . Ecco la stessa risposta, corretta e con importazioni corrette.

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)

4

Ho pensato di dare una risposta Django 2.0 usando DRY. Qualcuno lo ha già creato per noi, Google Django OAuth ToolKit. Disponibile con pip, pip install django-oauth-toolkit. Istruzioni per l'aggiunta dei token ViewSet con i router: https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html . È simile al tutorial ufficiale.

Quindi fondamentalmente OAuth1.0 era più la sicurezza di ieri che è ciò che è TokenAuthentication. Per ottenere token in scadenza di fantasia, OAuth2.0 è di gran moda in questi giorni. Ottieni un AccessToken, RefreshToken e una variabile di ambito per ottimizzare le autorizzazioni. Finisci con crediti come questo:

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

4

L'autore ha chiesto

la domanda è: l'applicazione dovrebbe rinnovare / cambiare periodicamente il token e se sì come? Dovrebbe essere l'app mobile a richiedere il rinnovo del token o la web-app dovrebbe farlo in autonomia?

Ma tutte le risposte stanno scrivendo su come cambiare automaticamente il token.

Penso che cambiare token periodicamente per token non abbia senso. Il resto del framework crea un token che ha 40 caratteri, se l'attaccante testa 1000 token ogni secondo, ci vogliono 16**40/1000/3600/24/365=4.6*10^7anni per ottenere il token. Non dovresti preoccuparti che l'attaccante metta alla prova il tuo gettone uno per uno. Anche se hai cambiato il tuo token, la probabilità di indovinarlo è la stessa.

Se sei preoccupato che forse gli aggressori possano prenderti il ​​token, quindi lo cambi periodicamente, quindi dopo che l'attaccante ha ottenuto il token, può anche cambiare il tuo token, quindi l'utente reale viene espulso.

Quello che dovresti davvero fare è impedire all'autore dell'attacco di ottenere il token dell'utente, usa https .

A proposito, sto solo dicendo che cambiare token per token non ha senso, cambiare token per nome utente e password a volte è significativo. Forse il token viene utilizzato in qualche ambiente http (dovresti sempre evitare questo tipo di situazione) o qualche terza parte (in questo caso, dovresti creare diversi tipi di token, usa oauth2) e quando l'utente sta facendo qualcosa di pericoloso come cambiare vincolando la casella di posta o eliminando l'account, dovresti assicurarti di non utilizzare più il token di origine perché potrebbe essere stato rivelato dall'aggressore utilizzando gli strumenti sniffer o tcpdump.


Sì, d'accordo, dovresti ottenere un nuovo token di accesso con altri mezzi (rispetto a un vecchio token di accesso). Come con un token di aggiornamento (o almeno il vecchio modo di forzare un nuovo accesso con password).
BjornW



0

Ho solo pensato di aggiungere il mio perché mi è stato utile. Di solito vado con il metodo JWT, ma a volte qualcosa di simile è migliore. Ho aggiornato la risposta accettata per django 2.1 con importazioni corrette ..

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

solo per continuare ad aggiungere alla risposta @odedfos, penso che ci siano state alcune modifiche alla sintassi, quindi il codice di ExpiringTokenAuthentication necessita di qualche aggiustamento:

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

Inoltre, non dimenticare di aggiungerlo a DEFAULT_AUTHENTICATION_CLASSES invece di rest_framework.authentication.TokenAuthentication

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.