Browse Source

Fixed #18616 -- added user_login_fail signal to contrib.auth

Thanks to Brad Pitcher for documentation
Michael Farrell 12 years ago
parent
commit
7cc4068c44

+ 22 - 1
django/contrib/auth/__init__.py

@@ -1,6 +1,8 @@
+import re
+
 from django.core.exceptions import ImproperlyConfigured
 from django.utils.importlib import import_module
-from django.contrib.auth.signals import user_logged_in, user_logged_out
+from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed
 
 SESSION_KEY = '_auth_user_id'
 BACKEND_SESSION_KEY = '_auth_user_backend'
@@ -33,6 +35,21 @@ def get_backends():
     return backends
 
 
+def _clean_credentials(credentials):
+    """
+    Cleans a dictionary of credentials of potentially sensitive info before
+    sending to less secure functions.
+
+    Not comprehensive - intended for user_login_failed signal
+    """
+    SENSITIVE_CREDENTIALS = re.compile('api|token|key|secret|password|signature', re.I)
+    CLEANSED_SUBSTITUTE = '********************'
+    for key in credentials:
+        if SENSITIVE_CREDENTIALS.search(key):
+            credentials[key] = CLEANSED_SUBSTITUTE
+    return credentials
+
+
 def authenticate(**credentials):
     """
     If the given credentials are valid, return a User object.
@@ -49,6 +66,10 @@ def authenticate(**credentials):
         user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
         return user
 
+    # The credentials supplied are invalid to all backends, fire signal
+    user_login_failed.send(sender=__name__,
+            credentials=_clean_credentials(credentials))
+
 
 def login(request, user):
     """

+ 1 - 0
django/contrib/auth/signals.py

@@ -1,4 +1,5 @@
 from django.dispatch import Signal
 
 user_logged_in = Signal(providing_args=['request', 'user'])
+user_login_failed = Signal(providing_args=['credentials'])
 user_logged_out = Signal(providing_args=['request', 'user'])

+ 15 - 1
django/contrib/auth/tests/signals.py

@@ -18,27 +18,41 @@ class SignalTestCase(TestCase):
     def listener_logout(self, user, **kwargs):
         self.logged_out.append(user)
 
+    def listener_login_failed(self, sender, credentials, **kwargs):
+        self.login_failed.append(credentials)
+
     def setUp(self):
         """Set up the listeners and reset the logged in/logged out counters"""
         self.logged_in = []
         self.logged_out = []
+        self.login_failed = []
         signals.user_logged_in.connect(self.listener_login)
         signals.user_logged_out.connect(self.listener_logout)
+        signals.user_login_failed.connect(self.listener_login_failed)
 
     def tearDown(self):
         """Disconnect the listeners"""
         signals.user_logged_in.disconnect(self.listener_login)
         signals.user_logged_out.disconnect(self.listener_logout)
+        signals.user_login_failed.disconnect(self.listener_login_failed)
 
     def test_login(self):
-        # Only a successful login will trigger the signal.
+        # Only a successful login will trigger the success signal.
         self.client.login(username='testclient', password='bad')
         self.assertEqual(len(self.logged_in), 0)
+        self.assertEqual(len(self.login_failed), 1)
+        self.assertEqual(self.login_failed[0]['username'], 'testclient')
+        # verify the password is cleansed
+        self.assertTrue('***' in self.login_failed[0]['password'])
+
         # Like this:
         self.client.login(username='testclient', password='password')
         self.assertEqual(len(self.logged_in), 1)
         self.assertEqual(self.logged_in[0].username, 'testclient')
 
+        # Ensure there were no more failures.
+        self.assertEqual(len(self.login_failed), 1)
+
     def test_logout_anonymous(self):
         # The log_out function will still trigger the signal for anonymous
         # users.

+ 4 - 0
docs/releases/1.5.txt

@@ -191,6 +191,10 @@ Django 1.5 also includes several smaller improvements worth noting:
   recommended as good practice to provide those templates in order to present
   pretty error pages to the user.
 
+* :mod:`django.contrib.auth` provides a new signal that is emitted
+  whenever a user fails to login successfully. See
+  :data:`~django.contrib.auth.signals.user_login_failed`
+
 Backwards incompatible changes in 1.5
 =====================================
 

+ 20 - 1
docs/topics/auth.txt

@@ -876,13 +876,15 @@ The auth framework uses two :doc:`signals </topics/signals>` that can be used
 for notification when a user logs in or out.
 
 .. data:: django.contrib.auth.signals.user_logged_in
+   :module:
+.. versionadded:: 1.3
 
 Sent when a user logs in successfully.
 
 Arguments sent with this signal:
 
 ``sender``
-    As above: the class of the user that just logged in.
+    The class of the user that just logged in.
 
 ``request``
     The current :class:`~django.http.HttpRequest` instance.
@@ -891,6 +893,8 @@ Arguments sent with this signal:
     The user instance that just logged in.
 
 .. data:: django.contrib.auth.signals.user_logged_out
+   :module:
+.. versionadded:: 1.3
 
 Sent when the logout method is called.
 
@@ -905,6 +909,21 @@ Sent when the logout method is called.
     The user instance that just logged out or ``None`` if the
     user was not authenticated.
 
+.. data:: django.contrib.auth.signals.user_login_failed
+   :module:
+.. versionadded:: 1.5
+
+Sent when the user failed to login successfully
+
+``sender``
+    The name of the module used for authentication.
+
+``credentials``
+    A dictonary of keyword arguments containing the user credentials that were
+    passed to :func:`~django.contrib.auth.authenticate()` or your own custom
+    authentication backend. Credentials matching a set of 'sensitive' patterns,
+    (including password) will not be sent in the clear as part of the signal.
+
 Limiting access to logged-in users
 ----------------------------------