Otentikasi Token untuk RESTful API: haruskah token diubah secara berkala?

115

Saya sedang membangun RESTful API dengan Django dan django-rest-framework .

Sebagai mekanisme otentikasi kami telah memilih "Otentikasi Token" dan saya telah mengimplementasikannya mengikuti dokumentasi Django-REST-Framework, pertanyaannya adalah, haruskah aplikasi memperbarui / mengubah Token secara berkala dan jika ya bagaimana? Haruskah aplikasi seluler yang memerlukan token untuk diperbarui atau aplikasi web harus melakukannya secara mandiri?

Apa praktik terbaiknya?

Adakah orang di sini yang berpengalaman dengan Kerangka REST Django dan dapat menyarankan solusi teknis?

(pertanyaan terakhir memiliki prioritas lebih rendah)

desain nemesis
sumber

Jawaban:

101

Merupakan praktik yang baik untuk meminta klien seluler memperbarui token autentikasi mereka secara berkala. Ini tentu saja terserah server untuk menegakkannya.

Kelas TokenAuthentication default tidak mendukung ini, namun Anda dapat memperluasnya untuk mencapai fungsionalitas ini.

Sebagai contoh:

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

Ini juga diperlukan untuk mengganti tampilan login framework lainnya, sehingga token di-refresh setiap kali login selesai:

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

Dan jangan lupa untuk mengubah url:

urlpatterns += patterns(
    '',
    url(r'^users/login/?$', '<path_to_file>.obtain_expiring_auth_token'),
)
odedfos.dll
sumber
6
Tidakkah Anda ingin membuat token baru di ObtainExpiringAuthToken jika sudah kedaluwarsa, daripada hanya memperbarui stempel waktu untuk yang lama?
Joar Leth
4
Membuat token baru masuk akal. Anda juga dapat membuat ulang nilai kunci token yang ada dan kemudian Anda tidak perlu menghapus token lama.
odedfos
Bagaimana jika saya ingin menghapus token saat kedaluwarsa? Ketika saya mendapatkan_or_create lagi, apakah token baru akan dibuat atau stempel waktu diperbarui?
Sayok88
3
Selain itu, Anda dapat mengakhiri token dari tabel dengan mengeluarkan token lama secara berkala dalam cronjob (Seledri Beat atau serupa), alih-alih mencegat validasi
BjornW
1
@BjornW Saya hanya akan melakukan penggusuran dan, menurut saya, adalah tanggung jawab orang yang berintegrasi dengan API (atau front-end Anda) untuk membuat permintaan, mereka menerima, "Token tidak valid", dan kemudian tekan refresh / buat titik akhir token baru
ShibbySham
25

Jika seseorang tertarik dengan solusi itu tetapi ingin memiliki token yang valid untuk waktu tertentu kemudian diganti dengan token baru inilah solusi lengkapnya (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')
)

proyek Anda urls.py (dalam larik 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)

Di setelan REST_FRAMEWORK Anda, tambahkan ExpiringTokenAuthentication sebagai kelas Authentification, bukan TokenAuthentication:

REST_FRAMEWORK = {

    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
        #'rest_framework.authentication.TokenAuthentication',
        'yourmodule.authentication.ExpiringTokenAuthentication',
    ),
}
galex
sumber
Saya mendapatkan kesalahan 'ObtainExpiringAuthToken' object has no attribute 'serializer_class'saat mencoba mengakses titik akhir api. Tidak yakin apa yang saya lewatkan.
Dharmit
2
Solusi menarik, yang akan saya uji nanti; saat ini postingan Anda membantu saya berada di jalur yang benar karena saya lupa menyetel AUTHENTICATION_CLASSES.
normik
2
Datang terlambat ke pesta, tetapi saya perlu melakukan beberapa perubahan halus untuk membuatnya berhasil. 1) utc_now = datetime.datetime.utcnow () harus utc_now = datetime.datetime.utcnow (). Ganti (tzinfo = pytz.UTC) 2) Di kelas ExpiringTokenAuthentication (TokenAuthentication): Anda membutuhkan model, self.model = self. get_model ()
Ishan Bhatt
5

Saya sudah mencoba jawaban @odedfos tetapi saya mengalami kesalahan yang menyesatkan . Inilah jawaban yang sama, tetap dan dengan impor yang tepat.

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

Pikir saya akan memberikan jawaban Django 2.0 menggunakan KERING. Seseorang telah membuat ini untuk kita, google Django OAuth ToolKit. Tersedia dengan pip pip install django-oauth-toolkit,. Petunjuk tentang menambahkan token ViewSets dengan router: https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html . Ini mirip dengan tutorial resmi.

Jadi pada dasarnya OAuth1.0 lebih merupakan keamanan kemarin yang merupakan TokenAuthentication. Untuk mendapatkan token kedaluwarsa yang mewah, OAuth2.0 adalah hal yang populer saat ini. Anda mendapatkan variabel AccessToken, RefreshToken, dan cakupan untuk menyesuaikan izin. Anda berakhir dengan kredibilitas seperti ini:

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

Penulis bertanya

pertanyaannya, apakah aplikasi harus memperbaharui / mengganti Token secara berkala dan jika ya bagaimana? Haruskah aplikasi seluler yang memerlukan token untuk diperbarui atau aplikasi web harus melakukannya secara mandiri?

Tetapi semua jawaban menulis tentang cara mengubah token secara otomatis.

Menurut saya, perubahan token secara berkala dengan token tidak ada artinya. Kerangka lainnya membuat token yang memiliki 40 karakter, jika penyerang menguji 1000 token setiap detik, dibutuhkan waktu 16**40/1000/3600/24/365=4.6*10^7bertahun - tahun untuk mendapatkan token. Anda tidak perlu khawatir penyerang akan menguji token Anda satu per satu. Bahkan Anda mengubah token Anda, kemungkinan tebakan Anda adalah sama.

Jika Anda khawatir mungkin penyerang bisa mendapatkan token Anda, jadi Anda mengubahnya secara berkala, daripada setelah penyerang mendapatkan token, dia juga dapat mengubah token Anda, daripada pengguna yang sebenarnya diusir.

Yang harus Anda lakukan adalah mencegah penyerang mendapatkan token pengguna Anda , gunakan https .

Ngomong-ngomong, saya hanya mengatakan mengubah token dengan token tidak ada artinya, mengubah token dengan nama pengguna dan kata sandi terkadang berarti. Mungkin token digunakan di beberapa lingkungan http (Anda harus selalu menghindari situasi seperti ini) atau pihak ketiga (dalam hal ini, Anda harus membuat jenis token yang berbeda, gunakan oauth2) dan ketika pengguna melakukan beberapa hal berbahaya seperti mengubah mengikat kotak surat atau menghapus akun, Anda harus memastikan bahwa Anda tidak akan menggunakan token asli lagi karena mungkin telah diungkapkan oleh penyerang menggunakan alat sniffer atau tcpdump.

ramwin
sumber
Ya, setuju, Anda harus mendapatkan token akses baru dengan cara lain (selain token akses lama). Seperti dengan token penyegaran (atau cara lama memaksa login baru dengan kata sandi setidaknya).
BjornW
1

Jika Anda memperhatikan bahwa sebuah token adalah seperti cookie sesi maka Anda dapat tetap menggunakan default seumur hidup cookie sesi di Django: https://docs.djangoproject.com/en/1.4/ref/settings/#session-cookie-age .

Saya tidak tahu apakah Django Rest Framework menanganinya secara otomatis tetapi Anda selalu dapat menulis skrip pendek yang menyaring yang kadaluwarsa dan menandainya sebagai kadaluarsa.

Tomasz Zieliński
sumber
1
Otentikasi Token tidak menggunakan cookie
s29
0

Hanya berpikir saya akan menambahkan milik saya karena ini membantu saya. Saya biasanya menggunakan metode JWT tetapi terkadang sesuatu seperti ini lebih baik. Saya memperbarui jawaban yang diterima untuk django 2.1 dengan impor yang tepat ..

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

hanya untuk terus menambahkan jawaban @odedfos, saya pikir ada beberapa perubahan pada sintaks sehingga kode ExpiringTokenAuthentication perlu beberapa penyesuaian:

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

Selain itu, jangan lupa untuk menambahkannya ke DEFAULT_AUTHENTICATION_CLASSES alih-alih rest_framework.authentication.TokenAuthentication

Luis Rodriguez-Moldes
sumber