認証周りは甘く見ると痛い目を見るという話(django-allauthを使った認証を導入してみて編)

## はじめに
今回6週間でチームビルディングからプロダクト公開までやるプロジェクトに参加させて頂きました。
プロジェクト全体に関しては以下のリンクでまとめておりますのでそちらを御覧ください。
私についてはその記事や過去記事等ご覧になっていただければと思いますが、端的に言うと未経験でエンジニアを目指して就職活動をしているものです。

[プロジェクトについてはこちら](https://qiita.com/shitikakei/items/18d94edbedac99ab196a)

閑話休題、この記事ではそのプロジェクトで私が担当した機能のうち認証の部分の話になります。
サインアップ、ログイン、ログアウト、パスワードリセット、メールアドレスリセット……といわゆる認証に関わる機能は実装する機会も多いですし、
初学者にとってはなんとなくマストなスキルだなと感じて率先してやりたくなってしまうものです。
今回はチームで開発してる中でそんな私がやってみて見事に苦い思いをした話と一緒にDjangoでソーシャルログインを導入する際に選択肢となるパッケージである
`django-allauth`の導入について少し掘り下げていきたいと思います。

## django-allauthを入れるとできること

- ローカルアカウントとソーシャルアカウントの両方のサインアップ
- 複数のソーシャルアカウントをローカルアカウントに接続する
- ソーシャルアカウントの連携解除(連携しているローカルアカウントを残す場合はパスワードの設定が必要)
- ソーシャルアカウントにオプションでインスタントサインアップを設定できる
- 電子メールアドレス管理（複数の電子メールアドレス、プライマリの設定）
- パスワードリセット
- メールアドレスリセット

ご覧になるとわかるように導入すると大体の認証周りで必要なことが一手にできるようになります。
ただし、これらはDjango標準の認証の機能に置き換わるような形で使われるので私のように

**多分解説サイトとか見ながらやればなんとかなるでしょう**

みたいな甘い考えでいると
思ったようにうまく行かずに難儀することになります。

## そもそも認証を実装するときに気をつけたいこと

今回はチームで1つのWebアプリケーションを作るという企画(以下プロジェクトと呼称します)に参加してそこでアプリの機能を2つほど担当し、それとは別に認証も担当するという形で請け負ったというのがこの話の前提です。

で、そのプロジェクトに参加して実際にチームで開発した際に感じた認証を実装するときに気をつけなければならない点というのが以下のようになります。
(お恥ずかしながら未だに求職中の身で、実際の実務経験がないのでその上で感じたこととして受け取っていただけると幸いです)

- 初回のMTGでPL及び設計担当の方とユーザーモデルと認証の仕方についてしっかりと擦り合わせをして、仕様を決める

→
ユーザーモデルは認証と切っても切り離せない関係であり、同時にWebアプリケーションにとってユーザーモデルは各機能にで使うモデルと1番外部キーなどで参照関係になりやすいものです。
つまりユーザーモデルは一度マイグレーションしたらコロコロ変えられるものではないし、そもそもそういうことは極力やらないというのがベターなのだなと思いました。
なので、初回のMTGで設計と認証の仕様についてしっかりと見解を共有し、採用するパッケージなども選定した上で設計の方に設計を行ってもらったほうが確実にトラブルを回避しやすいなと思いました。
今回私は担当するにあたりこれを怠った結果、モデルを勝手に書き換えたり、仕様を勝手に決めて結果ベターではない実装につながるようなことをしてしまいました。
チームで開発するにあたってこれはとてもよくないことでした……

- 認証の機能は最初に実装を行うのがベター

先述の通り、認証はユーザーモデルと切っても切れない関係であり例えば今回のようにDjango標準の認証ではなく、パッケージを使った認証に置き換えるといったようなことをすると、パッケージに対する理解不足や慣れてないといった要因で不都合があったり、うまく行かなったりするところが多々出てきたりします。
そうなるとユーザーモデルを定義し直したり、マイグレーションをやり直したりする必要が出てくるのですがそれをやるとDBの方で依存関係のエラーが出てきてしまったり、今回の場合は当初`django-allauth`を導入するということを決めていなかったのもあって、ディレクトリを修正しなければならなくなったりと想定外のタスクが増えてしまいました。
また、それとは別に認証が実装されていないと開発できない機能というのも中にはあり、今回のプロジェクトでもその機能があったのですが、今回認証後回しにした結果その機能の開発が遅れてしまうというトラブルが発生しました。(これはPLの方も反省すべき点として挙げていました)

## django-allauthについて

導入するとできることは冒頭で書いた通りです。
導入するにはpipなどでinstallコマンドを叩いてください。

```
# 例
pip install django-allauth

```

インストールしたらDjangoのsettigs.pyファイルで以下の項目の追加を行います

```python

AUTHENTICATION_BACKENDS = [
    ...
    # Needed to login by username in Django admin, regardless of `allauth`
    'django.contrib.auth.backends.ModelBackend',

    # `allauth` specific authentication methods, such as login by e-mail
    'allauth.account.auth_backends.AuthenticationBackend',
    ...
]

INSTALLED_APPS = [
    ...
    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',

    # ソーシャルログインで使いたいソーシャルプロパイダを指定してください。複数指定することも可
    'allauth.socialaccount.providers.google',
]

SITE_ID = 1


```

最低限指定する項目は以上です。
Djangoを扱っているのであればTEMPLATEの部分やstaticフォルダの設定についてはおわかりかと思いますので詳細は割愛します。
テンプレートに関しては大抵は`templates`で指定をされると思いますが、もし別にフォルダを作ってallauthで使うtemplateは全部そこで管理をしたいというのであれば個別に設定をお願いします。

あとはurls.py(アプリケーション個別に用意するものではなく、settings.pyと同じディレクトリにあるファイルの方です)に以下の項目を追加します。

```python

urlpatterns = [
    ...
    path('accounts/', include('allauth.urls')),
    ...
]

```

あとは認証機能のアプリケーションにある(ユーザモデルがあるアプリ)urls.pyで下記のようにルーティングすればOKです。

```python
from django.contrib import admin
from django.urls import include, path
from django.views.generic import TemplateView

app_name = 'accounts'

urlpatterns = [
    path('accounts/login/',
         TemplateView.as_view(template_name='login.html'), name='login'),
    path('accounts/logout/',
         TemplateView.as_view(template_name='logout.html'), name='logout'),
    path('accounts/signup/',
         TemplateView.as_view(template_name='signup.html'), name='signup'),
]


```

ここまで終わったらあとはマイグレーションを行ってください。
マイグレーションに関しては後述するトラブルシューティングの項目も参照してください。
マイグレーションを行ったら`createsuperuser`コマンドで管理ユーザーを作り管理サイトにアクセスしてください。
`django-allauth`での認証に関する設定等参考記事のドキュメントを参照してください。

ちなみに`django-allauth`を導入してマイグレーションすると、デフォルト及び各アプリのモデルの他に

- サイト
- アカウント
- ソーシャルアカウント

<img width="486" alt=" allauthモデル.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/428779/912db31e-805c-28eb-8523-01f87ac50a39.png">



という3つのモデルが自動で追加されます。
そのうちソーシャルアカウントのモデルには以下の画像のように各ソーシャルプロパイダのキーなどを登録する必要があります。
`settings.py`で指定もできますが今回は管理サイトに入って登録する形をとりました。

<img width="823" alt="socialモデル.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/428779/fe73c1cf-885f-63aa-f9ea-8c6798e5ffc3.png">

あとはテンプレートを作れば完成です。もちろん、CSSを適用できたりもします。
ただし、テンプレートという仕組みが慣れていない人には難しく、かつ`django-allauth`のそれのカスタムは少しややこしい[参考](https://qiita.com/s-katsumata/items/b667c81a127223d2e868) ので作者の方のリポジトリからテンプレート部分を拝借してそれを適用し、実際にブラウザ上でどのように表示されているかDeveloper Toolなどを使って確認しながら編集するといいかと思います。
特に個別にCSSを適用させたいとなるとテンプレートだけでは正直面倒なのでこの工程でやることをおすすめします。

[django-allauthのリポジトリ](https://github.com/pennersr/django-allauth)

手順としては

1. templates配下にaccountフォルダを作る。ソーシャルログインを導入する場合はsocialaccountフォルダも作る
2. accountではソーシャルログイン以外の認証画面のテンプレートなどを、socialaccountフォルダではソーシャルログインに関するテンプレートなどを管理するようにする。

というだけです。
CSSの適用に関しては普通のDjangoのそれと変わらないので割愛します。

## 今回のプロジェクトで採用した項目

今回のプロジェクトの趣旨としては大きく以下の3点です。

- 参加者のエンジニアリングの場を作るということ

- チームでの開発の手順や方式を体験すること

- 上記2点を優先した上で、Webアプリとしての完成度を参加者のできる範囲で高めていく

今回、先述の通り`django-allauth`を使うにあたって実装したのは

- 通常の認証機能(ログイン、ログアウト、会員登録)
- ソーシャルログイン

の2点だけでした。
これはまず`django-allauth`に私が不慣れてあることと、認証の作業を優先しなかった結果他の参加者の進捗が遅れつつあったということを考えた結果です。
本来ならば最低限は以上に加えて

- パスワード変更
- パスワードリセット
- メールアドレスリセット
- ログイン失敗回数制限
- ソーシャルログイン連携解除

の5点は盛り込まないといけないはずなのでそれができなかったのは大きな反省点だと感じています。


## セッションに関する処理

追加のタスクにおいて

**アカウント作成ボタン押下時にセッションからアンケート結果を登録ユーザーに紐づけ**

という処理の追加を振られたので対応したものです。

```python

# 具体的な処理のコード
def update_question_result(user_info, question_result_info):
    question_result_obj = QuestionResult.objects.get(
        id=question_result_info)
    user_obj = CustomUser.objects.get(id=user_info)
    question_result_obj.customuser_obj = user_obj
    data_to_update = question_result_obj.save()

    return data_to_update


# 上記の処理を呼び出して使うコード(views.pyに当たる部分)
class Top(LoginRequiredMixin, View):
    def get(self, request, *args, **kwargs):
        if 'unsaved_answer' in self.request.session and self.request.session['unsaved_answer'] == True:
            user_info = self.request.user.id
            question_result_info = self.request.session["primary_key"]
            update_question_result(user_info, question_result_info)
            self.request.session['unsaved_answer'] = False
        return render(request, 'top/top_page.html', context=context)


```

実際のコードから処理部分のコードを抜粋しました。
実際には1つ目のコードを別の`.py`ファイルに定義して、`views.py`にあたるファイルに定義してある2番のコードで呼び出して使っているという状態になります。

処理としては

1. アンケート回答時にDBにアンケート内容が保存される
2. その時、ユーザーが未ログイン状態だった場合、セッション内に`['unsaved_answer'] == True`としてフラグが立つ
3. その後アカウントを作成し、Topページへリダイレクトしたタイミングでログインも同時に完了するので、その際にセッションを確認して、`['unsaved_answer'] == True`フラグを確認した場合、ユーザー情報からユーザーidを取得し、それをキーにしてCustomUserテーブルからユーザー情報をオブジェクトの形で取得し、さらにその内容で1で登録したレコードの`question_result_obj.customuser_obj`対して更新処理を行うという処理になります。
`question_result_obj.customuser_obj `は主キーかつリレーションの関係上オブジェクトでの登録になっているので更新の際もオブジェクトで更新をかけないとエラーになります。

## トラブルシューティング

- マイグレーションの順番

→
これは`django-allauth`に限った話ではないですが、ユーザーモデルのマイグレーションは外部キーなどで参照関係にする都合おざなりにマイグレーションを行うと依存関係でエラーを起こしたりする可能性が高いです。
なので慣れないうちは

1. 各アプリケーションで使うmodelごとに個別に`makemigraton`と`migrate`を行う(それぞれのコマンドの後ろにアプリケーション名を指定)
2. `django.contrib.admin`や`django.contrib.auth`の`makemigraton`と`migrate`を行う
3. `django-allauth`の導入の設定をした上でもう一度`makemigraton`と`migrate`を行う

といった手順を踏むといいと思います。

- マイグレーションし直したい

→ データベースのファイル、各アプリのMigrationsフォルダを削除して、`settings.py`及び`urls.py`から`django-allauth`に関わる部分をすべてコメントアウトまたは削除してから、各アプリで`makemigrations`を行い、その上で`migrate`コマンドを実行してください。
ちなみに`django.contrib.admin`や`django.contrib.auth`のあたりでエラーが起きてうまくマイグレーションなどができないといった場合も同じように`settings.py`からそれぞれの項目をコメントアウトしてみると打開ができることがあります。

認証周りのトラブルで多いのはこのマイグレーションの部分と感じずにはいられないほど私も今回悩まされた部分なのでもし同じような方がいたら助けになれば幸いです。

## 終わりに

認証はフレームワークを使えば標準で備わっているのでなんとなく実装しようと思えばできるものですが、やはりちゃんとやろうとか少し変わったことをやろうと思うと難しいものですね。
やはり認証周りはなめてかかってはいけないということを思い知らされました。

そして、もう一つ今回プロジェクトで気付かされたのは成果物によって認証方法は目的によって適切に決めないといけないということでした。
もちろん、セキュリティをおざなりにするとかそういうことではなく、当初私はこれをやるにあたりログインの方法はメールアドレスとパスワードでやろうと思い、、
実際にメール認証で仮登録から本登録になるようにという処理のコードを書いていました。
しかし、PLの方に

>今回のものはサービスというよりも、あくまで成果物(ポートフォリオ)であり、それを考えるならユーザーには試しに使ってもらいやすいようにするのがベターなのでは？

メールアドレスでのログインは打ち込みが手間なのと、メール認証をしている間に面倒になってもういいやとなってしまう人もいるのでは？という視点からのご指摘を頂いて、
ユーザー名とパスワードでのログインの方法に変えたという経緯があります。
確かに言われてみればそれも一理ありますよね。
成果物として考えるのであれば自分はこれだけスキルがありますとか作り込みましたということを主張するのも大切ですが、実際に使ってもらう人のことも考えないとダメなのだなということを気付かされました。

## 追記

ソーシャルログイン部分については本番環境でエラーが出てしまったみたいで泣く泣く削除となりました。
私が対応できれば良かったのですが、そもそもリリース間近で万が一他に支障をきたしてしまわないことと当初の想定ではない機能であったための処置のようです。
やはり、認証関係は一筋縄ではいかないみたいです……次はがんばります。


## 参考

[django-allauthドキュメント](https://django-allauth.readthedocs.io/en/latest/index.html)
[django-allauthの設定](https://django-allauth.readthedocs.io/en/latest/configuration.html)
[【Django】django-allauthを使ったソーシャル認証の実装手順 ](https://sinyblog.com/django/django-allauth-002/#OAuth)
[django-Django django-allauthで、サクッとソーシャルログイン機能を実装 ](http://www.webcyou.com/?p=10212)
[django-allauthのテンプレートファイルをカスタマイズする手順](https://qiita.com/s-katsumata/items/b667c81a127223d2e868)
[Django allauthにおけるログイン画面の作成](https://qiita.com/kwashi/items/8421cf45bfaee9e6e238)