#36325: Inconsistent error handling for inactive users in ModelBackend
-------------------------------------+-------------------------------------
Reporter: Ariel Souza | Type: Bug
Status: new | Component:
| contrib.auth
Version: 5.2 | Severity: Normal
Keywords: | Triage Stage:
ModelBackend;authenticate | Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
### Bug Description
In the `ModelBackend`, the `authenticate` method checks whether
`user_can_authenticate(user)` returns `True` before calling
`confirm_login_allowed`. This causes `authenticate()` to return `None`
silently if the user is inactive (`is_active=False`), even when the
credentials are valid.
As a result, any login attempt with an inactive user results in a generic
"email or password is incorrect" error message. This is especially
misleading in the Django Admin, where users expect a specific message
indicating that the account is inactive.
### Proposed Solution
Refactor the logic in `authenticate()` to ensure that
`confirm_login_allowed(user)` is always called after the user is found and
credentials are valid, regardless of the user's active status. This way,
custom validation errors (such as inactive account warnings) are properly
raised.
### Steps to Reproduce
1. Create a user with `is_active=False`.
2. Attempt to log in using the correct credentials (e.g., via Django
Admin).
3. Observe that the system returns a generic "email or password is
incorrect" error.
### Expected Behavior
A clear message should be displayed indicating that the account is
inactive, as raised by `confirm_login_allowed`.
### My comment
I removed the self.user_can_authenticate(user) function from ModelBackend,
which solves the problem,
{{{
def authenticate(self, request, username=None, password=None, **kwargs):
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
if username is None or password is None:
return
try:
user = UserModel._default_manager.get_by_natural_key(username)
except UserModel.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user
(#20760).
UserModel().set_password(password)
else:
if user.check_password(password):
return user
}}}
but I couldn't create the test because the test always gave an error in
self.user_can_authenticate(user) of the ModelBackend, returning True for
the inactive user. Since I started studying Django recently and I'm
currently unemployed, I'll leave this up to you.
Here's my code:
{{{
def test_get_inactive_login_error(self):
User.objects.create_user(username="testinactive", password="pwd",
is_active=False)
data = {
"username": "testinactive",
"password": "pwd",
}
form = AuthenticationForm(None, data)
self.assertIn("This account is inactive.",
form.non_field_errors())
}}}
Thank you for reading this far.
--
Ticket URL: <
https://code.djangoproject.com/ticket/36325>
Django <
https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.