diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index 77458e15f9..a490eff040 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -94,7 +94,7 @@ def authenticate(request=None, **credentials): return user # The credentials supplied are invalid to all backends, fire signal - user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials)) + user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials), request=request) def login(request, user, backend=None): diff --git a/django/contrib/auth/signals.py b/django/contrib/auth/signals.py index 71ab6a11d1..6415da45fa 100644 --- a/django/contrib/auth/signals.py +++ b/django/contrib/auth/signals.py @@ -1,5 +1,5 @@ from django.dispatch import Signal user_logged_in = Signal(providing_args=['request', 'user']) -user_login_failed = Signal(providing_args=['credentials']) +user_login_failed = Signal(providing_args=['credentials', 'request']) user_logged_out = Signal(providing_args=['request', 'user']) diff --git a/docs/ref/contrib/auth.txt b/docs/ref/contrib/auth.txt index 38f18b6dc8..69e3f40514 100644 --- a/docs/ref/contrib/auth.txt +++ b/docs/ref/contrib/auth.txt @@ -479,6 +479,14 @@ can be used for notification when a user logs in or out. authentication backend. Credentials matching a set of 'sensitive' patterns, (including password) will not be sent in the clear as part of the signal. + ``request`` + The :class:`~django.http.HttpRequest` object, if one was provided to + :func:`~django.contrib.auth.authenticate`. + + .. versionchanged:: 1.11 + + The ``request`` argument was added. + .. _authentication-backends-reference: Authentication backends diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt index a6edfcdf25..3cd56c515e 100644 --- a/docs/releases/1.11.txt +++ b/docs/releases/1.11.txt @@ -117,6 +117,9 @@ Minor features which in turn passes it to the authentication backend if it accepts a ``request`` argument. +* The :func:`~django.contrib.auth.signals.user_login_failed` signal now + receives a ``request`` argument. + :mod:`django.contrib.contenttypes` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/auth_tests/test_forms.py b/tests/auth_tests/test_forms.py index 19f0f4521c..c0e2961424 100644 --- a/tests/auth_tests/test_forms.py +++ b/tests/auth_tests/test_forms.py @@ -12,6 +12,7 @@ from django.contrib.auth.forms import ( SetPasswordForm, UserChangeForm, UserCreationForm, ) from django.contrib.auth.models import User +from django.contrib.auth.signals import user_login_failed from django.contrib.sites.models import Site from django.core import mail from django.core.mail import EmailMultiAlternatives @@ -279,6 +280,24 @@ class AuthenticationFormTest(TestDataMixin, TestCase): self.assertFalse(form.is_valid()) self.assertEqual(form.non_field_errors(), [force_text(form.error_messages['inactive'])]) + def test_login_failed(self): + signal_calls = [] + + def signal_handler(**kwargs): + signal_calls.append(kwargs) + + user_login_failed.connect(signal_handler) + fake_request = object() + try: + form = AuthenticationForm(fake_request, { + 'username': 'testclient', + 'password': 'incorrect', + }) + self.assertFalse(form.is_valid()) + self.assertIs(signal_calls[0]['request'], fake_request) + finally: + user_login_failed.disconnect(signal_handler) + def test_inactive_user_i18n(self): with self.settings(USE_I18N=True), translation.override('pt-br', deactivate=True): # The user is inactive. diff --git a/tests/auth_tests/test_signals.py b/tests/auth_tests/test_signals.py index 6f1ad0a7b5..c21daa3c1d 100644 --- a/tests/auth_tests/test_signals.py +++ b/tests/auth_tests/test_signals.py @@ -1,4 +1,4 @@ -from django.contrib.auth import signals +from django.contrib.auth import authenticate, signals from django.contrib.auth.models import User from django.test import TestCase, override_settings from django.test.client import RequestFactory @@ -18,8 +18,8 @@ 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 listener_login_failed(self, sender, **kwargs): + self.login_failed.append(kwargs) def setUp(self): """Set up the listeners and reset the logged in/logged out counters""" @@ -41,9 +41,10 @@ class SignalTestCase(TestCase): 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') + self.assertEqual(self.login_failed[0]['credentials']['username'], 'testclient') # verify the password is cleansed - self.assertIn('***', self.login_failed[0]['password']) + self.assertIn('***', self.login_failed[0]['credentials']['password']) + self.assertIn('request', self.login_failed[0]) # Like this: self.client.login(username='testclient', password='password') @@ -77,3 +78,7 @@ class SignalTestCase(TestCase): user.refresh_from_db() self.assertEqual(user.username, 'staff') self.assertNotEqual(user.last_login, old_last_login) + + def test_failed_login_without_request(self): + authenticate(username='testclient', password='bad') + self.assertIsNone(self.login_failed[0]['request'])