Browse Source

Fixed #24855 -- Allowed using contrib.auth.login() without credentials.

Added an optional `backend` argument to login().
Paulo Poiati 9 years ago
parent
commit
b643386668

+ 1 - 0
AUTHORS

@@ -567,6 +567,7 @@ answer newbie questions, and generally made Django that much better:
     Paul Lanier <planier@google.com>
     Paul McLanahan <paul@mclanahan.net>
     Paul McMillan <Paul@McMillan.ws>
+    Paulo Poiati <paulogpoiati@gmail.com>
     Paulo Scardine <paulo@scardine.com.br>
     Paul Smith <blinkylights23@gmail.com>
     pavithran s <pavithran.s@gmail.com>

+ 16 - 2
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

+ 3 - 6
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()

+ 3 - 0
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`
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 

+ 30 - 13
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
-
-    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 <authentication-backends>` 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.
+    .. versionchanged:: 1.10
+
+        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 <authentication-backends>`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+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
 ---------------------

+ 43 - 0
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)

+ 0 - 1
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/')