Commit 7cc4068c authored by Michael Farrell's avatar Michael Farrell Committed by Preston Holmes
Browse files

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

Thanks to Brad Pitcher for documentation
parent 8bd7b598
Loading
Loading
Loading
Loading
+22 −1
Original line number Diff line number Diff line
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
Original line number Diff line number Diff line
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
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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
----------------------------------