This commit is contained in:
Omotayo Adeleke 2026-03-16 18:54:43 +02:00 committed by GitHub
commit b141f29b4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 82 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
@ -164,14 +165,37 @@ 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]
username = credentials.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
# See: https://github.com/jazzband/django-axes/issues/1159
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 credentials lookup",
username_field,
)
username = credentials.get(username_field, None)
return username # 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)
username = request_data.get(settings.AXES_USERNAME_FORM_FIELD, None)
if username is None:
# Fallback to Django's USERNAME_FIELD for compatibility
# See: https://github.com/jazzband/django-axes/issues/1159
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 request data lookup",
username_field,
)
username = request_data.get(username_field, None)
return username # type: ignore[return-value]
def get_client_ip_address(

View file

@ -47,7 +47,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

@ -792,6 +792,61 @@ 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)
def get_username(request, credentials: dict) -> str:
return "username"