REST 化した Django の JWT 認証を dj_rest_auth に丸投げする


これまでの経緯

  1. ローカルに Ubuntu Server を用意した
  2. そこに Docker Compose をインストールした
  3. Django+MySQL+nginx の開発環境を Docker Compose で構築した
  4. Django を AWS Fargate で動作させようとした → 失敗した
  5. AbstractBaseUser を継承したカスタムユーザーで E-mail ログインができるようにした
  6. Git で晒すとヤバい変数を django-environ と環境変数の設定で隠蔽した

GitHub のレポジトリはこちら:https://github.com/hajime-f/octave

今回やりたいこと

前々回で、カスタムユーザーで E-mail ログインができるようになったところから始めます。

今回やりたいことは、
- Django REST Framework を導入する
- dj_rest_auth を使って JWT 認証を実現する
の2つ。

「REST ってなんやねん」という人は、こちらの記事を読みましょう。
0からREST APIについて調べてみた

「JWT(ジョット)認証ってなんやねん」という人は、このあたりが参考になります。
JWT認証と流れのやわらかい解説

手順

  1. Django を動かすコンテナにパッケージをインストールする
  2. settings.py を編集する
  3. urls.py でルーティングを設定する
  4. 静的ファイルをベースディレクトリに集める

1. パッケージをインストールする

まずは REST Framework のパッケージと、JWT 認証用のパッケージをインストールします。コンテナにこれらをインストールするために、requirements.txt を編集します。なお、パスは docker-compose.dev.yml があるプロジェクトディレクトリからの相対パスです。

./python/requirements.txt
Django==3.1.4
uwsgi==2.0.18
mysqlclient==1.4.6
django-environ==0.4.5
djangorestframework==3.12.2           # 追加
djangorestframework-simplejwt==4.6.0  # 追加
dj-rest-auth==2.1.2                   # 追加

編集したらビルドしておきましょう。

$ make dev  # docker-compose -f docker-compose.dev.yml build と同じ

Makefile の全内容は、以前の記事を読んでね。

2. settings.py を編集する

公式ドキュメントに書いてあるとおりに、settings.py を編集します。

./src/octave/settings.py
INSTALLED_APPS = [
    ・・・
    'django.contrib.sites',      # 追加

    # 3rd party apps
    'rest_framework',            # 追加
    'rest_framework.authtoken',  # 追加
    'dj_rest_auth',              # 追加

    #My applications
    'users',
]

# 以下すべて追加
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'dj_rest_auth.jwt_auth.JWTCookieAuthentication',
    )
}
SITE_ID = 1
REST_USE_JWT = True
JWT_AUTH_COOKIE = 'user'
AUTH_USER_MODEL = 'users.User'

SIMPLE_JWT = {
    'USER_ID_FIELD': 'uuid',
}

ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_USER_MODEL_USERNAME_FIELD = None

ユーザーを特定する primary key を uuid としたので、そのための設定が必要です。

また、以前の記事のとおり、メールアドレスのみ(ユーザーネームは使わない)で認証できるようにしたいので、そのための設定がいろいろと入ってます。

3. urls.py でルーティングを設定する

これも公式ドキュメントに書いてあるとおりに、urls.py でルーティングを設定します。

./src/octave/urls.py
from django.contrib import admin
from django.urls import path
from django.urls import include  # 追加

urlpatterns = [
    path('admin/', admin.site.urls),
    path('dj-rest-auth/', include('dj_rest_auth.urls')),   # 追加
]

4. 静的ファイルをベースディレクトリに集める

REST Framework は独自の静的ファイルを持っているので、ベースディレクトリにその静的ファイルを集めておきましょう。

$ docker-compose -f docker-compose.dev.yml run python ./manage.py collectstatic

動作確認

ブラウザで http://[開発環境のIPアドレス]:[開発環境のポート番号]/dj-rest-auth/login/ にアクセスし、テストユーザーでログインできることを確認します。

こんな感じでアクセストークンが返ってきたら成功です。

端末から curl コマンドを叩いても、結果を確認できます。

$ curl \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]", "password": "test123456"}' \
  http://[開発環境のIPアドレス]:[開発環境のポート番号]/dj-rest-auth/login/
{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGci...",
"user":{"pk":"de0e84a7-8396-497e-aa07-e143cc0a2811",
"email":"[email protected]",
"first_name":"テスト",
"last_name":"テスト"}}%

いま苦しんでいるバグ

(追記)このバグは、公式に質問したら解決できました。

本当は、こうすればユーザー登録も丸投げできるはずなんですね。

./python/requirements.txt
Django==3.1.4
uwsgi==2.0.18
mysqlclient==1.4.6
django-environ==0.4.5
djangorestframework==3.12.2
djangorestframework-simplejwt==4.6.0
dj-rest-auth==2.1.2
django-allauth==0.44.0  # さらに追加
./src/octave/settings.py
INSTALLED_APPS = [
    ・・・
    'django.contrib.sites',

    # 3rd party apps
    'rest_framework',
    'rest_framework.authtoken',
    'allauth',                    # さらに追加  
    'allauth.account',            # さらに追加
    'dj_rest_auth',
    'dj_rest_auth.registration',  # さらに追加

    #My applications
    'users',
]

・・・

ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_EMAIL_REQUIRED = True              # さらに追加
ACCOUNT_UNIQUE_EMAIL = True                # さらに追加
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'   # さらに追加
./src/octave/urls.py
urlpatterns = [
    path('admin/', admin.site.urls),
    path('dj-rest-auth/', include('dj_rest_auth.urls')),
    path('dj-rest-auth/registration/', include('dj_rest_auth.registration.urls')),   # さらに追加
]

これは公式ドキュメントに書いてあるとおりなのですが、カスタムユーザーを使っているせいか「EmailAddress matching query does not exist」の例外が出て、さっきまで動いていたログインすらうまく動きません。

絶賛悩み中。なんでだ。
以下にエラーログを貼っておくので、誰か分かる人、教えて。

Environment:

Request Method: POST
Request URL: http://127.0.0.1:8081/dj-rest-auth/login/

Django Version: 3.1.4
Python Version: 3.9.1
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django.contrib.sites',
 'rest_framework',
 'rest_framework.authtoken',
 'allauth',
 'allauth.account',
 'allauth.socialaccount',
 'dj_rest_auth',
 'dj_rest_auth.registration',
 'users',
 'orchestra']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']

Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/usr/local/lib/python3.9/site-packages/django/core/handlers/base.py", line 179, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/utils/decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/views/decorators/debug.py", line 89, in sensitive_post_parameters_wrapper
    return view(request, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/dj_rest_auth/views.py", line 48, in dispatch
    return super(LoginView, self).dispatch(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 509, in dispatch
    response = self.handle_exception(exc)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 469, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
    raise exc
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 506, in dispatch
    response = handler(request, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/dj_rest_auth/views.py", line 138, in post
    self.serializer.is_valid(raise_exception=True)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/serializers.py", line 220, in is_valid
    self._validated_data = self.run_validation(self.initial_data)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/serializers.py", line 422, in run_validation
    value = self.validate(value)
  File "/usr/local/lib/python3.9/site-packages/dj_rest_auth/serializers.py", line 131, in validate
    self.validate_email_verification_status(user)
  File "/usr/local/lib/python3.9/site-packages/dj_rest_auth/serializers.py", line 112, in validate_email_verification_status
    email_address = user.emailaddress_set.get(email=user.email)
  File "/usr/local/lib/python3.9/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/db/models/query.py", line 429, in get
    raise self.model.DoesNotExist(

Exception Type: DoesNotExist at /dj-rest-auth/login/
Exception Value: EmailAddress matching query does not exist.

参考