Аутентификация Token для RESTful API: следует ли периодически менять токен?

Я создаю RESTful API с Django и django-rest-framework.

В качестве механизма аутентификации мы выбрали "Аутентификацию токена", и я уже реализовал его после документации Django-REST-Framework, вопрос в том, должно ли приложение периодически обновлять/изменять токен, и если да, то как? Должно ли быть мобильным приложением, которое требует обновления токена или веб-приложение должно делать это автономно?

Какова наилучшая практика?

Кто-нибудь здесь сталкивался с Django REST Framework и мог предложить техническое решение?

(последний вопрос имеет более низкий приоритет)

Ответ 1

Рекомендуется, чтобы мобильные клиенты периодически обновляли свой токен аутентификации. Это, конечно, зависит от сервера.

Класс TokenAuthentication по умолчанию не поддерживает это, однако вы можете расширить его для достижения этой функциональности.

Например:

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

Также необходимо переопределить стандартный вид входа в систему rest rest, чтобы токен обновлялся при каждом входе в систему:

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

И не забудьте изменить URL:

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

Ответ 2

Если кто-то заинтересован этим решением, но хочет иметь токен, действительный в течение определенного времени, то получает замененный новым токеном здесь полное решение (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')
)

ваш проект urls.py(в массиве 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)

В настройках REST_FRAMEWORK добавьте ExpiringTokenAuthentication в качестве класса Authentification вместо TokenAuthentication:

REST_FRAMEWORK = {

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

Ответ 3

Я пробовал ответить @odedfos, но У меня была ошибка в заблуждении. Вот тот же ответ, исправленный и с надлежащим импортом.

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

Вы можете использовать http://getblimp.github.io/django-rest-framework-jwt

Эта библиотека способна генерировать токен, срок действия которого

Чтобы понять разницу между токеном по умолчанию DRF и маркером, предоставляемым DRF, посмотрите:

Как сделать шкалу аутентификации Jango REST JWT с веб-серверами mulitple?

Ответ 5

Думаю, я бы дал ответ Django 2.0, используя DRY. Кто-то уже разработал это для нас, Google Django OAuth ToolKit. Доступен с pip, pip install django-oauth-toolkit. Инструкции по добавлению токенов ViewSets с маршрутизаторами: https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html. Это похоже на официальный учебник.

Таким образом, OAuth1.0 в большей степени был вчерашней безопасностью, чем и является TokenAuthentication. Чтобы получить модные токены с истекающим сроком действия, OAuth2.0 сейчас в моде. Вы получаете переменные AccessToken, RefreshToken и scope для точной настройки разрешений. В итоге вы получаете кредиты вроде этого:

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

Ответ 6

Если вы заметили, что токен похож на cookie сеанса, вы можете придерживаться времени жизни cookie сеанса по умолчанию в Django: https://docs.djangoproject.com/en/1.4/ref/settings/#session-cookie-age.

Я не знаю, обрабатывает ли Django Rest Framework это автоматически, но вы всегда можете написать короткий script, который отфильтровывает устаревшие и маркирует их как истек.

Ответ 7

Автор спросил

вопрос в том, должно ли приложение периодически обновлять/менять токен, и если да, то как? Должно ли мобильное приложение обновлять токен, или веб-приложение должно делать это автономно?

Но все ответы пишут о том, как автоматически менять токен.

Я думаю, что менять токен периодически токеном бессмысленно. Остальная структура создает токен, который имеет 40 символов. Если злоумышленник проверяет 1000 токенов каждую секунду, для получения токена требуется 16**40/1000/3600/24/365=4.6*10^7 лет. Вы не должны беспокоиться о том, что атакующий будет проверять ваш токен один за другим. Даже если вы изменили свой токен, вероятность угадать ваш токен одинакова.

Если вас беспокоит, что, возможно, злоумышленники могут получить ваш токен, поэтому вы периодически меняете его, чем после того, как злоумышленник получит токен, он также может сменить ваш токен, чем выгонят реального пользователя.

Что вы действительно должны сделать, это запретить злоумышленнику получить ваш пользовательский токен, используйте https.

Кстати, я просто говорю, что смена токена на токен не имеет смысла, изменение токена по имени пользователя и паролю иногда имеет смысл. Может быть, токен используется в какой-то среде http (вы всегда должны избегать такого рода ситуаций) или в какой-либо третьей стороне (в этом случае вам следует создать токен другого типа, используйте oauth2), а когда пользователь делает что-то опасное, например, изменение привязывая почтовый ящик или удаляя учетную запись, вы должны быть уверены, что больше не будете использовать маркер происхождения, поскольку он может быть обнаружен злоумышленником с помощью инструментов sniffer или tcpdump.

Ответ 8

Просто думал, что добавлю свой, так как это было полезно для меня. Я обычно использую метод JWT, но иногда что-то вроде этого лучше. Я обновил принятый ответ для Django 2.1 с правильным импортом.

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)