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

Thanks to Brad Pitcher for documentation
This commit is contained in:
Michael Farrell 2012-07-12 11:13:15 +09:30 committed by Preston Holmes
parent 8bd7b598b6
commit 7cc4068c44
5 changed files with 62 additions and 3 deletions

View File

@ -1,6 +1,8 @@
import re
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.utils.importlib import import_module 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' SESSION_KEY = '_auth_user_id'
BACKEND_SESSION_KEY = '_auth_user_backend' BACKEND_SESSION_KEY = '_auth_user_backend'
@ -33,6 +35,21 @@ def get_backends():
return 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): def authenticate(**credentials):
""" """
If the given credentials are valid, return a User object. 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__) user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
return user 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): def login(request, user):
""" """

View File

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

View File

@ -18,27 +18,41 @@ class SignalTestCase(TestCase):
def listener_logout(self, user, **kwargs): def listener_logout(self, user, **kwargs):
self.logged_out.append(user) self.logged_out.append(user)
def listener_login_failed(self, sender, credentials, **kwargs):
self.login_failed.append(credentials)
def setUp(self): def setUp(self):
"""Set up the listeners and reset the logged in/logged out counters""" """Set up the listeners and reset the logged in/logged out counters"""
self.logged_in = [] self.logged_in = []
self.logged_out = [] self.logged_out = []
self.login_failed = []
signals.user_logged_in.connect(self.listener_login) signals.user_logged_in.connect(self.listener_login)
signals.user_logged_out.connect(self.listener_logout) signals.user_logged_out.connect(self.listener_logout)
signals.user_login_failed.connect(self.listener_login_failed)
def tearDown(self): def tearDown(self):
"""Disconnect the listeners""" """Disconnect the listeners"""
signals.user_logged_in.disconnect(self.listener_login) signals.user_logged_in.disconnect(self.listener_login)
signals.user_logged_out.disconnect(self.listener_logout) signals.user_logged_out.disconnect(self.listener_logout)
signals.user_login_failed.disconnect(self.listener_login_failed)
def test_login(self): 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.client.login(username='testclient', password='bad')
self.assertEqual(len(self.logged_in), 0) 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: # Like this:
self.client.login(username='testclient', password='password') self.client.login(username='testclient', password='password')
self.assertEqual(len(self.logged_in), 1) self.assertEqual(len(self.logged_in), 1)
self.assertEqual(self.logged_in[0].username, 'testclient') 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): def test_logout_anonymous(self):
# The log_out function will still trigger the signal for anonymous # The log_out function will still trigger the signal for anonymous
# users. # users.

View File

@ -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 recommended as good practice to provide those templates in order to present
pretty error pages to the user. 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 Backwards incompatible changes in 1.5
===================================== =====================================

View File

@ -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. for notification when a user logs in or out.
.. data:: django.contrib.auth.signals.user_logged_in .. data:: django.contrib.auth.signals.user_logged_in
:module:
.. versionadded:: 1.3
Sent when a user logs in successfully. Sent when a user logs in successfully.
Arguments sent with this signal: Arguments sent with this signal:
``sender`` ``sender``
As above: the class of the user that just logged in. The class of the user that just logged in.
``request`` ``request``
The current :class:`~django.http.HttpRequest` instance. The current :class:`~django.http.HttpRequest` instance.
@ -891,6 +893,8 @@ Arguments sent with this signal:
The user instance that just logged in. The user instance that just logged in.
.. data:: django.contrib.auth.signals.user_logged_out .. data:: django.contrib.auth.signals.user_logged_out
:module:
.. versionadded:: 1.3
Sent when the logout method is called. 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 The user instance that just logged out or ``None`` if the
user was not authenticated. 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 Limiting access to logged-in users
---------------------------------- ----------------------------------