Django RemoteUserMiddleware ログイン時にユーザ情報を更新する方法

#背景
DjangoでSSOをやるにあたり、RemoteUserMiddlewareを使用する。

Djangoアプリ側ではSAMLモジュール（自環境ではShibboleth）によって設定される環境変数REMOTE_USERをID情報としてユーザの識別を行うが、このときにID情報以外のattribute（例えばemail）もDjangoアプリ側のUserクラスに取り込みたい。

関連記事：[Django + Shibboleth with RemoteUserMiddleware](https://qiita.com/hiro-jp/items/9d5a20baebc1c7d8f911)（RemoteUserMiddlewareの導入の解説）

#環境
- Windows Server 2016（直接は関係なし）
- Apache 2.4（直接は関係なし）
- Python 3.7.7（直接は関係なし）
- Django 3.0
- Shibboleth Service Provider 3.x（直接は関係なし）

#前提知識
`RemoteUserBackend`では、環境変数REMOTE_USERをusernameに持つオブジェクトを`settings.AUTH_USER_MODEL`で指定されたUserモデルから探す。見つかれば、そのユーザとしてログインする。見つからなければ、REMOTE_USERをusernameに持つUserオブジェクトを作成してログインする（この設定は変更可能）。

#ステップ１：`configure_user`をオーバーライド
もともと`RemoteUserBackend`は、Djangoアプリ側のUserオブジェクト作成時に、Userオブジェクトを操作するためのメソッドが存在するので、それを利用する。

```python:django.contrib.auth.backends
class RemoteUserBackend(ModelBackend):
...
    def configure_user(self, request, user):
        """
        Configure a user after creation and return the updated user.

        By default, return the user unmodified.
        """
        return user
```

適当なapp内に`backends.py`（名前はなんでもよい）を作成。

```
appname
└── appname
    ├── templates
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    ├── views.py
    ├── wsgi.py
    └── backends.py  # ←作成
```

`configure_user`をオーバーライドする。今回は、HTTP HeaderにShibbolethで追加した`ATR_mail`というattributeをUserオブジェクトに入力することにした。

```python:backends.py
from django.contrib.auth.backends import RemoteUserBackend

class MyRemoteUserBackend(RemoteUserBackend):
    def configure_user(self, request, user: MyUser):
        user.email = request.META['ATR_mail']
        user.save() # ←忘れないように！！
        return user
```

これを有効化するためには、`settings.py`の`AUTHENTICATION_BACKENDS`を変更する必要がある。

```python:settings.py
AUTHENTICATION_BACKENDS = (
    # 'django.contrib.auth.backends.RemoteUserBackend', # ← 削除
    'appname.backends.MyRemoteUserBackend', # ← 追加
    'django.contrib.auth.backends.ModelBackend',
)
```

#ステップ２：`authenticate`をオーバーライド
`configure_user`をオーバーライドしただけでは、**Djangoにとって初見のREMOTE_USERに対して新しくUserオブジェクトを作成してログインした場合にのみ、`configure_user`が実行される。**

IdP側のattribute情報が変わっても既存のUserオブジェクトは更新されないが、これは不便な場合のほうが多いと思われる。例えば、企業内のSSOを利用していて、部署や内線番号が時々変わる場合など。

その場合、`authenticate`に手を入れないといけない。幸い、先ほど作成した`backends.py`の`MyRemoteUserBackend`が使える。大半はsuperclassの`RemoteUserBackend`の`authenticate`のコピペである。

```python:backends.py
from django.contrib.auth.backends import RemoteUserBackend

from django.contrib.auth.models import User

import inspect
import warnings

from django.contrib.auth import get_user_model
from django.utils.deprecation import RemovedInDjango31Warning

UserModel = get_user_model()

class MyRemoteUserBackend(RemoteUserBackend):

    def authenticate(self, request, remote_user):
        """
        The username passed as ``remote_user`` is considered trusted. Return
        the ``User`` object with the given username. Create a new ``User``
        object if ``create_unknown_user`` is ``True``.

        Return None if ``create_unknown_user`` is ``False`` and a ``User``
        object with the given username is not found in the database.
        """
        if not remote_user:
            return
        user = None
        username = self.clean_username(remote_user)

        # Note that this could be accomplished in one try-except clause, but
        # instead we use get_or_create when creating unknown users since it has
        # built-in safeguards for multiple threads.
        if self.create_unknown_user:
            user, created = UserModel._default_manager.get_or_create(**{
                UserModel.USERNAME_FIELD: username
            })
            if created:                                      # ← 注目
                args = (request, user)
                try:
                    inspect.getcallargs(self.configure_user, request, user)
                except TypeError:
                    args = (user,)
                    warnings.warn(
                        'Update %s.configure_user() to accept `request` as '
                        'the first argument.'
                        % self.__class__.__name__, RemovedInDjango31Warning
                    )
                user = self.configure_user(*args)            # ← 注目
            else:                                            # ← 追加
                user = self.configure_user(request, user)    # ← 追加
        else:
            try:
                user = UserModel._default_manager.get_by_natural_key(username)
            except UserModel.DoesNotExist:
                pass
        return user if self.user_can_authenticate(user) else None

    def configure_user(self, request, user: User):
        user.email = request.META['ATR_mail']
        user.save()
        return user

```

「注目」に書いてあるように、もともとは`configure_user`が呼ばれるのは、`UserModel._default_manager.get_or_create`で`created == True`となった場合のみである。これでは既存のユーザの情報を更新できない。

したがって、「追加」の２行を加えて、`created != True`の場合も`configure_user`が呼ばれるようにする。Userオブジェクトの新規作成の場合と更新の場合で動作を分けたい場合は、`configure_user`を真似て`update_user`などを新たに作って、それが呼ばれるようにすればよい。

以上。