ジャンゴレストフレームワークカスタムJWT認証
36779 ワード
This article is not a tutorial or a guide, it is more like a request for code review and validate the implementation from more experienced Django developers, so please don't use this code unless you are able to review and validate yourself
導入
Webアプリケーションのセキュリティは重要であり、自分自身でそれを行うことは巨大な責任である.それは私がDjangoを愛している理由である.なぜなら、それはそこでのセキュリティ脆弱性のほとんどを面倒にするからだ.しかし、私はすべてのタイプのクライアント(SPA、モバイル、デスクトップ、…など)を接続できるように、RESTful APIとしてバックエンドを構築したいときに、私の問題が現れる
ジャンゴレストフレームワークはauthentication classes , トークン認証JWT 私のユースケースに行く方法ですが、私はまだクライアント側でトークンを保存する方法の心配がある
誰もがXSS attacks そして、httponlyクッキーであなたのトークンを保存するより良いです、しかし、クッキーは開いていますCSRF attack あまりにもDRFはすべてのapiviewのためのCSF保護を無効にするので、何がこれをする最高の実行ですか.
しばらくの間、私はすべての携帯電話(私はネイティブに反応)として我々は安全なストレージを持っている場合でも、デバイスがない場合(ルートAndroidまたはJailbreak IOS)は、各アプリはサンドボックスされ、トークンはほとんどの場合は安全であるとしてそれを使用し続けて
問題はまだWebクライアント(SPA)にあるので、私は役に立つかもしれない実装に入ってきて、ここでそれを文書化したいので、より経験のある開発者からのフィードバックを得て、次のステップにまとめます.
ユーザーは、ログインするためにユーザ名とパスワードでポスト要求を送ります、そして、サーバーは3つのものをします
access_token
そして、それは短い生命JWT(多分5分)です、そして、反応体にそれを送ってくださいrefresh_token
これは長寿命JWT(日)であり、httpOnlyクッキーでそれを送るので、それはクライアントJavaScriptクライアント側では開発者は注意しなければなりません.
access_token
にAuthorization
ヘッダ.X-CSRFTOKEN
彼がポスト要求をしているなら、ヘッダーaccess_token
, 彼は、トークン終点をリフレッシュするためにポストリクエストを送る必要がありますプロジェクト設定
python3 -m venv .venv
source .venv/bin/activate
pip install django django-cors-headers djangorestframework PyJWT
# create Django project with the venv activates
django-admin startproject project
# create an app
./manage.py start app accounts
プロジェクトの設定では、アプリケーションを有効にし、いくつかの設定を追加INSTALLED_APPS = [
...
# 3rd party apps
'corsheaders',
'rest_framework',
# project apps
'accounts',
]
CORS_ALLOW_CREDENTIALS = True # to accept cookies via ajax request
CORS_ORIGIN_WHITELIST = [
'http://localhost:3000' # the domain for front-end app(you can add more than 1)
]
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated', # make all endpoints private
)
}
最初の移行を適用する前に、official Django docs 私はそれを使用しない場合でも、カスタムユーザーモデルで私のプロジェクトを開始するのが好きですイン
accounts.models
ユーザモデルfrom django.contrib.auth.models import AbstractUser
class User(AbstractUser):
pass
にproject.settings.py
ユーザモデル...
AUTH_USER_MODEL = 'accounts.User'
...
後で、次のいずれかでユーザーモデルを参照できますfrom django.conf import settings
User = settings.AUTH_USER_MODEL
# OR
from django.contrib.auth import get_user_model
User = get_user_model()
とaccounts.admin
新規ユーザモデルを管理サイトに登録するfrom django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from accounts.models import User
admin.site.register(User, UserAdmin)
スーパーユーザーの作成./manage.py createsuperuser
そして、最後に、テストの終点は、上記の設定ですべてのエンドポイントがデフォルトで認証を必要とすると宣言するように、私たちが後でログインにおいてするつもりであるように、我々は特定の見解でこれを越えることができます現在の認証されたユーザーオブジェクトをJSONとして返すユーザープロファイルのエンドポイントを作成します
# accounts.serializers
from rest_framework import serializers
from accounts.models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email',
'first_name', 'last_name', 'is_active']
# project.urls
from accounts import urls as accounts_urls
urlpatterns = [
path('accounts/', include(accounts_urls)),
]
# accounts.urls
urlpatterns = [
path('profile', profile, name='profile'),
]
# accounts.views
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .serializers import UserSerializer
@api_view(['GET'])
def profile(request):
user = request.user
serialized_user = UserSerializer(user).data
return Response({'user': serialized_user })
さて、このエンドポイントを無効にしようとすると、403エラーが発生します.導入について説明したように、ログインする必要があります.ログイン表示
ログイン終了点はPOSTリクエストです
username
and password
リクエスト本文で.許可クラスのデコレータを使用してログインビューをパブリックにする
AllowAny
, また、ビューを飾る@ensure_csrf_cookie
ログイン成功の場合、djangoが応答でCSRFクッキーを送るのを強制しますログイン成功ならば、
access_token
を返します.refreshtoken
httponlyクッキーで.csrftoken
通常のクッキーではJavaScriptから読み込み、必要なときに再送できます# accounts.views
from django.contrib.auth import get_user_model
from rest_framework.response import Response
from rest_framework import exceptions
from rest_framework.permissions import AllowAny
from rest_framework.decorators import api_view, permission_classes
from django.views.decorators.csrf import ensure_csrf_cookie
from accounts.serializers import UserSerializer
from accounts.utils import generate_access_token, generate_refresh_token
@api_view(['POST'])
@permission_classes([AllowAny])
@ensure_csrf_cookie
def login_view(request):
User = get_user_model()
username = request.data.get('username')
password = request.data.get('password')
response = Response()
if (username is None) or (password is None):
raise exceptions.AuthenticationFailed(
'username and password required')
user = User.objects.filter(username=username).first()
if(user is None):
raise exceptions.AuthenticationFailed('user not found')
if (not user.check_password(password)):
raise exceptions.AuthenticationFailed('wrong password')
serialized_user = UserSerializer(user).data
access_token = generate_access_token(user)
refresh_token = generate_refresh_token(user)
response.set_cookie(key='refreshtoken', value=refresh_token, httponly=True)
response.data = {
'access_token': access_token,
'user': serialized_user,
}
return response
そしてここでトークンを生成する関数があります.セキュリティのために別の秘密を使ってリフレッシュトークンに署名してください# accounts.utils
import datetime
import jwt
from django.conf import settings
def generate_access_token(user):
access_token_payload = {
'user_id': user.id,
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=0, minutes=5),
'iat': datetime.datetime.utcnow(),
}
access_token = jwt.encode(access_token_payload,
settings.SECRET_KEY, algorithm='HS256').decode('utf-8')
return access_token
def generate_refresh_token(user):
refresh_token_payload = {
'user_id': user.id,
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=7),
'iat': datetime.datetime.utcnow()
}
refresh_token = jwt.encode(
refresh_token_payload, settings.REFRESH_TOKEN_SECRET, algorithm='HS256').decode('utf-8')
return refresh_token
DRF用カスタム認証クラス
Django RESTフレームワークはカスタム認証スキームを作成することを簡単にしますofficial docs
最初にDRFソースコードから取得したコードは、必要に応じて変更を加えます.
DRFがセッション認証だけでCSRFを施行することに注意してくださいrest_framework/authentication.py
# accounts.authentication
import jwt
from rest_framework.authentication import BaseAuthentication
from django.middleware.csrf import CsrfViewMiddleware
from rest_framework import exceptions
from django.conf import settings
from django.contrib.auth import get_user_model
class CSRFCheck(CsrfViewMiddleware):
def _reject(self, request, reason):
# Return the failure reason instead of an HttpResponse
return reason
class SafeJWTAuthentication(BaseAuthentication):
'''
custom authentication class for DRF and JWT
https://github.com/encode/django-rest-framework/blob/master/rest_framework/authentication.py
'''
def authenticate(self, request):
User = get_user_model()
authorization_heaader = request.headers.get('Authorization')
if not authorization_heaader:
return None
try:
# header = 'Token xxxxxxxxxxxxxxxxxxxxxxxx'
access_token = authorization_heaader.split(' ')[1]
payload = jwt.decode(
access_token, settings.SECRET_KEY, algorithms=['HS256'])
except jwt.ExpiredSignatureError:
raise exceptions.AuthenticationFailed('access_token expired')
except IndexError:
raise exceptions.AuthenticationFailed('Token prefix missing')
user = User.objects.filter(id=payload['user_id']).first()
if user is None:
raise exceptions.AuthenticationFailed('User not found')
if not user.is_active:
raise exceptions.AuthenticationFailed('user is inactive')
self.enforce_csrf(request)
return (user, None)
def enforce_csrf(self, request):
"""
Enforce CSRF validation
"""
check = CSRFCheck()
# populates request.META['CSRF_COOKIE'], which is used in process_view()
check.process_request(request)
reason = check.process_view(request, None, (), {})
print(reason)
if reason:
# CSRF failed, bail with explicit error message
raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
そのクラスを作成したら、project.settings
そして、REST_FRAMEWORK
セクションappname.filename.classname
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'accounts.authentication.SafeJWTAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
)
}
今私たちはaccess_token
そして、我々が再訪問することができる認証方法をセットしてくださいprofile
今回はエンドポイントですがAuthorization
ヘッダーリフレッシュトークンビュー
トークンが期限切れになったり、何らかの理由で新しいトークンを必要とするときはいつでも
refresh_token
エンドポイント.このビューは、
AlloAny
我々には、ありませんaccess_token
しかし、それは2他のものによって保護されますrefresh_token
httoponlyクッキーに送られます.access_token
そして、それを送り返してください.refresh_token
無効または有効期限が切れた場合、ユーザは再論理import jwt
from django.conf import settings
from django.contrib.auth import get_user_model
from django.views.decorators.csrf import csrf_protect
from rest_framework import exceptions
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from rest_framework.decorators import api_view, permission_classes
from accounts.utils import generate_access_token
@api_view(['POST'])
@permission_classes([AllowAny])
@csrf_protect
def refresh_token_view(request):
'''
To obtain a new access_token this view expects 2 important things:
1. a cookie that contains a valid refresh_token
2. a header 'X-CSRFTOKEN' with a valid csrf token, client app can get it from cookies "csrftoken"
'''
User = get_user_model()
refresh_token = request.COOKIES.get('refreshtoken')
if refresh_token is None:
raise exceptions.AuthenticationFailed(
'Authentication credentials were not provided.')
try:
payload = jwt.decode(
refresh_token, settings.REFRESH_TOKEN_SECRET, algorithms=['HS256'])
except jwt.ExpiredSignatureError:
raise exceptions.AuthenticationFailed(
'expired refresh token, please login again.')
user = User.objects.filter(id=payload.get('user_id')).first()
if user is None:
raise exceptions.AuthenticationFailed('User not found')
if not user.is_active:
raise exceptions.AuthenticationFailed('user is inactive')
access_token = generate_access_token(user)
return Response({'access_token': access_token})
取り消す
パズルの最後の部分は、長い寿命を持っているリフレッシュトークンを無効にする方法であり、トークンをブラックリストまたはトークンのUUIDを割り当てるかもしれないし、ペイロードにそれを置くかもしれないし、ユーザーにリンクし、データベースに保存するときには、データベース内のUUIDをペイロード内の値と一致しないように変更する場合、アプリケーションに必要なものを選ぶことができます
Reference
この問題について(ジャンゴレストフレームワークカスタムJWT認証), 我々は、より多くの情報をここで見つけました https://dev.to/a_atalla/django-rest-framework-custom-jwt-authentication-5n5テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol