Brian,
I reread your post and realized I mistakenly thought you were talking about LDAP syncing. I now realize you were wanting to sync user details from OIDC claims, but thought it might not be worth the effort. Just for fun I decided to look into how difficult it would be.
There doesn't seem to be a way to pass environment variables from Apache to Netbox while using ProxyPass (mod_proxy) and Gunicorn. So the alternative to environment variables is for Apache to set HTTP headers to pass parameters into the Netbox environment. One has to take care to unset these headers in requests from end users, otherwise it could be possible to spoof the headers used in authentication. I prefer to use environment variables and changed my environment to use mod_wsgi and not ProxyPass(mod_proxy) and Gunicorn to avoid the possibility of spoofed HTTP headers.
HTTP headers passed into the WSGI environment, either by Gunicorn or mod_wsgi, are converted to upper case and are prefixed with "HTTP_" to avoid the possibility of spoofing or overwriting environment variables. So the HTTP header "Authorization" becomes "HTTP_AUTHORIZATION", and "REMOTE_USER" becomes "HTTP_REMOTE_USER" (although I couldn't get this one to work for some reason).
From my reading of the mod_auth_openidc documentation(1), UserInfo claims retrieved by Apache can be configured to pass UserInfo claims as individual headers/variables (prefixed with "OIDC_CLAIM_", or "HTTP_OIDC_CLAIM_" if passed as HTTP headers), a JSON object passed as the "OIDC_userinfo_json" header/environment variable, or signed/encrypted JWT. The second option, JSON, seems to be the easiest to use.
From there I would subclass RemoteUserBackend and name it OidcUserInfoSyncRemoteUserBackend, and override the authentication method and create a new private method "oidc_userinfo_sync". I'd still use the "REMOTE_USER" variable to retrieve the User Model from the Django user management backend, but I would include a call to the newly created "_oidc_userinfo_sync" method to sync user details from the UserInfo claims.
With all of this in mind I came up with the following. Keep in mind I haven't tested this "solution" so your mileage my vary. It's more than likely I'm forgetting something. If you decide to try it out, let me know how it goes.
#Apache configuration
OIDCRemoteUserClaim email # Keep in mind some of the OIDC Providers (OP) don't guarantee the uniqueness of the email claim
RequestHeader set REMOTE_USER expr=%{REMOTE_USER}
OIDCPassUserInfoAs json
OIDCPassClaimsAs headers #I would prefer 'environment', but 'headers' should work with the existing ProxyPass configuration.
#netbox/netbox/settings.py file
AUTHENTICATION_BACKENDS = [
'utilities.backends.OidcUserInfoSyncRemoteUserBackend', # ADD
'django.contrib.auth.backends.ModelBackend',
]
MIDDLEWARE = (
'debug_toolbar.middleware.DebugToolbarMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.CustomHeaderRemoteUserMiddleware', # ADD. make sure this is after AuthenticationMiddleware and before SessionAuthenticationMiddleware
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'utilities.middleware.LoginRequiredMiddleware',
'utilities.middleware.APIVersionMiddleware',
)
# You'll have to subclass RemoteUserMiddleware to change the header from 'REMOTE_USER' to 'HTTP_REMOTE_USER' if you want to keep the ProxyPass configuration and Gunicorn. Place this in netbox/utilities/middleware.py
class CustomHeaderRemoteUserMiddleware(RemoteUserMiddleware):
header = "HTTP_REMOTE_USER"
#copy into the new netbox/utilities/backends.py file
import json
from django.contrib.auth.backends import RemoteUserBackend
from django.contrib.auth import get_user_model
UserModel = get_user_model()
class OidcUserInfoSyncRemoteUserBackend(RemoteUserBackend):
#HTTP header
USERINFO_JSON_HEADER = 'HTTP_OIDC_USERINFO_JSON'
# Environment variable
#USERINFO_JSON_HEADER = 'OIDC_userinfo_json'
def _oidc_userinfo_sync(self, request, user):
try:
USERINFO_DICT = json.loads(getattr(request.META, USERINFO_JSON_HEADER, ''))
except ValueError:
USERINFO_DICT = ''
if user and USERINFO_DICT:
email = getattr(USERINFO_DICT, 'email', '')
given_name = getattr(USERINFO_DICT, 'given_name', '')
family_name = getattr(USERINFO_DICT, 'family_name', '')
user.firstname, user.lastname, user.email = given_name, family_name, email
user.save()
def authenticate(self, request, remote_user):
"""
The username passed as ``remote_user`` is considered trusted. This
method simply returns the ``User`` object with the given username,
creating a new ``User`` object if ``create_unknown_user`` is ``True``.
Returns 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:
user = self.configure_user(user)
else:
try:
user = UserModel._default_manager.get_by_natural_key(username)
except UserModel.DoesNotExist:
pass
#Sync user model to any claims present
self._oidc_userinfo_sync(request, user)
return user if self.user_can_authenticate(user) else None