diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py
index 2f620a3..ca85879 100644
a
|
b
|
|
1 | 1 | import re |
| 2 | import logging |
2 | 3 | |
3 | 4 | from django.conf import settings |
4 | 5 | from django.core.exceptions import ImproperlyConfigured, PermissionDenied |
… |
… |
def authenticate(**credentials):
|
59 | 60 | user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) |
60 | 61 | return user |
61 | 62 | |
62 | | # The credentials supplied are invalid to all backends, fire signal |
63 | | user_login_failed.send(sender=__name__, |
64 | | credentials=_clean_credentials(credentials)) |
| 63 | # The credentials supplied are invalid to all backends, fire signal and log error |
| 64 | credentials=_clean_credentials(credentials) |
| 65 | user_login_failed.send(sender=__name__, credentials=credentials) |
| 66 | |
| 67 | logger = logging.getLogger('django.security.FailedLogin') |
| 68 | # Make sure username is in extra context, for formatting purposes |
| 69 | if 'username' not in credentials: |
| 70 | credentials['username'] = None |
| 71 | logger.warning("user login failed", extra=credentials) |
65 | 72 | |
66 | 73 | |
67 | 74 | def login(request, user): |
diff --git a/django/test/utils.py b/django/test/utils.py
index 591c588..ee6278e 100644
a
|
b
|
class IgnorePendingDeprecationWarningsMixin(IgnoreDeprecationWarningsMixin):
|
421 | 421 | |
422 | 422 | |
423 | 423 | @contextmanager |
424 | | def patch_logger(logger_name, log_level): |
| 424 | def patch_logger(logger_name, log_level, formatstr=''): |
425 | 425 | """ |
426 | 426 | Context manager that takes a named logger and the logging level |
427 | 427 | and provides a simple mock-like list of messages received |
428 | 428 | """ |
429 | 429 | calls = [] |
430 | | def replacement(msg): |
431 | | calls.append(msg) |
| 430 | def replacement(msg, *args, **kwargs): |
| 431 | extra = kwargs.get('extra',{}) |
| 432 | calls.append((formatstr % extra) + (msg % args)) |
432 | 433 | logger = logging.getLogger(logger_name) |
433 | 434 | orig = getattr(logger, log_level) |
434 | 435 | setattr(logger, log_level, replacement) |
diff --git a/docs/ref/contrib/auth.txt b/docs/ref/contrib/auth.txt
index afbc6ec..7fc0343 100644
a
|
b
|
can be used for notification when a user logs in or out.
|
409 | 409 | authentication backend. Credentials matching a set of 'sensitive' patterns, |
410 | 410 | (including password) will not be sent in the clear as part of the signal. |
411 | 411 | |
| 412 | It should be noted that the failure is also logged as a warning to ``django. |
| 413 | security.FailedLogin`` |
| 414 | |
| 415 | .. versionadded:: 1.6 |
| 416 | |
412 | 417 | .. _authentication-backends-reference: |
413 | 418 | |
414 | 419 | Authentication backends |
diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt
index a88201a..78cf32b 100644
a
|
b
|
level or handlers that are installed.
|
437 | 437 | ``django.security.*`` |
438 | 438 | ~~~~~~~~~~~~~~~~~~~~~~ |
439 | 439 | |
| 440 | .. versionadded:: 1.6 |
| 441 | |
440 | 442 | The security loggers will receive messages on any occurrence of |
441 | 443 | :exc:`~django.core.exceptions.SuspiciousOperation`. There is a sub-logger for |
442 | 444 | each sub-type of SuspiciousOperation. The level of the log event depends on |
… |
… |
specific logger following this example::
|
463 | 465 | 'propagate': False, |
464 | 466 | }, |
465 | 467 | |
| 468 | Invalid username/passwords will be logged to ``django.security.FailedLogin`` as |
| 469 | a warning. This logger will have the following extra context: |
| 470 | |
| 471 | * ``username``: the provided username which failed. |
| 472 | |
466 | 473 | Handlers |
467 | 474 | -------- |
468 | 475 | |
diff --git a/tests/logging_tests/tests.py b/tests/logging_tests/tests.py
index 0c2d269..ba87d78 100644
a
|
b
|
class SettingsConfigureLogging(TestCase):
|
358 | 358 | |
359 | 359 | |
360 | 360 | class SecurityLoggerTest(TestCase): |
361 | | |
| 361 | """ |
| 362 | Test that security messages are being logged |
| 363 | """ |
362 | 364 | urls = 'logging_tests.urls' |
363 | 365 | |
364 | 366 | def test_suspicious_operation_creates_log_message(self): |
… |
… |
class SecurityLoggerTest(TestCase):
|
374 | 376 | response = self.client.get('/suspicious_spec/') |
375 | 377 | self.assertEqual(len(calls), 1) |
376 | 378 | self.assertEqual(calls[0], 'dubious') |
| 379 | |
| 380 | def test_user_login_failed_creates_log_message(self): |
| 381 | with self.settings(DEBUG=True): |
| 382 | with patch_logger('django.security.FailedLogin', 'warning', "%(username)s : ") as calls: |
| 383 | self.client.login(username='testclient', password='bad') |
| 384 | self.assertEqual(len(calls), 1) |
| 385 | self.assertEqual(calls[0], "testclient : user login failed") |
| 386 | |
| 387 | def test_no_user_login_failed_creates_log_message(self): |
| 388 | with self.settings(DEBUG=True): |
| 389 | with patch_logger('django.security.FailedLogin', 'warning', "%(username)s : ") as calls: |
| 390 | self.client.login() |
| 391 | self.assertEqual(len(calls), 1) |
| 392 | self.assertEqual(calls[0], "None : user login failed") |