web-dev-qa-db-ja.com

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

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

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

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

ここでDjango RESTフレームワークを経験し、技術的な解決策を提案できますか?

(最後の質問は優先度が低い)

98
nemesisdesign

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

デフォルトの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'),
)
89
odedfos

誰かがそのソリューションに興味を持っているが、一定の期間有効なトークンを持ちたい場合は、新しいトークンに置き換えられますが得られます。完全なソリューションがあります(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',
    ),
}
22
galex

@odedfos answerを試しましたが、 誤解を招くエラーがありました 。これは同じ答えで、修正済みで適切にインポートされています。

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)
5
Benjamin Toueg

http://getblimp.github.io/Django-rest-framework-jwt を活用できます

このライブラリは、有効期限のあるトークンを生成できます

DRFデフォルトトークンとDRFが提供するトークンの違いを理解するには、以下をご覧ください。

作成方法Django=REST複数のWebサーバーでのJWT認証スケール?

3
Angky William

DRYを使用してDjango 2.0の答えを出したと思います。誰かがこれをすでに構築してくれました。googleDjango OAuth ToolKit。pipで利用可能、pip install Django-oauth-toolkit。ルーターにトークンビューセットを追加する手順: 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"
}
2
Ryan Dines

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

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

1

著者は尋ねた

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

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

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

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

本当にすべきなのは、攻撃者がユーザーのトークンを取得できないようにすることですse https

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

1
ramwin

これが私にとって役立つので、私は私のものを追加すると思いました。私は通常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
wdfc