diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index d96300c503..891be59657 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -82,6 +82,7 @@ def authenticate(request=None, **credentials): def _authenticate_with_backend(backend, backend_path, request, credentials): + credentials = credentials.copy() # Prevent a mutation from propagating. args = (request,) # Does the backend accept a request argument? try: diff --git a/docs/releases/1.11.10.txt b/docs/releases/1.11.10.txt index 49e19614a5..88d2d007bf 100644 --- a/docs/releases/1.11.10.txt +++ b/docs/releases/1.11.10.txt @@ -11,3 +11,7 @@ Bugfixes * Fixed incorrect foreign key nullification if a model has two foreign keys to the same model and a target model is deleted (:ticket:`29016`). + +* Fixed a regression where ``contrib.auth.authenticate()`` crashes if an + authentication backend doesn't accept ``request`` and a later one does + (:ticket:`29071`). diff --git a/docs/releases/2.0.2.txt b/docs/releases/2.0.2.txt index 06968d4ba3..000236af2c 100644 --- a/docs/releases/2.0.2.txt +++ b/docs/releases/2.0.2.txt @@ -20,3 +20,7 @@ Bugfixes * Fixed a regression where a queryset that annotates with geometry objects crashes (:ticket:`29054`). + +* Fixed a regression where ``contrib.auth.authenticate()`` crashes if an + authentication backend doesn't accept ``request`` and a later one does + (:ticket:`29071`). diff --git a/tests/auth_tests/test_auth_backends_deprecation.py b/tests/auth_tests/test_auth_backends_deprecation.py index 78a5d8945a..195239b686 100644 --- a/tests/auth_tests/test_auth_backends_deprecation.py +++ b/tests/auth_tests/test_auth_backends_deprecation.py @@ -13,6 +13,18 @@ class NoRequestBackend: pass +class NoRequestWithKwargs: + def authenticate(self, username=None, password=None, **kwargs): + pass + + +class RequestPositionalArg: + def authenticate(self, request, username=None, password=None, **kwargs): + assert username == 'username' + assert password == 'pass' + assert request is mock_request + + class RequestNotPositionArgBackend: def authenticate(self, username=None, password=None, request=None): assert username == 'username' @@ -34,6 +46,8 @@ class AcceptsRequestBackendTest(SimpleTestCase): method without a request parameter. """ no_request_backend = '%s.NoRequestBackend' % __name__ + no_request_with_kwargs_backend = '%s.NoRequestWithKwargs' % __name__ + request_positional_arg_backend = '%s.RequestPositionalArg' % __name__ request_not_positional_backend = '%s.RequestNotPositionArgBackend' % __name__ request_not_positional_with_used_kwarg_backend = '%s.RequestNotPositionArgWithUsedKwargBackend' % __name__ @@ -79,6 +93,21 @@ class AcceptsRequestBackendTest(SimpleTestCase): "argument." % self.no_request_backend ) + @override_settings(AUTHENTICATION_BACKENDS=[no_request_with_kwargs_backend, request_positional_arg_backend]) + def test_credentials_not_mutated(self): + """ + No problem if a backend doesn't accept `request` and a later one does. + """ + with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter('always') + authenticate(mock_request, username='username', password='pass') + self.assertEqual(len(warns), 1) + self.assertEqual( + str(warns[0].message), + "In %s.authenticate(), move the `request` keyword argument to the " + "first positional argument." % self.no_request_with_kwargs_backend + ) + @override_settings(AUTHENTICATION_BACKENDS=[request_not_positional_with_used_kwarg_backend]) def test_handles_backend_in_kwargs(self): with warnings.catch_warnings(record=True) as warns: