This commit is contained in:
Omotayo Adeleke 2026-03-20 13:55:54 +00:00 committed by GitHub
commit f1c8aaabc4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 126 additions and 3 deletions

View file

@ -5,6 +5,7 @@ from string import Template
from typing import Callable, Optional, Type, Union, List
from urllib.parse import urlencode
from django.contrib.auth import get_user_model
from django.core.cache import BaseCache, caches
from django.http import HttpRequest, HttpResponse, JsonResponse, QueryDict
from django.shortcuts import redirect, render
@ -131,6 +132,32 @@ def get_credentials(username: Optional[str] = None, **kwargs) -> dict:
return credentials
def get_username_from_data(data: dict) -> Optional[str]:
"""
Extract username from a dict-like object (credentials or request data).
Tries AXES_USERNAME_FORM_FIELD first, then falls back to Django's USERNAME_FIELD
for compatibility with Django's user_login_failed signal.
See: https://github.com/jazzband/django-axes/issues/1159
:param data: dict-like object containing username (credentials or request.POST)
:return: username string or None if not found
"""
username = data.get(settings.AXES_USERNAME_FORM_FIELD, None)
if username is None:
# Fallback to Django's USERNAME_FIELD for compatibility with
# Django's user_login_failed signal which uses USERNAME_FIELD as the key
username_field = get_user_model().USERNAME_FIELD
if username_field != str(settings.AXES_USERNAME_FORM_FIELD):
log.debug(
"Falling back to USERNAME_FIELD '%s' for data lookup",
username_field,
)
username = data.get(username_field, None)
return username
def get_client_username(
request: HttpRequest, credentials: Optional[dict] = None
) -> str:
@ -164,14 +191,14 @@ def get_client_username(
log.debug(
"Using parameter credentials to get username with key settings.AXES_USERNAME_FORM_FIELD"
)
return credentials.get(settings.AXES_USERNAME_FORM_FIELD, None) # type: ignore[return-value]
return get_username_from_data(credentials) # type: ignore[return-value]
log.debug(
"Using parameter request.POST to get username with key settings.AXES_USERNAME_FORM_FIELD"
)
request_data = getattr(request, "data", request.POST)
return request_data.get(settings.AXES_USERNAME_FORM_FIELD, None)
return get_username_from_data(request_data) # type: ignore[return-value]
def get_client_ip_address(

View file

@ -49,7 +49,7 @@ The following ``settings.py`` options are available for customizing Axes behavio
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| AXES_VERBOSE | True | If ``True``, you'll see slightly more logging for Axes. |
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| AXES_USERNAME_FORM_FIELD | 'settings.AUTH_USER_MODEL.USERNAME_FIELD' | The name of the form field that contains your users usernames. |
| AXES_USERNAME_FORM_FIELD | 'settings.AUTH_USER_MODEL.USERNAME_FIELD' | The name of the form field that contains your users usernames. When looking up the username from credentials or request data, Axes will first try this field, then fall back to Django's ``USERNAME_FIELD`` if not found. This ensures compatibility with Django's ``user_login_failed`` signal which uses ``USERNAME_FIELD``. |
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| AXES_USERNAME_CALLABLE | None | A callable or a string path to callable that takes two arguments for user lookups: ``def get_username(request: HttpRequest, credentials: dict) -> str: ...``. This can be any callable such as ``AXES_USERNAME_CALLABLE = lambda request, credentials: 'username'`` or a full Python module path to callable such as ``AXES_USERNAME_CALLABLE = 'example.get_username``. The ``request`` is a HttpRequest like object and the ``credentials`` is a dictionary like object. ``credentials`` are the ones that were passed to Django ``authenticate()`` in the login flow. If no function is supplied, Axes fetches the username from the ``credentials`` or ``request.POST`` dictionaries based on ``AXES_USERNAME_FORM_FIELD``. |
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

View file

@ -18,6 +18,7 @@ from axes.helpers import (
get_cool_off,
get_cool_off_iso8601,
get_lockout_response,
get_username_from_data,
is_client_ip_address_blacklisted,
is_client_ip_address_whitelisted,
is_client_method_whitelisted,
@ -792,6 +793,101 @@ class UsernameTestCase(AxesTestCase):
def test_get_client_username_str(self):
self.assertEqual(get_client_username(HttpRequest(), {}), "username")
@override_settings(AXES_USERNAME_FORM_FIELD="auth-username")
def test_get_client_username_fallback_to_username_field_from_credentials(self):
"""
Test that when AXES_USERNAME_FORM_FIELD is set to a custom value that
doesn't exist in credentials, we fallback to Django's USERNAME_FIELD.
This fixes https://github.com/jazzband/django-axes/issues/1159
"""
expected = "test-username"
request = HttpRequest()
# Credentials from Django's user_login_failed signal use USERNAME_FIELD (e.g., "username")
# not the custom AXES_USERNAME_FORM_FIELD value
credentials = {"username": expected}
actual = get_client_username(request, credentials)
self.assertEqual(expected, actual)
@override_settings(AXES_USERNAME_FORM_FIELD="auth-username")
def test_get_client_username_fallback_to_username_field_from_request(self):
"""
Test that when AXES_USERNAME_FORM_FIELD is set to a custom value that
doesn't exist in request.POST, we fallback to Django's USERNAME_FIELD.
This fixes https://github.com/jazzband/django-axes/issues/1159
"""
expected = "test-username"
request = HttpRequest()
# POST data might use Django's USERNAME_FIELD instead of custom form field
request.POST["username"] = expected
actual = get_client_username(request)
self.assertEqual(expected, actual)
@override_settings(AXES_USERNAME_FORM_FIELD="auth-username")
def test_get_client_username_custom_field_takes_priority(self):
"""
Test that AXES_USERNAME_FORM_FIELD takes priority when it exists in credentials.
"""
expected = "custom-field-username"
fallback = "username-field-value"
request = HttpRequest()
credentials = {
"auth-username": expected, # Custom field - should be used
"username": fallback, # Django's USERNAME_FIELD - should be ignored
}
actual = get_client_username(request, credentials)
self.assertEqual(expected, actual)
class GetUsernameFromDataTestCase(AxesTestCase):
"""Unit tests for the get_username_from_data helper function."""
@override_settings(AXES_USERNAME_FORM_FIELD="username")
def test_get_username_from_data_with_matching_field(self):
"""Test that username is returned when AXES_USERNAME_FORM_FIELD matches."""
data = {"username": "test-user"}
self.assertEqual(get_username_from_data(data), "test-user")
@override_settings(AXES_USERNAME_FORM_FIELD="custom-field")
def test_get_username_from_data_fallback_to_username_field(self):
"""
Test that when AXES_USERNAME_FORM_FIELD doesn't exist in data,
we fallback to Django's USERNAME_FIELD.
"""
# Django's default USERNAME_FIELD is "username"
data = {"username": "fallback-user"}
self.assertEqual(get_username_from_data(data), "fallback-user")
@override_settings(AXES_USERNAME_FORM_FIELD="custom-field")
def test_get_username_from_data_custom_field_takes_priority(self):
"""Test that AXES_USERNAME_FORM_FIELD takes priority over USERNAME_FIELD."""
data = {
"custom-field": "custom-user",
"username": "fallback-user",
}
self.assertEqual(get_username_from_data(data), "custom-user")
@override_settings(AXES_USERNAME_FORM_FIELD="nonexistent")
def test_get_username_from_data_returns_none_when_not_found(self):
"""Test that None is returned when username is not found in any field."""
data = {"other-field": "some-value"}
self.assertIsNone(get_username_from_data(data))
@override_settings(AXES_USERNAME_FORM_FIELD="username")
def test_get_username_from_data_with_empty_dict(self):
"""Test that None is returned for empty dict."""
self.assertIsNone(get_username_from_data({}))
def get_username(request, credentials: dict) -> str:
return "username"