Uwierzytelnianie tokenu dla RESTful API: czy token powinien być okresowo zmieniany?


115

Buduję RESTful API z Django i django-rest-framework .

Jako mechanizm uwierzytelniania wybraliśmy "Token Authentication" i już zaimplementowałem go zgodnie z dokumentacją Django-REST-Framework, pytanie brzmi, czy aplikacja powinna odnawiać / zmieniać token okresowo, a jeśli tak, to w jaki sposób? Czy to aplikacja mobilna powinna wymagać odnowienia tokena, czy też aplikacja internetowa powinna to robić autonomicznie?

Jaka jest najlepsza praktyka?

Czy ktoś ma doświadczenie z Django REST Framework i może zasugerować rozwiązanie techniczne?

(ostatnie pytanie ma niższy priorytet)

Odpowiedzi:


102

Dobrą praktyką jest okresowe odnawianie tokenu uwierzytelniania przez klientów mobilnych. To oczywiście zależy od serwera.

Domyślna klasa TokenAuthentication nie obsługuje tego, jednak można ją rozszerzyć, aby uzyskać tę funkcjonalność.

Na przykład:

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

Wymagane jest również nadpisanie domyślnego widoku logowania do struktury odpoczynku, aby token był odświeżany po każdym logowaniu:

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

I nie zapomnij zmodyfikować adresów URL:

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

6
Czy nie chciałbyś utworzyć nowego tokenu w ObtainExpiringAuthToken, jeśli wygasł, zamiast po prostu zaktualizować sygnaturę czasową starego?
Joar Leth

4
Utworzenie nowego tokena ma sens. Możesz również ponownie wygenerować wartość istniejącego klucza tokenów, a wtedy nie będziesz musiał usuwać starego tokena.
odedfos

Co jeśli chcę wyczyścić token po wygaśnięciu? Kiedy ponownie get_or_create zostanie wygenerowany nowy token lub zostanie zaktualizowany znacznik czasu?
Sayok88,

3
Ponadto możesz stracić tokeny ze stołu, okresowo usuwając stare w cronjob (Celery Beat lub podobny), zamiast przechwytywania weryfikacji
BjornW

1
@BjornW Po prostu dokonałbym eksmisji i moim zdaniem obowiązkiem osoby integrującej się z API (lub Twoim front-endem) jest wysłanie żądania, otrzyma „Invalid token”, a następnie kliknie przycisk refresh / utwórz nowe punkty końcowe tokenów
ShibbySham

25

Jeśli ktoś jest zainteresowany tym rozwiązaniem, ale chce mieć token ważny przez określony czas to zostanie zastąpiony nowym tokenem, oto kompletne rozwiązanie (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')
)

twój projekt urls.py (w tablicy 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)

W ustawieniach REST_FRAMEWORK dodaj ExpiringTokenAuthentication jako klasę uwierzytelniania zamiast TokenAuthentication:

REST_FRAMEWORK = {

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

Otrzymuję błąd, 'ObtainExpiringAuthToken' object has no attribute 'serializer_class'gdy próbuję uzyskać dostęp do punktu końcowego interfejsu API. Nie wiem, czego mi brakuje.
Dharmit

2
Ciekawe rozwiązanie, które później przetestuję; w tej chwili Twój post pomógł mi wejść na właściwą ścieżkę, ponieważ po prostu zapomniałem ustawić AUTHENTICATION_CLASSES.
normic

2
Spóźniłem się na imprezę, ale musiałem wprowadzić pewne subtelne zmiany, aby to zadziałało. 1) utc_now = datetime.datetime.utcnow () powinno być utc_now = datetime.datetime.utcnow (). Replace (tzinfo = pytz.UTC) 2) W klasie ExpiringTokenAuthentication (TokenAuthentication): Potrzebujesz modelu self.model = self. get_model ()
Ishan Bhatt,

5

Próbowałem odpowiedzieć @odedfos, ale wystąpił błąd wprowadzający w błąd . Oto ta sama odpowiedź, ustalona i z odpowiednim importem.

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

Pomyślałem, że udzielę odpowiedzi w Django 2.0 używając DRY. Ktoś już to dla nas zbudował, Google Django OAuth ToolKit. Dostępne z pip, pip install django-oauth-toolkit. Instrukcje dotyczące dodawania tokenów ViewSets z routerami: https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html . Jest podobny do oficjalnego samouczka.

Zasadniczo OAuth1.0 było bardziej wczorajszym zabezpieczeniem, którym jest TokenAuthentication. Aby uzyskać wymyślne wygasające tokeny, w dzisiejszych czasach popularna jest OAuth2.0. Otrzymasz AccessToken, RefreshToken i zmienną zakresu, aby dostosować uprawnienia. Otrzymasz takie kredyty:

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

4

Zapytał autor

pytanie brzmi, czy aplikacja powinna odnawiać / zmieniać Token okresowo, a jeśli tak, to w jaki sposób? Czy to aplikacja mobilna powinna wymagać odnowienia tokena, czy też aplikacja internetowa powinna to robić autonomicznie?

Ale wszystkie odpowiedzi piszą o tym, jak automatycznie zmienić token.

Myślę, że okresowe zmienianie tokena po tokenie jest bez znaczenia. Reszta frameworka tworzy token, który ma 40 znaków, jeśli atakujący testuje 1000 tokenów na sekundę, 16**40/1000/3600/24/365=4.6*10^7zdobycie tokena zajmuje lata. Nie powinieneś się martwić, że atakujący przetestuje twój żeton jeden po drugim. Nawet jeśli zmieniłeś swój token, prawdopodobieństwo odgadnięcia, że ​​token jest takie samo.

Jeśli obawiasz się, że może napastnicy mogą dostać twój token, więc zmieniasz go okresowo, to po tym, jak atakujący otrzyma token, może również zmienić twój token, a prawdziwy użytkownik jest wyrzucany.

To, co naprawdę powinieneś zrobić, to uniemożliwić atakującemu uzyskanie tokena użytkownika, użyj https .

Nawiasem mówiąc, mówię tylko, że zmiana tokena po tokenie jest bez znaczenia, zmiana tokena według nazwy użytkownika i hasła jest czasami znacząca. Może token jest używany w jakimś środowisku http (zawsze należy unikać tego typu sytuacji) lub w jakiejś trzeciej firmie (w tym przypadku należy stworzyć inny rodzaj tokena, użyć oauth2) i gdy użytkownik robi coś niebezpiecznego, jak np. Zmiana wiążąc skrzynkę pocztową lub usuń konto, powinieneś upewnić się, że nie będziesz już używać tokena pochodzenia, ponieważ mógł zostać ujawniony przez atakującego za pomocą narzędzi sniffer lub tcpdump.


Tak, zgadzam się, nowy token dostępu należy uzyskać w inny sposób (niż stary token dostępu). Podobnie jak w przypadku odświeżania tokena (lub przynajmniej starego sposobu wymuszania nowego logowania z hasłem).
BjornW



0

Pomyślałem, że dodam moje, ponieważ było to dla mnie pomocne. Zwykle korzystam z metody JWT, ale czasami coś takiego jest lepsze. Zaktualizowałem zaakceptowaną odpowiedź dla django 2.1 z poprawnymi importami.

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

aby nadal dodawać do odpowiedzi @odedfos, myślę, że nastąpiły pewne zmiany w składni, więc kod ExpiringTokenAuthentication wymaga dostosowania:

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

Nie zapomnij również dodać go do DEFAULT_AUTHENTICATION_CLASSES zamiast rest_framework.authentication.TokenAuthentication

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.