Use RemoteUserMiddleware to do SSO with Django.
On the Django app side, the environment variable REMOTE_USER set by the SAML module (Shibboleth in the own environment) is used as ID information to identify the user, but at this time, attributes other than the ID information (for example, email) are also included in the User class on the Django app side. I want to capture it.
Related article: Django + Shibboleth with RemoteUserMiddleware (Explanation of introduction of RemoteUserMiddleware)
--Windows Server 2016 (not directly related) --Apache 2.4 (not directly related) --Python 3.7.7 (not directly related)
RemoteUserBackend
searches for an object whose username has the environment variable REMOTE_USER from the User model specified by settings.AUTH_USER_MODEL
. If found, log in as that user. If not found, create a User object with REMOTE_USER as username and log in (this setting can be changed).
configure_user
Originally, RemoteUserBackend
uses the method for manipulating the User object when creating the User object on the Django application side.
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
Create backends.py
(any name) in a suitable app.
appname
└── appname
├── templates
├── __init__.py
├── settings.py
├── urls.py
├── views.py
├── wsgi.py
└── backends.py #← Create
Override configure_user
. This time, I decided to input the attribute ʻATR_mail` added by Shibboleth to the HTTP Header in the User object.
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() #← Don't forget! !!
return user
To enable this, you need to change ʻAUTHENTICATION_BACKENDS in
settings.py`.
settings.py
AUTHENTICATION_BACKENDS = (
# 'django.contrib.auth.backends.RemoteUserBackend', #← Delete
'appname.backends.MyRemoteUserBackend', #← Add
'django.contrib.auth.backends.ModelBackend',
)
Overriding configure_user
will only execute configure_user
if you create a new User object for Django's first-time REMOTE_USER and log in. ** **
Even if the attribute information on the IdP side changes, the existing User object is not updated, but this seems to be inconvenient in many cases. For example, if you are using SSO in a company and the department or extension number changes from time to time.
In that case, you have to modify ʻauthenticate. Fortunately, you can use the
MyRemoteUserBackend of
backends.pycreated earlier. Most are copies of the superclass
RemoteUserBackend ʻauthenticate
.
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: #← Attention
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) #← Attention
else: #← Add
user = self.configure_user(request, user) #← Add
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
As stated in "Attention", configure_user
is originally called only when created == True
in ʻUserModel._default_manager.get_or_create`. This does not allow you to update existing user information.
Therefore, add two lines of "add" so that configure_user
is called even when created! = True
. If you want to separate the operation for creating a new User object and updating it, you can imitate configure_user
and create a new ʻupdate_user` etc. so that it will be called.
that's all.
Recommended Posts