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

115

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

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

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

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

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

nemesisdesign
источник

Ответы:

102

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

Класс 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 framework по умолчанию, чтобы токен обновлялся при каждом входе в систему:

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'),
)
odedfos
источник
6
Разве вы не хотели бы создать новый токен в ObtainExpiringAuthToken, если он истек, а не просто обновить временную метку для старого?
Joar Leth
4
Создание нового токена имеет смысл. Вы также можете восстановить значение существующего ключа токенов, и тогда вам не придется удалять старый токен.
odedfos 07
Что, если я хочу очистить токен по истечении срока действия? Когда я снова буду get_or_create, будет ли сгенерирован новый токен или обновлена ​​временная метка?
Sayok88
3
Кроме того, вы можете истечь токены со стола, периодически удаляя старые в cronjob (Celery Beat или аналогичный), вместо перехвата проверки
BjornW
1
@BjornW Я бы просто выполнил выселение, и, на мой взгляд, человек, интегрирующийся с API (или вашим интерфейсом), должен сделать запрос, он получит «Недействительный токен», а затем нажмите кнопку обновления / создать новые конечные точки токенов
ShibbySham
25

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

REST_FRAMEWORK = {

    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
        #'rest_framework.authentication.TokenAuthentication',
        'yourmodule.authentication.ExpiringTokenAuthentication',
    ),
}
Галекс
источник
Я получаю сообщение об ошибке 'ObtainExpiringAuthToken' object has no attribute 'serializer_class'при попытке доступа к конечной точке API. Не уверен, что мне не хватает.
Дхармит
2
Интересное решение, которое я опробую позже; на данный момент ваш пост помог мне встать на правильный путь, поскольку я просто забыл установить AUTHENTICATION_CLASSES.
normic
2
Опоздал на вечеринку, но мне нужно было внести некоторые тонкие изменения, чтобы все заработало. 1) utc_now = datetime.datetime.utcnow () должно быть utc_now = datetime.datetime.utcnow (). Replace (tzinfo = pytz.UTC) 2) В классе ExpiringTokenAuthentication (TokenAuthentication): вам нужна модель, self.model = self. get_model ()
Ишан Бхатт
5

Я пробовал ответить @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

Думал, что дам ответ Django 2.0, используя DRY. Кто-то уже создал это для нас, google Django OAuth ToolKit. Доступно с пип, 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 и переменную области для точной настройки разрешений. В итоге вы получите такие кредиты:

{
    "access_token": "<your_access_token>",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "<your_refresh_token>",
    "scope": "read"
}
Райан Дайнс
источник
4

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

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

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

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

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

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

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

Рамвин
источник
Да, согласен, вы должны получить новый токен доступа каким-либо другим способом (кроме старого токена доступа). Например, с токеном обновления (или, по крайней мере, старым способом принудительно ввести новый логин с паролем).
BjornW
3

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

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

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

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

Ангки Уильям
источник
1

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

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

Томаш Зелиньски
источник
1
Для аутентификации токена не используются файлы cookie
29
0

Просто подумал, что добавлю свой, так как это было полезно для меня. Обычно я использую метод 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)
wdfc
источник
0

просто чтобы продолжать добавлять в ответ @odedfos, я думаю, что были некоторые изменения в синтаксисе, поэтому код ExpiringTokenAuthentication нуждается в некоторой корректировке:

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

Также не забудьте добавить его в DEFAULT_AUTHENTICATION_CLASSES вместо rest_framework.authentication.TokenAuthentication.

Луис Родригес-Мольдес
источник