Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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

Thanks to Brad Pitcher for documentation
  • Loading branch information...
commit 7cc4068c4470876c526830778cbdac2fdfd6dc26 1 parent 8bd7b59
@micolous micolous authored ptone committed
View
23 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):
"""
View
1  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'])
View
16 django/contrib/auth/tests/signals.py
@@ -18,27 +18,41 @@ def listener_login(self, user, **kwargs):
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.
View
4 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
=====================================
View
21 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
----------------------------------

2 comments on commit 7cc4068

@claudep
Collaborator

The 1.3 notices were removed in 837425b. I guess you shouldn't add new here, right?

@ptone
Collaborator

whoops - I had missed that one.

Do we have the lifetime of these documented somewhere - I don't recall seeing it, but could have missed it.

Thanks @claudep for reviewing me so far - a second set of eye is always appreciated.

Please sign in to comment.
Something went wrong with that request. Please try again.