LoginSignup
2
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-05

背景

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

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

関連記事:Django + Shibboleth with RemoteUserMiddleware(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オブジェクトを作成してログインする(この設定は変更可能)。

ステップ1:configure_userをオーバーライド

もともとRemoteUserBackendは、Djangoアプリ側のUserオブジェクト作成時に、Userオブジェクトを操作するためのメソッドが存在するので、それを利用する。

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オブジェクトに入力することにした。

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.pyAUTHENTICATION_BACKENDSを変更する必要がある。

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

ステップ2:authenticateをオーバーライド

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

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

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

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_createcreated == Trueとなった場合のみである。これでは既存のユーザの情報を更新できない。

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

以上。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2