#背景
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オブジェクトを操作するためのメソッドが存在するので、それを利用する。
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オブジェクトに入力することにした。
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
を変更する必要がある。
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.py
のMyRemoteUserBackend
が使える。大半はsuperclassのRemoteUserBackend
のauthenticate
のコピペである。
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
となった場合のみである。これでは既存のユーザの情報を更新できない。
したがって、「追加」の2行を加えて、created != True
の場合もconfigure_user
が呼ばれるようにする。Userオブジェクトの新規作成の場合と更新の場合で動作を分けたい場合は、configure_user
を真似てupdate_user
などを新たに作って、それが呼ばれるようにすればよい。
以上。