#37025: Deprecate the prefixing of HTTP_ to header names in
RemoteUserMiddleware.aprocess_request()
------------------------------+---------------------------------------
     Reporter:  Jacob Walls   |                    Owner:  Jacob Walls
         Type:  Bug           |                   Status:  assigned
    Component:  contrib.auth  |                  Version:  5.2
     Severity:  Normal        |               Resolution:
     Keywords:                |             Triage Stage:  Unreviewed
    Has patch:  0             |      Needs documentation:  0
  Needs tests:  0             |  Patch needs improvement:  0
Easy pickings:  0             |                    UI/UX:  0
------------------------------+---------------------------------------
Comment (by Jacob Walls):

 Thanks both. So the original proposal definitely involves breaking
 RemoteUserMiddleware + ASGI + default configuration, on the theory that if
 almost no one is using this, it's OK to make this "broken by default" ,
 because "broken by default" = "secure by default". But the `header_prefix`
 idea is interesting!

 That said, the recent doc edits are sufficient, so my motivation here was
 less about security and more "easier to reason about", given that if we
 lean toward Sarah's proposed doc edit in comment:6, then I wonder if we're
 doubling down on documenting a bug.

 ----
 To Sarah's question in comment:3, to get the tests working, we would need
 to adjust how we parameterize the header names in the tests. I recently
 learned that the sync request factory expects prefixed header names, e.g.
 `"HTTP_MYHEADER"`, but the async request factory expects unprefixed header
 names, e.g. `"MYHEADER"`, since they go through `ASGIRequest`, which
 applies prefixing.

 {{{#!diff
 diff --git a/django/contrib/auth/middleware.py
 b/django/contrib/auth/middleware.py
 index a28e1705a3..7136a64c44 100644
 --- a/django/contrib/auth/middleware.py
 +++ b/django/contrib/auth/middleware.py
 @@ -184,7 +184,7 @@ class RemoteUserMiddleware:
                  " before the RemoteUserMiddleware class."
              )
          try:
 -            username = request.META["HTTP_" + self.header]
 +            username = request.META[self.header]
          except KeyError:
              # If specified header doesn't exist then remove any existing
              # authenticated remote-user, or return (leaving request.user
 set to
 diff --git a/tests/auth_tests/test_remote_user.py
 b/tests/auth_tests/test_remote_user.py
 index 4a97fc2120..6745009589 100644
 --- a/tests/auth_tests/test_remote_user.py
 +++ b/tests/auth_tests/test_remote_user.py
 @@ -3,8 +3,9 @@ from datetime import UTC, datetime
  from django.conf import settings
  from django.contrib.auth import aauthenticate, authenticate
  from django.contrib.auth.backends import RemoteUserBackend
 -from django.contrib.auth.middleware import RemoteUserMiddleware
 +from django.contrib.auth.middleware import
 PersistentRemoteUserMiddleware, RemoteUserMiddleware
  from django.contrib.auth.models import User
 +from django.http.request import HttpHeaders
  from django.middleware.csrf import _get_new_csrf_string,
 _mask_cipher_secret
  from django.test import (
      AsyncClient,
 @@ -17,15 +18,24 @@ from django.test import (

  @override_settings(ROOT_URLCONF="auth_tests.urls")
  class RemoteUserTest(TestCase):
 -    middleware = "django.contrib.auth.middleware.RemoteUserMiddleware"
 +    # Use a custom middleware if we want this test case to run on both
 WSGI & ASGI.
 +    middleware = "auth_tests.test_remote_user.CustomHeaderMiddleware"
      backend = "django.contrib.auth.backends.RemoteUserBackend"
 -    header = "REMOTE_USER"
 +    header = "HTTP_AUTHUSER"
      email_header = "REMOTE_EMAIL"

      # Usernames to be passed in REMOTE_USER for the test_known_user test
 case.
      known_user = "knownuser"
      known_user2 = "knownuser2"

 +    @property
 +    def async_header(self):
 +        if self.header.startswith(HttpHeaders.HTTP_PREFIX):
 +            # The tests like to parameterize headers, but if the defined
 header
 +            # is already prefixed, strip it before using it.
 +            return self.header.lstrip(HttpHeaders.HTTP_PREFIX)
 +        return self.header
 +
      @classmethod
      def setUpClass(cls):
          cls.enterClassContext(
 @@ -65,7 +75,7 @@ class RemoteUserTest(TestCase):
          self.assertTrue(response.context["user"].is_anonymous)
          self.assertEqual(await User.objects.acount(), num_users)

 -        response = await self.async_client.get("/remote_user/",
 **{self.header: ""})
 +        response = await self.async_client.get("/remote_user/",
 **{self.async_header: ""})
          self.assertTrue(response.context["user"].is_anonymous)
          self.assertEqual(await User.objects.acount(), num_users)

 @@ -142,7 +152,7 @@ class RemoteUserTest(TestCase):
          """See test_unknown_user."""
          num_users = await User.objects.acount()
          response = await self.async_client.get(
 -            "/remote_user/", **{self.header: "newuser"}
 +            "/remote_user/", **{self.async_header: "newuser"}
          )
          self.assertEqual(response.context["user"].username, "newuser")
          self.assertEqual(await User.objects.acount(), num_users + 1)
 @@ -150,7 +160,7 @@ class RemoteUserTest(TestCase):

          # Another request with same user should not create any new users.
          response = await self.async_client.get(
 -            "/remote_user/", **{self.header: "newuser"}
 +            "/remote_user/", **{self.async_header: "newuser"}
          )
          self.assertEqual(await User.objects.acount(), num_users + 1)

 @@ -176,14 +186,14 @@ class RemoteUserTest(TestCase):
          await User.objects.acreate(username="knownuser2")
          num_users = await User.objects.acount()
          response = await self.async_client.get(
 -            "/remote_user/", **{self.header: self.known_user}
 +            "/remote_user/", **{self.async_header: self.known_user}
          )
          self.assertEqual(response.context["user"].username, "knownuser")
          self.assertEqual(await User.objects.acount(), num_users)
          # A different user passed in the headers causes the new user
          # to be logged in.
          response = await self.async_client.get(
 -            "/remote_user/", **{self.header: self.known_user2}
 +            "/remote_user/", **{self.async_header: self.known_user2}
          )
          self.assertEqual(response.context["user"].username, "knownuser2")
          self.assertEqual(await User.objects.acount(), num_users)
 @@ -221,7 +231,7 @@ class RemoteUserTest(TestCase):
          await user.asave()

          response = await self.async_client.get(
 -            "/remote_user/", **{self.header: self.known_user}
 +            "/remote_user/", **{self.async_header: self.known_user}
          )
          self.assertNotEqual(default_login,
 response.context["user"].last_login)

 @@ -229,7 +239,7 @@ class RemoteUserTest(TestCase):
          user.last_login = default_login
          await user.asave()
          response = await self.async_client.get(
 -            "/remote_user/", **{self.header: self.known_user}
 +            "/remote_user/", **{self.async_header: self.known_user}
          )
          self.assertEqual(default_login,
 response.context["user"].last_login)

 @@ -259,7 +269,7 @@ class RemoteUserTest(TestCase):
          await User.objects.acreate(username="knownuser")
          # Known user authenticates
          response = await self.async_client.get(
 -            "/remote_user/", **{self.header: self.known_user}
 +            "/remote_user/", **{self.async_header: self.known_user}
          )
          self.assertEqual(response.context["user"].username, "knownuser")
          # During the session, the REMOTE_USER header disappears. Should
 trigger
 @@ -295,12 +305,12 @@ class RemoteUserTest(TestCase):
          await User.objects.acreate(username="knownuser")
          # Known user authenticates
          response = await self.async_client.get(
 -            "/remote_user/", **{self.header: self.known_user}
 +            "/remote_user/", **{self.async_header: self.known_user}
          )
          self.assertEqual(response.context["user"].username, "knownuser")
          # During the session, the REMOTE_USER changes to a different
 user.
          response = await self.async_client.get(
 -            "/remote_user/", **{self.header: "newnewuser"}
 +            "/remote_user/", **{self.async_header: "newnewuser"}
          )
          # The current user is not the prior remote_user.
          # In backends that create a new user, username is "newnewuser"
 @@ -315,7 +325,7 @@ class RemoteUserTest(TestCase):
      async def test_inactive_user_async(self):
          await User.objects.acreate(username="knownuser", is_active=False)
          response = await self.async_client.get(
 -            "/remote_user/", **{self.header: "knownuser"}
 +            "/remote_user/", **{self.async_header: "knownuser"}
          )
          self.assertTrue(response.context["user"].is_anonymous)

 @@ -343,7 +353,7 @@ class RemoteUserNoCreateTest(RemoteUserTest):
      async def test_unknown_user_async(self):
          num_users = await User.objects.acount()
          response = await self.async_client.get(
 -            "/remote_user/", **{self.header: "newuser"}
 +            "/remote_user/", **{self.async_header: "newuser"}
          )
          self.assertTrue(response.context["user"].is_anonymous)
          self.assertEqual(await User.objects.acount(), num_users)
 @@ -362,7 +372,7 @@ class
 AllowAllUsersRemoteUserBackendTest(RemoteUserTest):
      async def test_inactive_user_async(self):
          user = await User.objects.acreate(username="knownuser",
 is_active=False)
          response = await self.async_client.get(
 -            "/remote_user/", **{self.header: self.known_user}
 +            "/remote_user/", **{self.async_header: self.known_user}
          )
          self.assertEqual(response.context["user"].username,
 user.username)

 @@ -436,6 +446,10 @@ class RemoteUserCustomTest(RemoteUserTest):
          self.assertEqual(newuser.email, "[email protected]")


 +"""
 +These tests are now duplicative in the current demonstration. We wouldn't
 merge
 +it in this state.
 +"""
  class CustomHeaderMiddleware(RemoteUserMiddleware):
      """
      Middleware that overrides custom HTTP auth user header.
 @@ -454,13 +468,17 @@ class CustomHeaderRemoteUserTest(RemoteUserTest):
      header = "HTTP_AUTHUSER"


 +class
 CustomPersistentRemoteUserMiddleware(PersistentRemoteUserMiddleware):
 +    header = "HTTP_AUTHUSER"
 +
 +
  class PersistentRemoteUserTest(RemoteUserTest):
      """
      PersistentRemoteUserMiddleware keeps the user logged in even if the
      subsequent calls do not contain the header value.
      """

 -    middleware =
 "django.contrib.auth.middleware.PersistentRemoteUserMiddleware"
 +    middleware =
 "auth_tests.test_remote_user.CustomPersistentRemoteUserMiddleware"
      require_header = False

      def test_header_disappears(self):
 @@ -482,7 +500,7 @@ class PersistentRemoteUserTest(RemoteUserTest):
          await User.objects.acreate(username="knownuser")
          # Known user authenticates
          response = await self.async_client.get(
 -            "/remote_user/", **{self.header: self.known_user}
 +            "/remote_user/", **{self.async_header: self.known_user}
          )
          self.assertEqual(response.context["user"].username, "knownuser")
          # Should stay logged in if the REMOTE_USER header disappears.

 }}}

 I had to be aware of that in caf90a971f09323775ed0cacf94eadaf39d040e0,
 where I added a little change in the `AsyncRequestFactory` to smooth out
 this difference with respect to underscores. It's possible we could
 deprecate and undo that going forward, if we want to standardize how
 headers are provided in tests? But that would be more disruptive than what
 I'm proposing here.

 ----
 I'll definitely support wontfix'ing this if there is no value, but
 something seems off.  I wonder if the quirks of `AsyncRequestFactory` put
 a little design pressure on the decision to have
 `RemoteUserMiddleware.aprocess_request` apply prefixing?
-- 
Ticket URL: <https://code.djangoproject.com/ticket/37025#comment:7>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/django-updates/0107019d72b7e61c-b467e250-73c1-4ea0-b8aa-9e18255e7be0-000000%40eu-central-1.amazonses.com.

Reply via email to