From b6433866682baac35a953e59298dff7f399ac49b Mon Sep 17 00:00:00 2001 From: Paulo Poiati Date: Sun, 5 Jul 2015 17:54:25 -0300 Subject: [PATCH] Fixed #24855 -- Allowed using contrib.auth.login() without credentials. Added an optional `backend` argument to login(). --- AUTHORS | 1 + django/contrib/auth/__init__.py | 18 +++++++++-- django/test/client.py | 9 ++---- docs/releases/1.10.txt | 3 ++ docs/topics/auth/default.txt | 41 +++++++++++++++++------- tests/auth_tests/test_auth_backends.py | 43 ++++++++++++++++++++++++++ tests/test_client/tests.py | 1 - 7 files changed, 95 insertions(+), 21 deletions(-) diff --git a/AUTHORS b/AUTHORS index eeeca06e52c..b391c00eb72 100644 --- a/AUTHORS +++ b/AUTHORS @@ -567,6 +567,7 @@ answer newbie questions, and generally made Django that much better: Paul Lanier Paul McLanahan Paul McMillan + Paulo Poiati Paulo Scardine Paul Smith pavithran s diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index 7e663f545bd..63d59398f57 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -86,7 +86,7 @@ def authenticate(**credentials): credentials=_clean_credentials(credentials)) -def login(request, user): +def login(request, user, backend=None): """ Persist a user id and a backend in the request. This way a user doesn't have to reauthenticate on every request. Note that data set during @@ -108,8 +108,22 @@ def login(request, user): request.session.flush() else: request.session.cycle_key() + + try: + backend = backend or user.backend + except AttributeError: + backends = _get_backends(return_tuples=True) + if len(backends) == 1: + _, backend = backends[0] + else: + raise ValueError( + 'You have multiple authentication backends configured and ' + 'therefore must provide the `backend` argument or set the ' + '`backend` attribute on the user.' + ) + request.session[SESSION_KEY] = user._meta.pk.value_to_string(user) - request.session[BACKEND_SESSION_KEY] = user.backend + request.session[BACKEND_SESSION_KEY] = backend request.session[HASH_SESSION_KEY] = session_auth_hash if hasattr(request, 'user'): request.user = user diff --git a/django/test/client.py b/django/test/client.py index bd926645487..78a5df5c528 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -603,12 +603,9 @@ class Client(RequestFactory): return False def force_login(self, user, backend=None): - if backend is None: - backend = settings.AUTHENTICATION_BACKENDS[0] - user.backend = backend - self._login(user) + self._login(user, backend) - def _login(self, user): + def _login(self, user, backend=None): from django.contrib.auth import login engine = import_module(settings.SESSION_ENGINE) @@ -619,7 +616,7 @@ class Client(RequestFactory): request.session = self.session else: request.session = engine.SessionStore() - login(request, user) + login(request, user, backend) # Save the session values. request.session.save() diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt index 0eec66faeef..64a755bdfb4 100644 --- a/docs/releases/1.10.txt +++ b/docs/releases/1.10.txt @@ -68,6 +68,9 @@ Minor features to prevent an issue where Safari caches redirects and prevents a user from being able to log out. +* Added the optional ``backend`` argument to :func:`~django.contrib.auth.login` + to allowing using it without credentials. + :mod:`django.contrib.contenttypes` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index 046b7731c01..e0740a201d6 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -322,7 +322,7 @@ How to log a user in If you have an authenticated user you want to attach to the current session - this is done with a :func:`~django.contrib.auth.login` function. -.. function:: login(request, user) +.. function:: login(request, user, backend=None) To log a user in, from a view, use :func:`~django.contrib.auth.login()`. It takes an :class:`~django.http.HttpRequest` object and a @@ -354,18 +354,35 @@ If you have an authenticated user you want to attach to the current session # Return an 'invalid login' error message. ... -.. admonition:: Calling ``authenticate()`` first + .. versionchanged:: 1.10 - When you're manually logging a user in, you *must* successfully authenticate - the user with :func:`~django.contrib.auth.authenticate()` before you call - :func:`~django.contrib.auth.login()`. - :func:`~django.contrib.auth.authenticate()` - sets an attribute on the :class:`~django.contrib.auth.models.User` noting - which authentication backend successfully authenticated that user (see the - :ref:`backends documentation ` for details), and - this information is needed later during the login process. An error will be - raised if you try to login a user object retrieved from the database - directly. + In older versions, when you're manually logging a user in, you *must* + successfully authenticate the user with + :func:`~django.contrib.auth.authenticate()` before you call + :func:`~django.contrib.auth.login()`. Now you can set the backend using + the new ``backend`` argument. + +Selecting the :ref:`authentication backend ` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When a user logs in, the user's ID and the backend that was used for +authentication are saved in the user's session. This allows the same +authentication backend to fetch the user's details on a future request. The +authentication backend to save in the session is selected as follows: + +#. Use the value of the optional ``backend`` argument, if provided. +#. Use the value of the ``user.backend`` attribute, if present. This allows + pairing :func:`~django.contrib.auth.authenticate()` and + :func:`~django.contrib.auth.login()`: + :func:`~django.contrib.auth.authenticate()` + sets the ``user.backend`` attribute on the ``User`` object it returns. +#. Use the ``backend`` in :setting:`AUTHENTICATION_BACKENDS`, if there is only + one. +#. Otherwise, raise an exception. + +In cases 1 and 2, the value of the ``backend`` argument or the ``user.backend`` +attribute should be a dotted import path string (like that found in +:setting:`AUTHENTICATION_BACKENDS`), not the actual backend class. How to log a user out --------------------- diff --git a/tests/auth_tests/test_auth_backends.py b/tests/auth_tests/test_auth_backends.py index 6ded9f2eb85..c0ccbb39710 100644 --- a/tests/auth_tests/test_auth_backends.py +++ b/tests/auth_tests/test_auth_backends.py @@ -605,6 +605,14 @@ class ImportedModelBackend(ModelBackend): pass +class CustomModelBackend(ModelBackend): + pass + + +class OtherModelBackend(ModelBackend): + pass + + class ImportedBackendTests(TestCase): """ #23925 - The backend path added to the session should be the same @@ -622,3 +630,38 @@ class ImportedBackendTests(TestCase): request = HttpRequest() request.session = self.client.session self.assertEqual(request.session[BACKEND_SESSION_KEY], self.backend) + + +class SelectingBackendTests(TestCase): + backend = 'auth_tests.test_auth_backends.CustomModelBackend' + other_backend = 'auth_tests.test_auth_backends.OtherModelBackend' + username = 'username' + password = 'password' + + def assertBackendInSession(self, backend): + request = HttpRequest() + request.session = self.client.session + self.assertEqual(request.session[BACKEND_SESSION_KEY], backend) + + @override_settings(AUTHENTICATION_BACKENDS=[backend]) + def test_backend_path_login_without_authenticate_single_backend(self): + user = User.objects.create_user(self.username, 'email', self.password) + self.client._login(user) + self.assertBackendInSession(self.backend) + + @override_settings(AUTHENTICATION_BACKENDS=[backend, other_backend]) + def test_backend_path_login_without_authenticate_multiple_backends(self): + user = User.objects.create_user(self.username, 'email', self.password) + expected_message = ( + 'You have multiple authentication backends configured and ' + 'therefore must provide the `backend` argument or set the ' + '`backend` attribute on the user.' + ) + with self.assertRaisesMessage(ValueError, expected_message): + self.client._login(user) + + @override_settings(AUTHENTICATION_BACKENDS=[backend, other_backend]) + def test_backend_path_login_with_explicit_backends(self): + user = User.objects.create_user(self.username, 'email', self.password) + self.client._login(user, self.other_backend) + self.assertBackendInSession(self.other_backend) diff --git a/tests/test_client/tests.py b/tests/test_client/tests.py index c47107a6761..ccddf76cf14 100644 --- a/tests/test_client/tests.py +++ b/tests/test_client/tests.py @@ -522,7 +522,6 @@ class ClientTest(TestCase): # Log in self.client.force_login(self.u1, backend='test_client.auth_backends.TestClientBackend') - self.assertEqual(self.u1.backend, 'test_client.auth_backends.TestClientBackend') # Request a page that requires a login response = self.client.get('/login_protected_view/')