Autenticação de token para API RESTful: o token deve ser alterado periodicamente?

115

Estou construindo uma API RESTful com Django e django-rest-framework .

Escolhemos como mecanismo de autenticação "Autenticação de Token" e já implementei seguindo a documentação do Django-REST-Framework, a pergunta é, a aplicação deve renovar / mudar o Token periodicamente e se sim como? Deve ser o aplicativo móvel que requer a renovação do token ou o aplicativo da web deve fazer isso de forma autônoma?

Qual é a melhor prática?

Alguém aqui com experiência com Django REST Framework e poderia sugerir uma solução técnica?

(a última pergunta tem prioridade mais baixa)

nemesisdesign
fonte

Respostas:

101

É uma boa prática ter clientes móveis renovando periodicamente seu token de autenticação. É claro que isso depende do servidor.

A classe TokenAuthentication padrão não oferece suporte para isso, mas você pode estendê-la para obter essa funcionalidade.

Por exemplo:

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

Também é necessário substituir a visualização de login da estrutura restante padrão, para que o token seja atualizado sempre que um login for feito:

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 não se esqueça de modificar os urls:

urlpatterns += patterns(
    '',
    url(r'^users/login/?$', '<path_to_file>.obtain_expiring_auth_token'),
)
Odedfos
fonte
6
Você não gostaria de criar um novo token em ObtainExpiringAuthToken se ele tiver expirado, em vez de apenas atualizar o carimbo de data / hora para o antigo?
Joar Leth
4
Criar um novo token faz sentido. Você também poderia gerar novamente o valor da chave de tokens existente e, então, não teria que excluir o token antigo.
odedfos
E se eu quiser limpar o token na expiração? Quando eu get_or_create novamente, um novo token será gerado ou o carimbo de data / hora será atualizado?
Sayok88
3
Além disso, você pode expirar tokens da mesa removendo os antigos periodicamente em um cronjob (Celery Beat ou semelhante), em vez de interceptar a validação
BjornW
1
@BjornW Gostaria apenas de fazer o despejo e, na minha opinião, é responsabilidade da pessoa que se integra com a API (ou seu front-end) fazer uma solicitação, eles recebem, "Token inválido", e depois clica em atualizar / criar novos terminais de tokens
ShibbySham
25

Se alguém estiver interessado por essa solução, mas quiser ter um token que seja válido por um certo tempo, ele será substituído por um novo token aqui está a solução completa (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')
)

seu projeto urls.py (na matriz 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)

Nas configurações de REST_FRAMEWORK, adicione ExpiringTokenAuthentication como uma classe de autenticação em vez de TokenAuthentication:

REST_FRAMEWORK = {

    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
        #'rest_framework.authentication.TokenAuthentication',
        'yourmodule.authentication.ExpiringTokenAuthentication',
    ),
}
galex
fonte
Estou recebendo o erro 'ObtainExpiringAuthToken' object has no attribute 'serializer_class'quando tento acessar o endpoint da API. Não tenho certeza do que estou perdendo.
Dharmit
2
Solução interessante, que testarei mais tarde; No momento, sua postagem me ajudou a entrar no caminho certo, pois simplesmente esqueci de definir AUTHENTICATION_CLASSES.
normic de
2
Chegando tarde para a festa, mas eu precisava fazer algumas mudanças sutis para que funcionasse. 1) utc_now = datetime.datetime.utcnow () deve ser utc_now = datetime.datetime.utcnow (). Replace (tzinfo = pytz.UTC) 2) Na classe ExpiringTokenAuthentication (TokenAuthentication): Você precisa do modelo, self.model = self. get_model ()
Ishan Bhatt
5

Tentei a resposta @odedfos, mas ocorreu um erro enganador . Aqui está a mesma resposta, fixa e com importações adequadas.

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

Pensei em dar uma resposta Django 2.0 usando DRY. Alguém já criou isso para nós, google Django OAuth ToolKit. Disponível com pip pip install django-oauth-toolkit,. Instruções sobre como adicionar o token ViewSets com roteadores: https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html . É semelhante ao tutorial oficial.

Então, basicamente, OAuth1.0 era mais a segurança de ontem, que é o que TokenAuthentication é. Para obter tokens de expiração sofisticados, o OAuth2.0 está na moda atualmente. Você obtém uma variável AccessToken, RefreshToken e uma variável de escopo para ajustar as permissões. Você acaba com creds como este:

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

O autor perguntou

a questão é: o aplicativo deve renovar / alterar o token periodicamente e, se sim, como? Deve ser o aplicativo móvel que requer a renovação do token ou o aplicativo da web deve fazer isso de forma autônoma?

Mas todas as respostas estão escrevendo sobre como alterar automaticamente o token.

Acho que mudar token periodicamente por token não tem sentido. O restante do framework cria um token de 40 caracteres, se o invasor testar 1000 tokens a cada segundo, é necessário16**40/1000/3600/24/365=4.6*10^7 leva anos para obter o token. Você não deve se preocupar se o invasor irá testar seu token um por um. Mesmo que você tenha alterado seu token, a probabilidade de adivinhar seu token é a mesma.

Se você está preocupado que talvez os invasores possam obter seu token, então você o altera periodicamente, depois que o invasor obter o token, ele também pode alterar seu token, então o usuário real é expulso.

O que você realmente deve fazer é evitar que o invasor obtenha o token do seu usuário, use https .

A propósito, estou apenas dizendo que alterar token por token não faz sentido, mas alterar token por nome de usuário e senha às vezes é significativo. Talvez o token seja usado em algum ambiente http (você deve sempre evitar esse tipo de situação) ou algum terceiro (neste caso, você deve criar um tipo diferente de token, usar oauth2) e quando o usuário está fazendo algo perigoso como mudar vinculando a caixa de correio ou excluir a conta, você deve ter certeza de não usar mais o token de origem, pois ele pode ter sido revelado pelo invasor usando ferramentas sniffer ou tcpdump.

Ramwin
fonte
Sim, concordo, você deve obter um novo token de acesso por algum outro meio (que não um token de acesso antigo). Como com um token de atualização (ou a velha maneira de forçar um novo login com senha, pelo menos).
BjornW
1

Se você notar que um token é como um cookie de sessão, você pode manter o tempo de vida padrão dos cookies de sessão no Django: https://docs.djangoproject.com/en/1.4/ref/settings/#session-cookie-age .

Não sei se o Django Rest Framework lida com isso automaticamente, mas você sempre pode escrever um script curto que filtra os desatualizados e os marca como expirados.

Tomasz Zieliński
fonte
1
A autenticação de token não usa cookies
s29
0

Só pensei em adicionar o meu, pois isso foi útil para mim. Eu geralmente uso o método JWT, mas às vezes algo assim é melhor. Atualizei a resposta aceita para django 2.1 com as importações adequadas.

autenticação.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
fonte
0

apenas para continuar adicionando à resposta @odedfos, acho que houve algumas mudanças na sintaxe, então o código de ExpiringTokenAuthentication precisa de alguns ajustes:

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

Além disso, não se esqueça de adicioná-lo a DEFAULT_AUTHENTICATION_CLASSES em vez de rest_framework.authentication.TokenAuthentication

Luis Rodriguez-Moldes
fonte