RESTful APIのトークン認証:トークンを定期的に変更する必要がありますか?


115

Djangoとdjango-rest-frameworkを使用してRESTful APIを構築しています。

認証メカニズムとして「トークン認証」を選択し、Django-REST-Frameworkのドキュメントに従ってすでに実装しているので、問題は、アプリケーションがトークンを定期的に更新/変更する必要があるかどうか、そうであればどのようにですか?トークンの更新が必要なのはモバイルアプリですか、それともWebアプリで自律的に更新する必要がありますか?

ベストプラクティスは何ですか?

Django REST Frameworkの経験があり、技術的な解決策を提案できる人はいますか?

(最後の質問は優先度が低くなっています)

回答:


101

モバイルクライアントに定期的に認証トークンを更新させることをお勧めします。もちろん、これは強制するサーバー次第です。

デフォルトの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

また、ログインが行われるたびにトークンが更新されるように、デフォルトの残りのフレームワークログインビューをオーバーライドする必要があります。

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'),
)

6
古いトークンのタイムスタンプを更新するだけでなく、期限切れの場合は、OctainExpiringAuthTokenに新しいトークンを作成しませんか?
Joar Leth 2013

4
新しいトークンを作成することには意味があります。既存のトークンキーの値を再生成することもでき、古いトークンを削除する必要はありません。
odedfos 2013年

有効期限が切れたときにトークンをクリアしたい場合はどうなりますか?もう一度get_or_createすると、新しいトークンが生成されるか、タイムスタンプが更新されますか?
Sayok88、2018年

3
また、検証を傍受する代わりに、cronジョブ(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設定で、TokenAuthenticationではなくExpiringTokenAuthenticationを認証クラスとして追加します。

REST_FRAMEWORK = {

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

'ObtainExpiringAuthToken' object has no attribute 'serializer_class'APIエンドポイントにアクセスしようとすると、エラーが発生します。何が欠けているかわからない。
ダルミット2015年

2
興味深いソリューションです。後でテストします。AUTHENTICATION_CLASSESを設定するのを忘れたので、現時点では、あなたの投稿が正しい方向に進むのに役立ちました。
Normic、2017年

2
パーティーに遅刻しましたが、機能させるには微妙な変更を加える必要がありました。1)utc_now = datetime.datetime.utcnow()はutc_now = datetime.datetime.utcnow()。replace(tzinfo = pytz.UTC)である必要があります2)クラスExpiringTokenAuthentication(TokenAuthentication):モデル、self.model = selfが必要です。 get_model()
Bhatt

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

DRYを使用してDjango 2.0の答えを出すと思いました。だれかがすでにGoogleのDjango OAuth ToolKitを使って構築しています。ピップで利用できpip install django-oauth-toolkitます。ルーターでトークンViewSetを追加する手順: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

著者は尋ねました

問題は、アプリケーションがトークンを定期的に更新/変更する必要があるか、そしてそうであればどのように?トークンの更新が必要なのはモバイルアプリですか、それともWebアプリで自律的に更新する必要がありますか?

しかし、すべての答えは、トークンを自動的に変更する方法について書いています。

トークンを定期的に定期的に変更しても意味がないと思います。 残りのフレームワークは40文字のトークンを作成します。攻撃者が毎秒1000トークンをテストする場合、トークン16**40/1000/3600/24/365=4.6*10^7を取得するには何年もかかります。攻撃者がトークンを1つずつテストすることを心配する必要はありません。トークンを変更しても、トークンを推測する確率は同じです。

攻撃者がトークンを取得できるのではないかと心配している場合は、定期的に変更します。攻撃者がトークンを取得した後、実際のユーザーが追い出されるよりも、トークンを変更することができます。

実際にすべきことは、攻撃者がユーザーのトークンを取得できないようにすることです。httpsを使用してください

ちなみに、トークンごとの変更は意味がないと言っているだけですが、ユーザー名とパスワードごとのトークンの変更は意味がある場合があります。たぶん、トークンはhttp環境(この種の状況は常に回避する必要があります)またはサードパーティ(この場合、別の種類のトークンを作成し、oauth2を使用する必要があります)で使用され、ユーザーが変更などの危険なことを行っている場合メールボックスのバインドまたはアカウントの削除を行う場合は、攻撃者がスニファーまたはtcpdumpツールを使用してそれを明らかにした可能性があるため、オリジントークンを使用しないようにしてください。


はい、同意します。(古いアクセストークン以外の)別の方法で新しいアクセストークンを取得する必要があります。更新トークンの場合と同じです(または、少なくともパスワードで新しいログインを強制する古い方法)。
BjornW


1

トークンがセッションCookieのようなものであることに気付いた場合は、DjangoのセッションCookieのデフォルトの有効期間を守ることができます:https : //docs.djangoproject.com/en/1.4/ref/settings/#session-cookie-age

Django Rest Frameworkがそれを自動的に処理するかどうかはわかりませんが、古いスクリプトを除外して期限切れとしてマークする短いスクリプトをいつでも作成できます。


1
トークン認証はcookieを使用しません
s29

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)

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

また、rest_framework.authentication.TokenAuthenticationの代わりに、DEFAULT_AUTHENTICATION_CLASSESに追加することを忘れないでください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.