Skip to content

Commit

Permalink
Updated RemoteUser middleware to use native async logic (needs tests)
Browse files Browse the repository at this point in the history
  • Loading branch information
bigfootjon committed Mar 31, 2024
1 parent 75702ac commit 2c59d74
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 9 deletions.
78 changes: 74 additions & 4 deletions django/contrib/auth/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def process_request(self, request):
request.auser = partial(auser, request)


class RemoteUserMiddleware(MiddlewareMixin):
class RemoteUserMiddleware:
"""
Middleware for utilizing web-server-provided authentication.
Expand All @@ -48,13 +48,22 @@ class RemoteUserMiddleware(MiddlewareMixin):
different header.
"""

sync_capable = True
async_capable = True

def __init__(self, get_response):
if get_response is None:
raise ValueError("get_response must be provided.")
self.get_response = get_response
super().__init__()

# Name of request header to grab username from. This will be the key as
# used in the request.META dictionary, i.e. the normalization of headers to
# all uppercase and the addition of "HTTP_" prefix apply.
header = "REMOTE_USER"
force_logout_if_no_header = True

def process_request(self, request):
def __call__(self, request):
# AuthenticationMiddleware is required so that request.user exists.
if not hasattr(request, "user"):
raise ImproperlyConfigured(
Expand All @@ -72,13 +81,13 @@ def process_request(self, request):
# AnonymousUser by the AuthenticationMiddleware).
if self.force_logout_if_no_header and request.user.is_authenticated:
self._remove_invalid_user(request)
return
return self.get_response(request)
# If the user is already authenticated and that user is the user we are
# getting passed in the headers, then the correct user is already
# persisted in the session and we don't need to continue.
if request.user.is_authenticated:
if request.user.get_username() == self.clean_username(username, request):
return
return self.get_response(request)
else:
# An authenticated user is associated with the request, but
# it does not match the authorized user in the header.
Expand All @@ -92,6 +101,51 @@ def process_request(self, request):
# by logging the user in.
request.user = user
auth.login(request, user)
return self.get_response(request)

async def __acall__(self, request):
# AuthenticationMiddleware is required so that request.user exists.
if not hasattr(request, "user"):
raise ImproperlyConfigured(
"The Django remote user auth middleware requires the"
" authentication middleware to be installed. Edit your"
" MIDDLEWARE setting to insert"
" 'django.contrib.auth.middleware.AuthenticationMiddleware'"
" before the RemoteUserMiddleware class."
)
try:
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
# AnonymousUser by the AuthenticationMiddleware).
if self.force_logout_if_no_header:
user = await request.auser()
if user.is_authenticated:
await self._aremove_invalid_user(request)
return await self.get_response(request)
user = await request.auser()
# If the user is already authenticated and that user is the user we are
# getting passed in the headers, then the correct user is already
# persisted in the session and we don't need to continue.
if user.is_authenticated:
if user.get_username() == self.clean_username(username, request):
return await self.get_response(request)
else:
# An authenticated user is associated with the request, but
# it does not match the authorized user in the header.
await self._aremove_invalid_user(request)

# We are seeing this user for the first time in this session, attempt
# to authenticate the user.
user = await auth.aauthenticate(request, remote_user=username)
if user:
# User is valid. Set request.user and persist user in the session
# by logging the user in.
request.user = user
await auth.alogin(request, user)

return await self.get_response(request)

def clean_username(self, username, request):
"""
Expand Down Expand Up @@ -122,6 +176,22 @@ def _remove_invalid_user(self, request):
if isinstance(stored_backend, RemoteUserBackend):
auth.logout(request)

async def _aremove_invalid_user(self, request):
"""
Remove the current authenticated user in the request which is invalid
but only if the user is authenticated via the RemoteUserBackend.
"""
try:
stored_backend = load_backend(
await request.session.aget(auth.BACKEND_SESSION_KEY, "")
)
except ImportError:
# backend failed to load
await auth.alogout(request)
else:
if isinstance(stored_backend, RemoteUserBackend):
await auth.alogout(request)


class PersistentRemoteUserMiddleware(RemoteUserMiddleware):
"""
Expand Down
5 changes: 5 additions & 0 deletions tests/async/test_async_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ async def test_alogin(self):
async def test_alogin_without_user(self):
request = HttpRequest()
request.user = self.test_user

async def auser():
return self.test_user

request.auser = auser
request.session = await self.client.asession()
await alogin(request, None)
user = await aget_user(request)
Expand Down
4 changes: 4 additions & 0 deletions tests/auth_tests/test_auth_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,10 @@ class TypeErrorBackend:
def authenticate(self, request, username=None, password=None):
raise TypeError

@sensitive_variables("password")
async def aauthenticate(self, request, username=None, password=None):
raise TypeError


class SkippedBackend:
def authenticate(self):
Expand Down
6 changes: 1 addition & 5 deletions tests/deprecation/test_middleware_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
from asgiref.sync import async_to_sync, iscoroutinefunction

from django.contrib.admindocs.middleware import XViewMiddleware
from django.contrib.auth.middleware import (
AuthenticationMiddleware,
RemoteUserMiddleware,
)
from django.contrib.auth.middleware import AuthenticationMiddleware
from django.contrib.flatpages.middleware import FlatpageFallbackMiddleware
from django.contrib.messages.middleware import MessageMiddleware
from django.contrib.redirects.middleware import RedirectFallbackMiddleware
Expand Down Expand Up @@ -46,7 +43,6 @@ class MiddlewareMixinTests(SimpleTestCase):
LocaleMiddleware,
MessageMiddleware,
RedirectFallbackMiddleware,
RemoteUserMiddleware,
SecurityMiddleware,
SessionMiddleware,
UpdateCacheMiddleware,
Expand Down

0 comments on commit 2c59d74

Please sign in to comment.