REMOTE_USER / Shibboleth SSO login silently fails to pass forward parameters right
Hi, We have a problem in authentication with shibboleth (SSO) and nav, which we would like to know if there is any resolution for. The authenticated user is seen in Apache variables but not passed to nav. This is not a Shibboleth, Apache, or `webfront.conf` configuration problem. It is a code defect probably introduced in 5.17.0 and probably still present, unchanged, in 5.19.0 (latest at time of writing). It might be a known bug (resolved?) but still present. It seems to be some shortcomings in the code in file (`/opt/venvs/nav/lib/python3.13/site-packages/nav/web/auth/backends.py`) ... import logging from django.contrib.auth.backends import RemoteUserBackend from nav.auditlog.models import LogEntry from nav.web.auth import remote_user _logger = logging.getLogger(__name__) class NAVRemoteUserBackend(RemoteUserBackend): "An adaptation of Django's RemoteUserBackend that is configurable the NAV way" def __init__(self): self.create_unknown_user = remote_user.CONFIG.will_autocreate_user() def authenticate(self, request, user): if not remote_user.CONFIG.is_remote_user_enabled(): return None user = super().authenticate(request, user) return user With DEBUG logging enabled for `nav.web.auth, we see that web-server side is fully functional. ... request.META["REMOTE_USER"]: "user@xxx.yyy" [INFO] nav.web.auth.signals] failed login: None REMOTE_USER: "default" from "/navlets/get-user-navlet/12?mode=VIEW" ... `REMOTE_USER` is present and correct in `request.META`, confirming the web-server side is fully functional. The subsequent `failed login: None` and fallback to the `default` (anonymous) account confirm the failure occurs entirely inside NAV's Django authentication backend dispatch. Auto creating accounts does not work, since the remote_user is not passed along right. ## Steps to reproduce 1. Deploy NAV 5.19.0 behind Apache + Shibboleth SP, with `webfront.conf`: ... [remote-user] enabled = yes varname = REMOTE_USER autocreate = yes 2. Log in via the IdP. Shibboleth authenticates successfully and Apache passes `REMOTE_USER` correctly (verified independently via `LogFormat "... REMOTE_USER=%u" ...` / `%{REMOTE_USER}e` in Apache access logs — confirms the header reaches Apache/mod_wsgi). 3. NAV does not log the user in; the session remains bound to the anonymous/`default` account. Is there any suggested solution? Kejvan Redjamand www.chalmers.se<http://www.chalmers.se/>
This is the Environment. May be the code is corrected elsewhere but not I Debian's distribution? | Component | Version | |-----------------------|--------------------------------------------| | NAV | 5.19.0 | | Python | 3.13.5 | | Django | 5.2.15 | | django-allauth | 65.18.0 | | django-htmx | 1.27.0 | | django-filter | 25.2 | | django-tagging | 0.5.0 | | djangorestframework | 3.17.1 | | PostgreSQL | 17.11 (Debian 17.11-0+deb13u1) | | libapache2-mod-wsgi-py3 | 5.0.2-1 | | Shibboleth SP | 3.5.0 | | OS | Debian 13.6 (trixie) | | NAV install method | Debian package into venv (`/opt/venvs/nav`)| Kind Regards, Kejvan ________________________________ From: Kejvan Redjamand <kejvan.redjamand@chalmers.se> Sent: Friday, August 21, 2026 12:44 To: nav-users@lister.sikt.no <nav-users@lister.sikt.no> Subject: REMOTE_USER / Shibboleth SSO login silently fails to pass forward parameters right Hi, We have a problem in authentication with shibboleth (SSO) and nav, which we would like to know if there is any resolution for. The authenticated user is seen in Apache variables but not passed to nav. This is not a Shibboleth, Apache, or `webfront.conf` configuration problem. It is a code defect probably introduced in 5.17.0 and probably still present, unchanged, in 5.19.0 (latest at time of writing). It might be a known bug (resolved?) but still present. It seems to be some shortcomings in the code in file (`/opt/venvs/nav/lib/python3.13/site-packages/nav/web/auth/backends.py`) ... import logging from django.contrib.auth.backends import RemoteUserBackend from nav.auditlog.models import LogEntry from nav.web.auth import remote_user _logger = logging.getLogger(__name__) class NAVRemoteUserBackend(RemoteUserBackend): "An adaptation of Django's RemoteUserBackend that is configurable the NAV way" def __init__(self): self.create_unknown_user = remote_user.CONFIG.will_autocreate_user() def authenticate(self, request, user): if not remote_user.CONFIG.is_remote_user_enabled(): return None user = super().authenticate(request, user) return user With DEBUG logging enabled for `nav.web.auth, we see that web-server side is fully functional. ... request.META["REMOTE_USER"]: "user@xxx.yyy" [INFO] nav.web.auth.signals] failed login: None REMOTE_USER: "default" from "/navlets/get-user-navlet/12?mode=VIEW" ... `REMOTE_USER` is present and correct in `request.META`, confirming the web-server side is fully functional. The subsequent `failed login: None` and fallback to the `default` (anonymous) account confirm the failure occurs entirely inside NAV's Django authentication backend dispatch. Auto creating accounts does not work, since the remote_user is not passed along right. ## Steps to reproduce 1. Deploy NAV 5.19.0 behind Apache + Shibboleth SP, with `webfront.conf`: ... [remote-user] enabled = yes varname = REMOTE_USER autocreate = yes 2. Log in via the IdP. Shibboleth authenticates successfully and Apache passes `REMOTE_USER` correctly (verified independently via `LogFormat "... REMOTE_USER=%u" ...` / `%{REMOTE_USER}e` in Apache access logs — confirms the header reaches Apache/mod_wsgi). 3. NAV does not log the user in; the session remains bound to the anonymous/`default` account. Is there any suggested solution? Kejvan Redjamand www.chalmers.se<http://www.chalmers.se/>
Hi, We have made a little fix for the problem. Now another little finding. This will probably be a small fix, but it creates some problems among users. False "insecure/plaintext password" warning for newly created remote user accounts: After fixing the login issue, newly created (authenticated) REMOTE_USER accounts see a banner: "Your account has an insecure or old password. It should be reset." The same false positive also appears in the "User and API Administration" tool: "This account's password is stored in plain text. Its password should be changed immediately, or the account disabled." Attempting to act on either warning leads to the change-password page, which correctly refuses: "You are not allowed to change your password from this page. Your account is managed through an external system, known as 'REMOTE_USER'." — i.e. two parts of the UI give contradictory guidance for the same account. The problem probably is that the REMOTE_USER accounts are now in 5.1 9created via Django. In the database, for the accounts created, the password column is left as an empty string, not a secret. (We see old remote accounts, created with NAV (not Diango) have a dummy secret. You see: login | password | pw_len | ext_sync ... user-old-by-nav@xyz | {pbkdf2}xxxxxxxxxxxxxxxxxxxxxxxx | 65 | REMOTE_USER ... user-new-by-diango@xyz | | 0 | REMOTE_USER It seems that, some check fails for an empty string, and it is reported as "plaintext" instead of something else: no local password or externally managed password or similar. To correct the check may probably be a simple fix, hopefully. Med vänliga hälsningar Kejvan ________________________________ From: Kejvan Redjamand <kejvan@chalmers.se> Sent: Friday, August 21, 2026 12:52 To: nav-users@lister.sikt.no <nav-users@lister.sikt.no> Subject: [Nav-users] Re: REMOTE_USER / Shibboleth SSO login silently fails to pass forward parameters right This is the Environment. May be the code is corrected elsewhere but not I Debian's distribution? | Component | Version | |-----------------------|--------------------------------------------| | NAV | 5.19.0 | | Python | 3.13.5 | | Django | 5.2.15 | | django-allauth | 65.18.0 | | django-htmx | 1.27.0 | | django-filter | 25.2 | | django-tagging | 0.5.0 | | djangorestframework | 3.17.1 | | PostgreSQL | 17.11 (Debian 17.11-0+deb13u1) | | libapache2-mod-wsgi-py3 | 5.0.2-1 | | Shibboleth SP | 3.5.0 | | OS | Debian 13.6 (trixie) | | NAV install method | Debian package into venv (`/opt/venvs/nav`)| Kind Regards, Kejvan ________________________________ From: Kejvan Redjamand <kejvan.redjamand@chalmers.se> Sent: Friday, August 21, 2026 12:44 To: nav-users@lister.sikt.no <nav-users@lister.sikt.no> Subject: REMOTE_USER / Shibboleth SSO login silently fails to pass forward parameters right Hi, We have a problem in authentication with shibboleth (SSO) and nav, which we would like to know if there is any resolution for. The authenticated user is seen in Apache variables but not passed to nav. This is not a Shibboleth, Apache, or `webfront.conf` configuration problem. It is a code defect probably introduced in 5.17.0 and probably still present, unchanged, in 5.19.0 (latest at time of writing). It might be a known bug (resolved?) but still present. It seems to be some shortcomings in the code in file (`/opt/venvs/nav/lib/python3.13/site-packages/nav/web/auth/backends.py`) ... import logging from django.contrib.auth.backends import RemoteUserBackend from nav.auditlog.models import LogEntry from nav.web.auth import remote_user _logger = logging.getLogger(__name__) class NAVRemoteUserBackend(RemoteUserBackend): "An adaptation of Django's RemoteUserBackend that is configurable the NAV way" def __init__(self): self.create_unknown_user = remote_user.CONFIG.will_autocreate_user() def authenticate(self, request, user): if not remote_user.CONFIG.is_remote_user_enabled(): return None user = super().authenticate(request, user) return user With DEBUG logging enabled for `nav.web.auth, we see that web-server side is fully functional. ... request.META["REMOTE_USER"]: "user@xxx.yyy" [INFO] nav.web.auth.signals] failed login: None REMOTE_USER: "default" from "/navlets/get-user-navlet/12?mode=VIEW" ... `REMOTE_USER` is present and correct in `request.META`, confirming the web-server side is fully functional. The subsequent `failed login: None` and fallback to the `default` (anonymous) account confirm the failure occurs entirely inside NAV's Django authentication backend dispatch. Auto creating accounts does not work, since the remote_user is not passed along right. ## Steps to reproduce 1. Deploy NAV 5.19.0 behind Apache + Shibboleth SP, with `webfront.conf`: ... [remote-user] enabled = yes varname = REMOTE_USER autocreate = yes 2. Log in via the IdP. Shibboleth authenticates successfully and Apache passes `REMOTE_USER` correctly (verified independently via `LogFormat "... REMOTE_USER=%u" ...` / `%{REMOTE_USER}e` in Apache access logs — confirms the header reaches Apache/mod_wsgi). 3. NAV does not log the user in; the session remains bound to the anonymous/`default` account. Is there any suggested solution? Kejvan Redjamand www.chalmers.se<http://www.chalmers.se/>
On Fri 21 Aug 2026 at 10:44, Kejvan Redjamand <kejvan.redjamand@chalmers.se> wrote:
Hi,
We have a problem in authentication with shibboleth (SSO) and nav, which we would like to know if there is any resolution for.
Hi Kejvan, Thanks for the unusually well-researched report. Your diagnosis is correct, and you pointed at exactly the right method. This is a genuine regression in NAV, not a configuration problem on your end. We knew the rewrite of the auth system in 5.17 could be a problem, particularly for the REMOTE_USER backend, which is why we took particular care to note the need for proper testing in the 5.17.0 changelog.
It is a code defect probably introduced in 5.17.0 and probably still present, unchanged, in 5.19.0 (latest at time of writing).
Confirmed on both counts.
It seems to be some shortcomings in the code in file (`/opt/venvs/nav/lib/python3.13/site-packages/nav/web/auth/backends.py`)
def authenticate(self, request, user):
That signature is likely the entire bug. Django's `RemoteUserMiddleware` calls the backend like this: user = auth.authenticate(request, remote_user=username) and `django.contrib.auth.authenticate()` does not simply call every backend in turn - it first binds the credential keyword arguments against each backend's signature, and silently skips any backend that doesn't accept them: backend_signature = signature(backend.authenticate) try: backend_signature.bind(request, **credentials) except TypeError: # This backend doesn't accept these credentials as arguments. # Try the next one. continue Since `NAVRemoteUserBackend.authenticate()` names its parameter `user` and not `remote_user`, binding raises `TypeError`, and the backend is never called at all. Nothing in the class body ever runs - which is why no amount of `REMOTE_USER`, varname or autocreate configuration makes any difference.
... request.META["REMOTE_USER"]: "user@xxx.yyy" [INFO] nav.web.auth.signals] failed login: None
That log line is the smoking gun, and it also explains the `None`. Once `NAVRemoteUserBackend` is skipped, the credentials fall through to the remaining backends (`ModelBackend`, NAV's LDAP backend, allauth). Those all accept `**kwargs`, so they *do* bind, get handed no username and no password, and duly return `None`. Django then fires `user_login_failed`, and NAV's log receiver looks for the credential under the keys "username", "login" or "user" - but the actual key is "remote_user", so it finds nothing and logs `None`. Two symptoms, one root cause.
Auto creating accounts does not work, since the remote_user is not passed along right.
Right - account autocreation lives inside the backend that never gets invoked, so it can't fire. Same for the audit log entry that would normally record the account creation.
Is there any suggested solution?
Essentially a one-word change. If you need SSO working before the next release, you can patch your installation: --- a/nav/web/auth/backends.py +++ b/nav/web/auth/backends.py @@ from nav.web.auth import remote_user +from nav.web.auth.remote_user import CONFIG @@ - def authenticate(self, request, user): - if not remote_user.CONFIG.is_remote_user_enabled(): + def authenticate(self, request, remote_user): + if not CONFIG.is_remote_user_enabled(): return None - - user = super().authenticate(request, user) - return user + return super().authenticate(request, remote_user) The extra import is not gratuitous: naming the parameter `remote_user` shadows the imported `nav.web.auth.remote_user` module inside the method body, so `remote_user.CONFIG` on the next line would blow up. Renaming the parameter alone is not enough. The packaged fix will also correct the "failed login: None" log line, and add a regression test that goes through `django.contrib.auth.authenticate()` rather than calling the backend directly - calling it directly, with positional arguments, is precisely why our existing unit tests never caught this. I've filed this as https://github.com/Uninett/nav/issues/4168 so you can follow along. We'll target a 5.19.x patch release. Thanks again for taking the time to dig into the code and to verify the Apache side independently - it made this a five-minute diagnosis instead of a long back-and-forth. -- Sincerely, Morten Brekkevold Sikt – Norwegian Agency for Shared Services in Education and Research
Hi, We have made a little fix for that problem (user/user_name, authentication) and wait for new release. Now about another little finding. This will probably be a small fix, but right now it creates some concerns/problems among users. False "insecure/plaintext password" warning for newly created remote user accounts: After fixing the login issue, newly created (authenticated) REMOTE_USER accounts see a banner: "Your account has an insecure or old password. It should be reset." The same false positive also appears in the "User and API Administration" tool: "This account's password is stored in plain text. Its password should be changed immediately, or the account disabled." Attempting to act on either warning leads to the change-password page, which correctly refuses: "You are not allowed to change your password from this page. Your account is managed through an external system, known as 'REMOTE_USER'." — i.e. two parts of the UI give contradictory guidance for the same account. The problem probably is that the REMOTE_USER accounts are now in 5.1 9created via Django. In the database, for the accounts created, the password column is left as an empty string, not a secret. (We see old remote accounts, created with NAV (not Diango) have a dummy secret. You see: login | password | pw_len | ext_sync ... user-old-by-nav@xyz | {pbkdf2}xxxxxxxxxxxxxxxxxxxxxxxx | 65 | REMOTE_USER ... user-new-by-diango@xyz | | 0 | REMOTE_USER It seems that, some check fails for an empty string, and it is reported as "plaintext" instead of something else: no local password or externally managed password or similar. To correct the check may probably be a simple fix, hopefully. / BR Kejvan ________________________________ From: Morten Brekkevold <morten.brekkevold@sikt.no> Sent: Thursday, August 27, 2026 11:10 To: Kejvan Redjamand <kejvan.redjamand@chalmers.se> Cc: nav-users@lister.sikt.no <nav-users@lister.sikt.no> Subject: Re: [Nav-users] REMOTE_USER / Shibboleth SSO login silently fails to pass forward parameters right On Fri 21 Aug 2026 at 10:44, Kejvan Redjamand <kejvan.redjamand@chalmers.se> wrote:
Hi,
We have a problem in authentication with shibboleth (SSO) and nav, which we would like to know if there is any resolution for.
Hi Kejvan, Thanks for the unusually well-researched report. Your diagnosis is correct, and you pointed at exactly the right method. This is a genuine regression in NAV, not a configuration problem on your end. We knew the rewrite of the auth system in 5.17 could be a problem, particularly for the REMOTE_USER backend, which is why we took particular care to note the need for proper testing in the 5.17.0 changelog.
It is a code defect probably introduced in 5.17.0 and probably still present, unchanged, in 5.19.0 (latest at time of writing).
Confirmed on both counts.
It seems to be some shortcomings in the code in file (`/opt/venvs/nav/lib/python3.13/site-packages/nav/web/auth/backends.py`)
def authenticate(self, request, user):
That signature is likely the entire bug. Django's `RemoteUserMiddleware` calls the backend like this: user = auth.authenticate(request, remote_user=username) and `django.contrib.auth.authenticate()` does not simply call every backend in turn - it first binds the credential keyword arguments against each backend's signature, and silently skips any backend that doesn't accept them: backend_signature = signature(backend.authenticate) try: backend_signature.bind(request, **credentials) except TypeError: # This backend doesn't accept these credentials as arguments. # Try the next one. continue Since `NAVRemoteUserBackend.authenticate()` names its parameter `user` and not `remote_user`, binding raises `TypeError`, and the backend is never called at all. Nothing in the class body ever runs - which is why no amount of `REMOTE_USER`, varname or autocreate configuration makes any difference.
... request.META["REMOTE_USER"]: "user@xxx.yyy" [INFO] nav.web.auth.signals] failed login: None
That log line is the smoking gun, and it also explains the `None`. Once `NAVRemoteUserBackend` is skipped, the credentials fall through to the remaining backends (`ModelBackend`, NAV's LDAP backend, allauth). Those all accept `**kwargs`, so they *do* bind, get handed no username and no password, and duly return `None`. Django then fires `user_login_failed`, and NAV's log receiver looks for the credential under the keys "username", "login" or "user" - but the actual key is "remote_user", so it finds nothing and logs `None`. Two symptoms, one root cause.
Auto creating accounts does not work, since the remote_user is not passed along right.
Right - account autocreation lives inside the backend that never gets invoked, so it can't fire. Same for the audit log entry that would normally record the account creation.
Is there any suggested solution?
Essentially a one-word change. If you need SSO working before the next release, you can patch your installation: --- a/nav/web/auth/backends.py +++ b/nav/web/auth/backends.py @@ from nav.web.auth import remote_user +from nav.web.auth.remote_user import CONFIG @@ - def authenticate(self, request, user): - if not remote_user.CONFIG.is_remote_user_enabled(): + def authenticate(self, request, remote_user): + if not CONFIG.is_remote_user_enabled(): return None - - user = super().authenticate(request, user) - return user + return super().authenticate(request, remote_user) The extra import is not gratuitous: naming the parameter `remote_user` shadows the imported `nav.web.auth.remote_user` module inside the method body, so `remote_user.CONFIG` on the next line would blow up. Renaming the parameter alone is not enough. The packaged fix will also correct the "failed login: None" log line, and add a regression test that goes through `django.contrib.auth.authenticate()` rather than calling the backend directly - calling it directly, with positional arguments, is precisely why our existing unit tests never caught this. I've filed this as https://github.com/Uninett/nav/issues/4168 so you can follow along. We'll target a 5.19.x patch release. Thanks again for taking the time to dig into the code and to verify the Apache side independently - it made this a five-minute diagnosis instead of a long back-and-forth. -- Sincerely, Morten Brekkevold Sikt – Norwegian Agency for Shared Services in Education and Research
On Thu 27 Aug 2026 at 14:04, Kejvan Redjamand <kejvan@chalmers.se> wrote:
False "insecure/plaintext password" warning for newly created remote user accounts: ... It seems that, some check fails for an empty string, and it is reported as "plaintext" instead of something else: no local password or externally managed password or similar.
To correct the check may probably be a simple fix, hopefully.
Thanks for another good report, Kejvan. The insecure password hash verification does indeed seem a bit too eager: It wasn't built for the case where a user account has no local password hash stored at all (i.e. just a blank string). I believe there is already an open pull request to fix this now: https://github.com/Uninett/nav/pull/4172 -- Sincerely, Morten Brekkevold Sikt – Norwegian Agency for Shared Services in Education and Research
participants (3)
-
Kejvan Redjamand -
Kejvan Redjamand -
Morten Brekkevold