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

# これまでの経緯

1. [ローカルに Ubuntu Server を用意した](https://qiita.com/hajime-f/items/5d3373e98985776b4240)
2. [そこに Docker Compose をインストールした](https://qiita.com/hajime-f/items/2bd1cad93d3ae4a62be8)
3. [Django＋MySQL＋nginx の開発環境を Docker Compose で構築した](https://qiita.com/hajime-f/items/c40571647135df327aa7)
4. [Django を AWS Fargate で動作させようとした → 失敗した](https://qiita.com/hajime-f/items/4511f2aa036380249a3f)
5. [AbstractBaseUser を継承したカスタムユーザーで E-mail ログインができるようにした](https://qiita.com/hajime-f/items/c2f02306556c1ea16e5c)
6. [Git で晒すとヤバい変数を django-environ と環境変数の設定で隠蔽した](https://qiita.com/hajime-f/items/734991651d84be0c9c81)

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

# 今回やりたいこと

[前々回](https://qiita.com/hajime-f/items/c2f02306556c1ea16e5c)で、カスタムユーザーで E-mail ログインができるようになったところから始めます。

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

「REST ってなんやねん」という人は、こちらの記事を読みましょう。
[0からREST APIについて調べてみた](https://qiita.com/masato44gm/items/dffb8281536ad321fb08)

「JWT（ジョット）認証ってなんやねん」という人は、このあたりが参考になります。
[JWT認証と流れのやわらかい解説](https://techblog.roxx.co.jp/entry/2019/03/13/135739)

# 手順

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

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

```bash:./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                   # 追加
```

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

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

```Makefile``` の全内容は、[以前の記事](https://qiita.com/hajime-f/items/4511f2aa036380249a3f#makefile-%E3%81%AE%E4%BD%9C%E6%88%90)を読んでね。

## 2. ```settings.py``` を編集する

[公式ドキュメント](https://dj-rest-auth.readthedocs.io/en/latest/)に書いてあるとおりに、```settings.py``` を編集します。

```python:./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 としたので、そのための設定が必要です。

また、[以前の記事](https://qiita.com/hajime-f/items/c2f02306556c1ea16e5c)のとおり、メールアドレスのみ（ユーザーネームは使わない）で認証できるようにしたいので、そのための設定がいろいろと入ってます。

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

これも[公式ドキュメント](https://dj-rest-auth.readthedocs.io/en/latest/)に書いてあるとおりに、```urls.py``` でルーティングを設定します。

```python:./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 は独自の静的ファイルを持っているので、ベースディレクトリにその静的ファイルを集めておきましょう。

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

# 動作確認

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

![スクリーンショット 2021-01-03 22.51.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667942/b2b28170-49c5-2f18-9c1c-a02851761b8e.png)

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

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

```bash
$ curl \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"email": "test@test.com", "password": "test123456"}' \
  http://[開発環境のIPアドレス]:[開発環境のポート番号]/dj-rest-auth/login/
```

```
{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGci...",
"user":{"pk":"de0e84a7-8396-497e-aa07-e143cc0a2811",
"email":"test@test.com",
"first_name":"テスト",
"last_name":"テスト"}}%
```

# いま苦しんでいるバグ

（追記）このバグは、[公式に質問](https://github.com/jazzband/dj-rest-auth/issues/203)したら解決できました。

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

```bash:./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  # さらに追加
```

```python:./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'   # さらに追加
```

```python:./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')),   # さらに追加
]
```

これは[公式ドキュメント](https://dj-rest-auth.readthedocs.io/en/latest/)に書いてあるとおりなのですが、カスタムユーザーを使っているせいか「EmailAddress matching query does not exist」の例外が出て、さっきまで動いていたログインすらうまく動きません。

![スクリーンショット 2021-01-04 18.17.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667942/d94d08a0-cc4c-1428-324c-76ff1e02e4ae.png)

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

```
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.
```

# 参考
- [dj-rest-auth の公式ドキュメント](https://dj-rest-auth.readthedocs.io/en/latest/)
- [simple JWT の公式ドキュメント](https://django-rest-framework-simplejwt.readthedocs.io/en/latest/)