Fixed #18616 -- added user_login_fail signal to contrib.auth
Thanks to Brad Pitcher for documentation
This commit is contained in:
parent
8bd7b598b6
commit
7cc4068c44
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue