Django REST FrameworkでのJWT Authentication

Python,TIPSDjango

前回まではDjangoとVue.jsを使用したWEBアプリの実装について紹介していましたが、一旦今回は途中ではあるもののDRFを使用したJWT認証について少し掘り下げたいと思います。

JWTはJSON Web Tokenの略称でJavascriptやAngular、React、Vue.jsなどのフレームワークを使用したアプリケーションでの認証に使用することができます。

JWTの仕組み

JWTは全てのリクエストに含める必要がある認証トークンです。

curl http://127.0.0.1:8000/api/user -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Imtlbm55IiwiZXhwIjoxNTYzNjQwMDA5LCJlbWFpbCI6IiIsIm9yaWdfaWF0IjoxNTYzNTUzNjA5fQ.b65KjST9WF4MU9y0UUf9HkitjRMRalmwjUCBWVe5slI

JWTはユーザー名とパスワードをアクセスコードとリフレッシュトークンに交換することで取得されます。
アクセストークンは賞味期限がとても早くデフォルトでは5分ほどで腐って使用不可になってしまいます。
もちろんカスタマイズすることで期限は変更することができます。

対してリフレッシュトークンは少し長めに保存することができます。
(カスタマイズが可能で24時間ほどまで延長可能)
リフレッシュトークンの期限が切れた際はユーザーは再びIDとパスワードを使用してフルログインをし直す必要があります。

これはセキュリティ上の要件でJWTがいくつかの情報を保持していることに起因しています。
JWTは以下のように3つの構成で出来ています。

aaaaa.bbbbb.ccccc

これはそれぞれ、

header.payload.signature

となっており、先ほどの例ではこのようになっています。

header = eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
payload = eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Imtlbm55IiwiZXhwIjoxNTYzNjQwMDA5LCJlbWFpbCI6IiIsIm9yaWdfaWF0IjoxNTYzNTUzNjA5fQ
signature = b65KjST9WF4MU9y0UUf9HkitjRMRalmwjUCBWVe5slI

この情報はBase64を使用してエンコードされています、デコードしてみましょう。
header

{
  "typ": "JWT",
  "alg": "HS256"
}

payload

{
  "token_type": "access",
  "exp": 1543828431,
  "jti": "7f5997b7150d46579dc2b49167097e7b",
  "user_id": 1
}

signature

署名は header base64 + payload base64 + SECRET_KEY を使用しJWTバックエンドによって発行されます。
リクエストがあるたびにこの署名は検証されます。
ヘッダーまたはペイロード内の情報がクライアントによって変更された場合は署名が無効になります。
署名を確認して検証する唯一の方法はアプリケーションのSECRET_KEYを使用することです。
SECRET_KEYを常に秘匿する必要があるのはそのためです。


導入と設定方法

このTIPSではdjangorestframework_simplejwt というライブラリを使用します。

pip install djangorestframework_simplejwt
python manage.py startapp harimo

続いてアプリをDjangoに登録しルーティングをします。

settings.py

INSTALLED_APPS = [
    ...

    'rest_framework', # 追加
    'harimo', # 追加
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

urls.py

from django.urls import path
from rest_framework_simplejwt import views as jwt_views

urlpatterns = [
    path('api/token/', jwt_views.TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'),
]

実装例

views.py

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated


class HedgeHogView(APIView):
    permission_classes = (IsAuthenticated,)

    def get(self, request):
        content = {'message': 'harimo!'}
        return Response(content)

urls.py

from django.urls import path
from rest_framework_simplejwt import views as jwt_views
from harimo import views

urlpatterns = [
    path('hedgehog/', views.HedgeHogView.as_view(), name='hedgehog'), # 追加
    path('api/token/', jwt_views.TokenObtainPairView.as_view(),
         name='token_obtain_pair'),
    path('api/token/refresh/', jwt_views.TokenRefreshView.as_view(),
         name='token_refresh'),
]

叩いてみる

早速先ほど作成したAPIを叩いてみましょう。
以前も使用したPostmanを使用します。

トークンの取得

以下の様なレスポンスが返りました。

{
    "refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU2Mzk4NDc5OCwianRpIjoiNDcyNjM3MmI1YTc2NGI1M2E4NWFhM2Q5MGVkOTk4ZTIiLCJ1c2VyX2lkIjoxfQ._HdX7HtE9kjN-vHmrvFVRqq_BnBm4GGciA-3Np5G9mY",
    "access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTYzODk4Njk4LCJqdGkiOiJmMmMxYTVkMzNhNzU0ZTY5ODlkNjQ1YTdlNDcwY2VlYyIsInVzZXJfaWQiOjF9.vyR0yvAV3oIRP1ZhfzCEF4R8V3yilaTp1_24SUr6sL4"
}

アクセストークンリフレッシュトークンの両方をクライアント側、localStrageやsessionStrageに格納します。
バックエンドの保護されたview(つまり認証を必要とするAPIエンドポイント)にアクセスするには全てのリクエストヘッダーにアクセストークンを含める必要があります。

このアクセストークンは5分間使用できます。
期限切れになった後に再度viewへアクセスを試みると次の様なエラーが発生します。

トークンの更新

新しいトークンを取得するには /api/token/refresh/ に期限切れになったトークンをつけてPOSTすることで再取得が可能になっています。

レスポンスは更新された新しいアクセストークンになっています。
リフレッシュトークンは24時間有効です。
それが期限切れになるとユーザーは新しいアクセストークン + リフレッシュトークンを取得するためにユーザー名とパスワードを使用して再度認証を実行する必要があります。


リフレッシュトークンの必要性

一見するとリフレッシュトークンは無意味に見えるかもしれませんが、実際にはユーザーが正しいアクセス許可を持っていることを確認する必要があります。
アクセストークンの有効期限が長い場合はトークンに関連付けられた情報の更新に時間がかかることもありえます。

これは、データベースにクエリを実行してデータを検証する代わりに認証チェックが暗号化手段によって行われるためであり、従って一部の情報は一種のキャッシュされます。
また、リフレッシュトークンはPOSTデータ内でのみ移動するという意味でセキュリティ面でのメリットもあります。
それはアクセストークンはHTTPヘッダーを介して送信され、途中でログに記録されてしまう可能性があることによります。


今回のソースコードはGitHubにアップロードしてあります。
ご自由にお試しください。

Python,TIPSDjango

Posted by Kenny