Django Rest Frameworkで認証機能をカスタムしてみた話(カスタムユーザー使用)

# はじめに
はじめに申し上げておきますと、私は5月ごろにプログラミングをはじめた初心者であり、もちろん同様に`django`も初心者です。
ですので、他のプログラマーの方から見れば変なところで詰まっていると思われます。
しかし、ネットにあまり近しい事例が載っていないと感じたので、一部始終と解決策を書いていきたいと思います(チラリと`django-rest-framework`の日本語記事が少ないというのも見かけたので、貢献できたらいいなとも考えてはいます)。
_P.S.この丸一週間の苦しみは忘れないでしょう。_

### 環境
`Django` = 3.1.1
`django-allauth` = 0.42.0
`django-rest-auth` = 0.9.5
`djangorestframework` = 3.11.1

# 流れと解決策
#### 1.自分が定義した`User`モデルが、登録及び認証では使えない。
解決法
__`django`が提供する`User`をカスタムすることにより、自分のプロジェクトに近い`User`を作成__

#### 2.`django-rest-auth`提供の登録画面で`password1`と`password2`を打っても、「この項目は必須です。」、つまり、空欄に扱われる。
解決法
__`RegisterSerializer`を編集して、必須項目を変更した。__


#### 3.登録画面を通して作成された`User`が、ログインでは認証されない(`non FieldError:与えらえた情報では認証できません`とのエラーメッセージが出る)。
解決法
__`RegisterSerializer`内の、`User`をcreateする部分(メソッド)を書き換えた。__


これが概要となります。
最後までご覧いただければ嬉しい限りです。

# 1. `User`モデルが認証・登録で使えない
## エラー概要
この問題が発生した原因は、__私が中途半端にログイン機能の実装方法を知っていたから__に他なりません。


- 私が知っていた実装法
 - `User(models.Model)`から、独自に`User`モデルを作成
 - `session`を用いて、`session`内に`user_id`(ログイン時にDB内でフィルターをしてassignされる)があるかで、画面遷移を行う(エセ認証機能といったところでしょうか)
 - 故に、`api`で閲覧のみ許可といった、permissionが実装できない
 - また、パスワードリセットといった方法がDB操作のみでしかできない

- 本来の実装法(一般的な実装法という方が近いかもしれません)
 - フレームワークが提供する`User`モデルを使用して認証を行う
 - トークンを用いてログイン済みユーザーか判定
 - ログインしているかしていないかで、`api`の操作の制限を行える
 - パスワードリセットといった操作がメール送信等で実装可能


上記のように、`User`モデルの定義の仕方というスタートラインから私は勘違いをしていました。フレームワークが`User`を提供しているというのは微かに知っていましたが、__認証機能もそのフレームワークで提供された`User`ではないと実装できない__というのが私の見解との大きな差であり、苦戦した原因でした。

おそらく、`models.Model`で定義した`User`は認証に使えないので、認証機能を実装するのであれば、__少々面倒ですが`django`提供の`User`をカスタムする形でプロジェクトの最初から使ってください。__

後に知りましたが、フレームワークを使うならフレームワーク組み込みのモデル・メソッドを使う必要があるので、その点は事前にチェックをしておいた方が良さそうです。

## 解決方法

実際に解決した流れを書いていきます。
参考にした記事はこちらです。
[公式ドキュメント](https://docs.djangoproject.com/ja/3.0/topics/auth/customizing/#django.contrib.auth.models.AbstractBaseUser)
[[Django] カスタムユーザーを利用する](https://e-tec-memo.herokuapp.com/article/124/)

まず、カスタムユーザーを定義するにあたり必要なものは、`BaseUserManager`, `AbstractUser`, `AbstractBaseUser`です。

- __`BaseUserManager`__は、__`User`作成周りの設定ができる__クラスです。管理ユーザー(この場合は`auth`の方ではなく`admin`の方だと思います)作成の項目を`username`から`email`に変更したい場合等に使います(正直特にこのBaseUserManagerに関しては理解が浅いです、申し訳ないです)。

- __`AbstractUser`__は__デフォルトの`User`が持ってる機能全て__を持っています。電話番号等少しだけ変更を加えたい場合に向いています。

- __`AbstractBaseUser`__は__最低限の機能(パスワードのハッシュ化等)__を持っていて、大胆に変更をしたい場合に向いています。


私はフィールドを多く追加する予定でしたが、後のエラーを恐れ今回は__`AbstractUser`__を使いました。尚、これら3つのクラスのソースコードは[こちら](https://github.com/django/django/blob/master/django/contrib/auth/models.py)となります。

```app.models.py(appはdjangoプロジェクト内のアプリ名)
class CustomUserManager(UserManager):

    use_in_migrations = True

    def _create_user(self, email, password, **extra_fields):
        if not email:
            raise ValueError('メールアドレスは必須項目です。')

        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, email, password=None, **extra_fields):

        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(email, password, **extra_fields)

    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')
        return self._create_user(email, password, **extra_fields)
```

基本的にほとんど元のコードのコピーを`models.py`に貼り付けました。管理ユーザー作成に求められる項目を`username`から`email`に変えただけです。

```app.models.py
class User(AbstractUser):
    # AbstractUserでpasswordは定義されているため、ここではpasswordをしません(DBにはちゃんと保存されます。)
    # 自分の要望にあわせて既に存在するフィールドも上書きをしています。
    username = models.CharField(max_length=150, unique=True)
    email = models.EmailField(max_length=100, unique=True)
    profile = models.TextField(max_length=800, blank=True, null=True)
    icon = models.ImageField(blank=True, null=True)
    background = models.ImageField(blank=True, null=True)
    # AbstractUserはfirst_name,last_nameを保持していますが、私には不要なので無効にしています
    first_name = None
    last_name = None

    is_staff = models.BooleanField(
        ('staff status'),
        default=True,
        help_text=(
            '管理サイトへのアクセス権の有無'),
    )

    is_active = models.BooleanField(
        ('active'),
        default=True,
        help_text=(
            'ユーザーのアクティブ判定'
        ),
    )

    is_superuser = models.BooleanField(
        ('superuser status'),
        default=True,
        help_text=(
            'Designates that this user has all permissions without '
            'explicitly assigning them.'
        ),
    )
    # 後になって気づきましたが、date_joinedというのが既に`AbstractUser`では入っているのでcreated_atは必要ありませんでした。
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

　　#ここに上記のBaseUserManagerのクラス名を書きます
    objects = CustomUserManager()

    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'username'
    REQUIRED_FIELD = ["username", "email"]

    class Meta:
        db_table = "users"
```

これで、私が望んでいた`User`モデルを作成することができました。

しかし、ここで忘れてはならないことがあります。

```project.settings.py(projectはdjangoプロジェクトのことです)
AUTH_USER_MODEL = 'アプリ名.User'
```
__これをしないと、作成した`User`モデルが認証ユーザーモデルとして設定されないので注意が必要です。__


また、豆知識として、作成した`User`を1対多とかで関連付けたい場合は少しトリッキーで表記方法が変わります。実際の私の`Model`を具体例として出すとこの様になります。

```app.models.py
class Comment(models.Model):
    comment = models.CharField(max_length=400)
    owner = models.ForeignKey(
#
# Userはこの場合リレーションに使えません。
#
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="comment")
    item = models.ForeignKey(Give_Item, on_delete=models.CASCADE, null=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
```
他の`Model`と同じようにモデル名を素直に入れるわけではなく、__`settings.AUTH_USER_MODEL`__と書かなければいけません。


これにて、自分の望み通りの`User`モデルをつかえて、それが認証にも使えるようになったわけです。次に認証周りに進みます。



# 2.`django-rest-auth`でフィールドが認識されない
## エラー概要
正直、これの原因については遂に解明できませんでした。しかし、1のカスタムユーザーを用いないで認証を実装する方法をいろいろ試していた段階で気付かぬうちに変に設定をいじっていたんだろうなと思います。
検証用で、全く異なる新規プロジェクトを作成したら同様のエラーが起きなかったので、ファイルのどこかに問題があったのでしょう(問題解明をしたかったです...)。

`django-rest-auth`, `django-all-auth`といった認証周りについては本記事ではほとんど説明しません。その代わり、私が参考にした記事を記載しておきます。
[django-rest-auth を用いたユーザー認証の導入](https://qiita.com/fumuumuf/items/bbe29abb2676ba18c5ce)

私が遭遇したエラーは、ユーザー登録時において`password1`,`password2`を確実に、しかも全く同じ文字を入力しているはずなのに、_「この項目は必須です」_と返ってくるというものです。
![Screen Shot 2020-09-06 at 13.40.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/676004/d68563fa-79b6-7b23-ad6e-ff977e56ef72.png)
以下が実際の画面です(尚、`POSTMAN`でも同様のコードをPOSTリクエストで送信しましたが同じレスポンスが返ってきました)。
このエラーに関しては、1のカスタムユーザーの作成に取り掛かる前から遭遇していたので、かなり苦しめられたものになります笑(説明しにくく、検索しても類似例がヒットしないというのが余計に難しくさせました)。

## 解決方法

実際の解決法について記載をしていきます。

ステータスコードが400なので、クライエント側に問題があったのは明白でした。
そこで、私が考え実行した解決策は__登録時の項目を変更する__というものです。
自分で設定ができれば上記の問題をコントロールできると考えたからです。また、パスワード確認はバックエンドで行わず、フロントエンド(私の場合は`React`です)のValidationのみでもできると判断をしたためです。

__`django-rest-auth`にて登録機能を担っているのは`RegisterSerializer`です__(`django-rest-auth`が認証のメインですが、実装には`django-allauth`が必要です)。
[公式ドキュメント](https://django-rest-auth.readthedocs.io/en/latest/configuration.html)と[ソースコード](https://github.com/Tivix/django-rest-auth/blob/master/rest_auth/registration/serializers.py)のリンクを貼っておくので興味がある方はご覧ください。

```rest_auth.registraion.serializers.py
class RegisterSerializer(serializers.Serializer):
    username = serializers.CharField(
        max_length=get_username_max_length(),
        min_length=allauth_settings.USERNAME_MIN_LENGTH,
        required=allauth_settings.USERNAME_REQUIRED
    )
    email = serializers.EmailField(required=allauth_settings.EMAIL_REQUIRED)
#
# password1とpassword2を削除してpasswordフィールド一つのみにしています。
#
    password = serializers.CharField(write_only=True)

    def validate_username(self, username):
        username = get_adapter().clean_username(username)
        return username

    def validate_email(self, email):
        email = get_adapter().clean_email(email)
        if allauth_settings.UNIQUE_EMAIL:
            if email and email_address_exists(email):
                raise serializers.ValidationError(
                    _("A user is already registered with this e-mail address."))
        return email

    def validate_password(self, password):
        return get_adapter().clean_password(password)

    def custom_signup(self, request, user):
        pass

    def get_cleaned_data(self):
        return {
            'username': self.validated_data.get('username', ''),
            'password': self.validated_data.get('password', ''),
            'email': self.validated_data.get('email', '')
        }

    def save(self, request):
        adapter = get_adapter()
        user = adapter.new_user(request)
        self.cleaned_data = self.get_cleaned_data()
        adapter.save_user(request, user, self)
        self.custom_signup(request, user)
        setup_user_email(request, user, [])
        return user
```
変更した箇所はシンプルで、`password`を一つのフィールドのみにすることです。基本的に、必要な箇所のみを編集した形です。尚、ログインの方も私は設定したかったので、`rest_auth.serializers.py`[(ソースコード)](https://github.com/Tivix/django-rest-auth/blob/master/rest_auth/serializers.py)から`LoginSerializer`を編集しました(変更内容はログイン項目を`username`のみに変更するというものです)。

当初わたしは、この`RegisterSerializer`を自分のアプリ内の`app.serializers.py`で上書きをしていました。しかし、`RegisterSerializer`が色々なメソッドと結びついており`import`のパス等で中々上手く変更が適用できなかったので、パッケージ内に直接変更を行いました。直感的に、パッケージへは直接変更をしない方がいいとは思うのですが、時間がかなりかかりそうなので今回はこの方法を採りました。皆さんは真似をしないでください。

もし、上書きをするのであれば、以下の様なコードを`settings.py`に書くことが必要です。

```settings.py

REST_AUTH_SERIALIZERS = {
    'LOGIN_SERIALIZER': 'Serializerへのパス.LoginSerializer',
    ...
}
```

また、同時に`Serializer`の名前を`CustomLoginSerializer`等に変える場合、`View`を変えるのもお忘れなく。具体的に書くとするなら、以下の様になると思います。

```views.py
class LoginView(GenericAPIView):
    """
    Check the credentials and return the REST Token
    if the credentials are valid and authenticated.
    Calls Django Auth login method to register User ID
    in Django session framework
    Accept the following POST parameters: username, password
    Return the REST Framework Token Object's key.
    """
    permission_classes = (AllowAny,)
#
# Serializerを変更後の名前に変更
#
    serializer_class = CustomLoginSerializer
    token_model = TokenModel

```

上記の変更を加えて、無事エラーは解決しユーザー登録をすることができました。
最後のエラーが待ち受けることを知らずに、私は一瞬ながら喜んでおりました。

# 3.登録画面を通した`User`がログイン時に認証されない
## エラー概要
このエラーの原因は、__私が`RegisterSerializer`のフィールドを編集し、他のメソッドが参照できなかったために発生しました。__

サブタイトルだけでは分かりづらいと思うので、詳しくエラーを説明していきたいと思います。

当然といえば当然ですが、`django_rest_framework`はユーザー登録時に`User`クエリも作成してくれます。ユーザー登録へのURLは`localhost:8000/rest-auth/registration/`なので、このURLへ必要項目と一緒にPOSTリクエストを送信すればユーザーも作成され、また、登録されるというわけです。

しかし、必要項目と一緒に`localhost:8000/rest-auth/login/`へPOSTリクエストを送信しても_`non FieldError提供された情報では認証を行えません`_と返ってきてログインが認証されません。

`User`クエリを作成する方法はもう一つあり、それは認証ではなく、APIのエンドポイントを使うことです。すなわち、`localhost:8000/api/user/`へPOSTリクエストを送信し、単純にcreateメソッドを用いて`User`を作成するということです。

後者の方法で、`User`を作成しログインを試みたところ、`User`は認証されトークンが発行されました。

これを受けて、私は以下の様に考えました。

_ユーザー登録とAPIでは`User`の作成方法が異なるのではないか_
_>APIによるcreateは`User.objects.create(...)`を用いたもので、この方法なら認証はされる_
_>>ユーザー登録時において、`User`は`User.objects.create`以外で作成されている可能性がある_

これを具体的にいうと、
__`RegisterSerializer`が`User.objects.create`以外の方法で`User`クエリを作っている可能性がある__ということです。


## 解決方法

実際に解決した流れを書いていきます。
まずは、`RegisterSerializer`のユーザー作成に関わるメソッドを見ていきます。

```rest_auth.registration.serializers.py

def save(self, request):
        adapter = get_adapter()
        user = adapter.new_user(request)
        self.cleaned_data = self.get_cleaned_data()
        adapter.save_user(request, user, self)
        self.custom_signup(request, user)
        setup_user_email(request, user, [])
        return user
```
この部分が登録時のユーザー作成を担っています。
`adapter`については、[公式ドキュメント](https://django-allauth.readthedocs.io/en/latest/advanced.html)から説明を読むことができます。 基本的に、登録周りの使用は`django-allauth`の方のドキュメントを参照しなければいけません。
>
__new_user(self, request)__: Instantiates a new, empty User.
__save_user(self, request, user, form)__: Populates and saves the User instance using information provided in the signup form.

`save_user`の方に、ロジックがありそうなので`save_user`のメソッドを参照しにいきます。ソースコードは[こちら](https://github.com/pennersr/django-allauth/blob/master/allauth/account/adapter.py)です。

```allauth.account.adapter.py

def save_user(self, request, user, form, commit=True):
        """
        Saves a new `User` instance using information provided in the
        signup form.
        """
        from .utils import user_email, user_field, user_username

        data = form.cleaned_data
        first_name = data.get('first_name')
        last_name = data.get('last_name')
        email = data.get('email')
        username = data.get('username')
        user_email(user, email)
        user_username(user, username)
        if first_name:
            user_field(user, 'first_name', first_name)
        if last_name:
            user_field(user, 'last_name', last_name)
#
# ここがpassword1となっているのが原因でした
#
        if 'password1' in data:
            user.set_password(data["password1"])
        else:
            user.set_unusable_password()
        self.populate_username(request, user)
        if commit:
            # Ability not to commit makes it easier to derive from
            # this adapter by adding
            user.save()
        return user

```
原因は、`save_user`においてはパスワード生成は`password1`が元となっていたからでした。
__つまり、私が`RegisterSerializer`のフィールドを`password`に変更していたから発生したエラーです。__

上記の箇所を`password`に変更したところユーザー登録時に作成された`User`も無事ログイン認証に成功しました。

以上が今回遭遇したエラーの全てです。

# 感想

今回の解決に時間がかかったのは、間違いなく最初からカスタムユーザーを使わなかったことです。一週間の最初の2,3日は、独自作成した`User`での実装方法を模索していたからです。最初から使っていれば、変にこねくり回すことなく2のエラーも起きなかったかもしれません(知識不足だったのもありますが)。

しかし、今回で学んだことは私自身としては大きかったです。

__- 公式ドキュメントは細かいところまで目を通すこと__
__- パッケージのソースコードを場合によっては参照すること__
__- `git reset --hard HEAD^`を乱用しない笑__


知識不足もありましたが、たった一つの機能で、しかもそこまで難易度が高くないだろうものにここまで時間がかかるとは思っていませんでした。

ポートフォリオ作成に関しては、`django-rest-framework`をこのまま使用していくので、日本語記事を増やすためにも気づいたことがあればアウトプット・共有をしていきたいと思います。


拙く、ひどく読みづらい、長い文章だったとは思いますが目を止めていただきありがとうございました。
間違っている箇所や、アドバイス等コメントをいただければ幸いです。