Token-Authentifizierung für RESTful API: Sollte das Token regelmäßig geändert werden?

115

Ich erstelle eine RESTful-API mit Django und Django-Rest-Framework .

Als Authentifizierungsmechanismus haben wir "Token-Authentifizierung" ausgewählt und ich habe es bereits gemäß der Dokumentation von Django-REST-Framework implementiert. Die Frage ist, ob die Anwendung das Token regelmäßig erneuern / ändern soll und wenn ja, wie? Sollte es die mobile App sein, für die das Token erneuert werden muss, oder sollte die Web-App dies autonom tun?

Was ist die beste Vorgehensweise?

Hat hier jemand Erfahrung mit Django REST Framework und könnte eine technische Lösung vorschlagen?

(Die letzte Frage hat eine niedrigere Priorität)

nemesisdesign
quelle

Antworten:

101

Es wird empfohlen, dass mobile Clients ihr Authentifizierungstoken regelmäßig erneuern. Dies muss natürlich vom Server durchgesetzt werden.

Die Standard-TokenAuthentication-Klasse unterstützt dies nicht. Sie können sie jedoch erweitern, um diese Funktionalität zu erreichen.

Beispielsweise:

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

Es ist auch erforderlich, die Standard-Anmeldeansicht für das Rest-Framework zu überschreiben, damit das Token bei jeder Anmeldung aktualisiert wird:

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

Und vergessen Sie nicht, die URLs zu ändern:

urlpatterns += patterns(
    '',
    url(r'^users/login/?$', '<path_to_file>.obtain_expiring_auth_token'),
)
odedfos
quelle
6
Möchten Sie nicht ein neues Token in ObtainExpiringAuthToken erstellen, wenn es abgelaufen ist, anstatt nur den Zeitstempel für das alte zu aktualisieren?
Joar Leth
4
Das Erstellen eines neuen Tokens ist sinnvoll. Sie können auch den Wert des vorhandenen Token-Schlüssels neu generieren und müssen dann das alte Token nicht löschen.
odedfos
Was ist, wenn ich den Token nach Ablauf löschen möchte? Wenn ich get_or_create erneut bekomme, wird ein neues Token generiert oder der Zeitstempel aktualisiert?
Sayok88
3
Sie können auch Token vom Tisch verfallen lassen, indem Sie alte regelmäßig in einem Cronjob (Sellerie-Beat oder ähnliches) entfernen, anstatt die Validierung abzufangen
BjörnW
1
@BjornW Ich würde nur die Räumung durchführen und meiner Meinung nach liegt es in der Verantwortung der Person, die sich in die API (oder Ihr Front-End) integriert, eine Anfrage zu stellen. Sie erhalten "Ungültiges Token" und klicken dann auf "Aktualisieren /". Erstellen Sie neue Token-Endpunkte
ShibbySham
24

Wenn jemand an dieser Lösung interessiert ist, aber ein Token haben möchte, das für eine bestimmte Zeit gültig ist, wird es durch ein neues Token ersetzt. Hier ist die vollständige Lösung (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')
)

Ihr Projekt urls.py (im Array 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)

Fügen Sie in Ihren REST_FRAMEWORK-Einstellungen ExpiringTokenAuthentication als Authentifizierungsklasse anstelle von TokenAuthentication hinzu:

REST_FRAMEWORK = {

    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
        #'rest_framework.authentication.TokenAuthentication',
        'yourmodule.authentication.ExpiringTokenAuthentication',
    ),
}
Galex
quelle
Ich erhalte die Fehlermeldung, 'ObtainExpiringAuthToken' object has no attribute 'serializer_class'wenn ich versuche, auf den API-Endpunkt zuzugreifen. Ich bin mir nicht sicher, was mir fehlt.
Dharmit
2
Interessante Lösung, die ich später testen werde; Im Moment hat mir Ihr Beitrag geholfen, auf den richtigen Weg zu kommen, da ich einfach vergessen hatte, die AUTHENTICATION_CLASSES festzulegen.
Normale
2
Ich kam zu spät zur Party, aber ich musste einige subtile Änderungen vornehmen, damit es funktioniert. 1) utc_now = datetime.datetime.utcnow () sollte utc_now = datetime.datetime.utcnow () sein. Ersetzen (tzinfo = pytz.UTC) 2) In der Klasse ExpiringTokenAuthentication (TokenAuthentication): Sie benötigen das Modell self.model = self. get_model ()
Ishan Bhatt
5

Ich habe versucht, @odedfos zu beantworten, aber ich hatte einen irreführenden Fehler . Hier ist die gleiche Antwort, fest und mit richtigen Importen.

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
quelle
4

Ich dachte, ich würde eine Django 2.0-Antwort mit DRY geben. Jemand hat dies bereits für uns entwickelt, Google Django OAuth ToolKit. Erhältlich mit pip , pip install django-oauth-toolkit. Anweisungen zum Hinzufügen des Tokens ViewSets mit Routern: https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html . Es ähnelt dem offiziellen Tutorial.

Im Grunde genommen war OAuth1.0 also mehr Sicherheit von gestern, was TokenAuthentication ist. OAuth2.0 ist heutzutage der letzte Schrei, um ausgefallene Token zu erhalten. Sie erhalten eine AccessToken-, RefreshToken- und Bereichsvariable, um die Berechtigungen zu optimieren. Sie haben am Ende folgende Creds:

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

Der Autor fragte

Die Frage ist, sollte die Anwendung das Token regelmäßig erneuern / ändern und wenn ja, wie? Sollte es die mobile App sein, für die das Token erneuert werden muss, oder sollte die Web-App dies autonom tun?

In allen Antworten wird jedoch darüber geschrieben, wie das Token automatisch geändert werden kann.

Ich denke, Token regelmäßig durch Token zu ändern ist bedeutungslos. Das Rest-Framework erstellt ein Token mit 40 Zeichen. Wenn der Angreifer 1000 Token pro Sekunde testet, dauert es 16**40/1000/3600/24/365=4.6*10^7Jahre, bis das Token abgerufen wird. Sie sollten sich keine Sorgen machen, dass der Angreifer Ihren Token einzeln testet. Selbst wenn Sie Ihren Token geändert haben, ist die Wahrscheinlichkeit, dass Sie Ihren Token erraten, gleich.

Wenn Sie befürchten, dass die Angreifer Ihnen möglicherweise einen Token erhalten, ändern Sie ihn regelmäßig. Nachdem der Angreifer den Token erhalten hat, kann er Sie auch ändern, bevor der echte Benutzer rausgeschmissen wird.

Was Sie wirklich tun sollten, ist zu verhindern, dass der Angreifer das Token Ihres Benutzers erhält. Verwenden Sie https .

Übrigens sage ich nur, dass das Ändern von Token für Token bedeutungslos ist. Das Ändern von Token für Benutzername und Passwort ist manchmal sinnvoll. Möglicherweise wird das Token in einer http-Umgebung (Sie sollten diese Situation immer vermeiden) oder in einem Drittanbieter (in diesem Fall sollten Sie eine andere Art von Token erstellen, oauth2 verwenden) verwendet, und wenn der Benutzer gefährliche Aktionen wie Änderungen ausführt Wenn Sie ein Postfach binden oder ein Konto löschen, sollten Sie sicherstellen, dass Sie das Ursprungs-Token nicht mehr verwenden, da es möglicherweise vom Angreifer mithilfe von Sniffer- oder TCPDump-Tools aufgedeckt wurde.

Ramwin
quelle
Ja, stimmen Sie zu, Sie sollten ein neues Zugriffstoken auf andere Weise erhalten (als durch ein altes Zugriffstoken). Wie bei einem Aktualisierungstoken (oder der alten Methode, zumindest eine neue Anmeldung mit Kennwort zu erzwingen).
BjörnW
1

Wenn Sie feststellen, dass ein Token wie ein Sitzungscookie ist, können Sie sich an die Standardlebensdauer von Sitzungscookies in Django halten: https://docs.djangoproject.com/de/1.4/ref/settings/#session-cookie-age .

Ich weiß nicht, ob Django Rest Framework dies automatisch erledigt, aber Sie können jederzeit ein kurzes Skript schreiben, das die veralteten herausfiltert und sie als abgelaufen markiert.

Tomasz Zieliński
quelle
1
Token-Authentifizierung verwendet keine Cookies
s29
0

Ich dachte nur, ich würde meine hinzufügen, da dies für mich hilfreich war. Normalerweise gehe ich mit der JWT-Methode, aber manchmal ist so etwas besser. Ich habe die akzeptierte Antwort für Django 2.1 mit den richtigen Importen aktualisiert.

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
quelle
0

Ich denke, es wurden einige Änderungen an der Syntax vorgenommen, sodass der Code von ExpiringTokenAuthentication angepasst werden muss, um die Antwort von @odedfos weiter zu ergänzen:

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

Vergessen Sie auch nicht, es zu DEFAULT_AUTHENTICATION_CLASSES anstelle von rest_framework.authentication.TokenAuthentication hinzuzufügen

Luis Rodriguez-Moldes
quelle