Your Django app sits behind a corporate SSO proxy that puts the authenticated username in a request header; how do you wire this up safely?
answer
- the web server already authenticated
- a middleware plus a backend
- header name as a request.META key
- who can set that header?
basics
~10 sAdd a RemoteUserMiddleware subclass whose header attribute names the proxy's request.META key, after AuthenticationMiddleware, and list RemoteUserBackend in AUTHENTICATION_BACKENDS. The proxy must always strip or overwrite that header, or anyone can impersonate anyone.
solid answer
~40 sDjango's `RemoteUserMiddleware` reads a trusted username from `request.META` and calls `authenticate(request, remote_user=...)`, which `RemoteUserBackend` answers without a password. I subclass the middleware with `header = "HTTP_X_AUTH_USER"` (the `request.META` form of `X-Auth-User`), place it after `AuthenticationMiddleware`, and put `RemoteUserBackend` (or a subclass) in `AUTHENTICATION_BACKENDS`. In the subclass I set `create_unknown_user = False` if accounts must be pre-provisioned, override `clean_username()` to normalise the value, and `configure_user(request, user, created)` to sync attributes. The safety rule: the header is a credential, so the proxy must set or strip it on every request and the app must be unreachable except through the proxy, including the underscore variant `X-Auth_User`. Under ASGI even the default `REMOTE_USER` arrives as an HTTP header.
code
python · 16 linesfrom django.test import TestCase, override_settings
@override_settings(
MIDDLEWARE=[
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"accounts.middleware.ProxyUserMiddleware",
],
AUTHENTICATION_BACKENDS=["accounts.backends.DirectoryHeaderBackend"],
ROOT_URLCONF="accounts.test_urls",
)
class ProxyHeaderTests(TestCase):
def test_unknown_user_is_refused(self):
response = self.client.get("/whoami/", headers={"X-Auth-User": "[email protected]"})
self.assertContains(response, "anonymous")go deeper
Know that Django can trust a username set by the web server, using RemoteUserMiddleware and RemoteUserBackend together.
Explain the header attribute as a request.META key, create_unknown_user, clean_username() and configure_user(), and the middleware ordering requirement.
Show the threat model: header spoofing, proxy bypass, underscore normalisation, the ASGI difference, and whether ModelBackend should stay as a fallback.
Decide who owns identity: provisioning rules, which attributes the directory may overwrite, and how a retired proxy or directory is revoked.
## The model: authentication happens before Django In many intranets a **reverse proxy or web server** performs sign-in (Kerberos, a corporate identity provider, client certificates) and forwards the request with the verified username attached. Django's job is only to **trust** that value and map it to a user row. Django ships a pair for this in `django.contrib.auth`: - `RemoteUserMiddleware` reads the username from `request.META` and calls `authenticate(request, remote_user=username)`, then `login()`. - `RemoteUserBackend` implements `authenticate(self, request, remote_user)`: no password, the value is trusted. ## Wiring it ```python # accounts/middleware.py from django.contrib.auth.middleware import RemoteUserMiddleware class ProxyUserMiddleware(RemoteUserMiddleware): header = "HTTP_X_AUTH_USER" # request.META key for the X-Auth-User header ``` ```python # accounts/backends.py from django.contrib.auth.backends import RemoteUserBackend class DirectoryHeaderBackend(RemoteUserBackend): create_unknown_user = False # only pre-provisioned staff may enter def clean_username(self, username): return username.split("@", 1)[0].lower() # '[email protected]' -> 'jdoe' def configure_user(self, request, user, created=True): if user is not None: # the proxy must strip X-Auth-Email from clients too user.email = request.META.get("HTTP_X_AUTH_EMAIL", user.email) user.save(update_fields=["email"]) return user ``` ```python # settings.py MIDDLEWARE = [ # ... "django.contrib.auth.middleware.AuthenticationMiddleware", "accounts.middleware.ProxyUserMiddleware", # ... ] AUTHENTICATION_BACKENDS = ["accounts.backends.DirectoryHeaderBackend"] ``` ## The knobs | Attribute or method | Default | What it controls | |---|---|---| | `RemoteUserMiddleware.header` | `"REMOTE_USER"` | Which `request.META` key holds the username | | `RemoteUserMiddleware.force_logout_if_no_header` | `True` | Log out a remote-authenticated session when the header disappears | | `RemoteUserBackend.create_unknown_user` | `True` | `get_or_create()` a user for an unseen name, or refuse | | `RemoteUserBackend.clean_username()` | returns input | Normalise the raw value before lookup | | `RemoteUserBackend.configure_user(request, user, created=True)` | returns user | Sync attributes after lookup or creation | The backend inherits from `ModelBackend`, so inactive users are refused and permissions still come from the database. `AllowAllUsersRemoteUserBackend` drops the active check. For a proxy that authenticates only on a login URL and then lets the app keep the session, use `PersistentRemoteUserMiddleware`, which sets `force_logout_if_no_header = False`. Middleware behaviour per request: if the header is missing and the session was created by a `RemoteUserBackend`, the user is logged out (unless persistent). If a different username arrives than the session holds, the middleware authenticates the new name and logs it in, replacing the old session. ## Why it is dangerous The header **is** the password. Anyone who can reach Django with a forged header is whoever they claim to be. 1. **The proxy must set or strip the header on every request**, never passing a client-supplied value through. 2. **Nothing may bypass the proxy**: no public port on the app server, no internal route that skips it. 3. **Watch normalisation.** `X-Auth-User` and `X-Auth_User` both become `HTTP_X_AUTH_USER` in `request.META`; the proxy must also drop the underscore spelling. 4. **WSGI vs ASGI.** Under WSGI the default `REMOTE_USER` key is a server environ variable that no client header can produce. Under ASGI there is no environ, so the default is read from `HTTP_REMOTE_USER`, a client header, and the proxy rule applies to every configuration. ## Production judgement - Keep `ModelBackend` out of the list unless a break-glass local login is intended; if it stays, remember the admin then accepts passwords too. - `createsuperuser` and the admin user pages still manage database users; they do not integrate with the directory. - Prefer `create_unknown_user = False` plus explicit provisioning when the proxy fronts more than your organisation's staff. - Log `configure_user()` changes: they are how directory attributes enter your database. ## Testing without the proxy The test client can play the proxy: `self.client.get(url, headers={"X-Auth-User": "jdoe"})` places the value in `request.META` under `HTTP_X_AUTH_USER`, exactly as a real proxy header would arrive. Worth covering: - A provisioned user is logged in, and the session's `_auth_user_backend` names your remote backend. - An unprovisioned name stays anonymous when `create_unknown_user = False`. - Changing the header between requests switches the session to the new user. - Removing the header logs a remote-authenticated user out, unless the persistent middleware is used. These tests prove the Django half; the header-stripping half lives in the proxy configuration and needs its own check in the deployment pipeline.
- Why must the remote-user middleware come after AuthenticationMiddleware?It reads `request.user` to see whether the session already belongs to the header's user and calls `login()` on a new one. Without `AuthenticationMiddleware` earlier in `MIDDLEWARE`, `request.user` does not exist and the middleware raises `ImproperlyConfigured` naming the missing class.
- When would you choose PersistentRemoteUserMiddleware?When the proxy authenticates only on a dedicated login URL, for example an expensive Kerberos negotiation, and the rest of the site relies on the Django session. It keeps the user logged in when later requests lack the header instead of logging them out.
saying these in an interview costs you the question
- Trust the header because only our own proxy normally sends it.
- RemoteUserBackend still checks a password against the database.
- Header names with dashes and underscores can never collide in request.META.
- Under ASGI the default REMOTE_USER cannot come from a client header.
- Leaving create_unknown_user at its default only admits existing accounts.