Opened 10 days ago

Closed 10 days ago

Last modified 10 days ago

#36325 closed Bug (invalid)

Inconsistent error handling for inactive users in ModelBackend

Reported by: Ariel Souza Owned by:
Component: contrib.auth Version: 5.2
Severity: Normal Keywords: ModelBackend; authenticate
Cc: Ariel Souza Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

### 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.

Change History (3)

by Ariel Souza, 10 days ago

Attachment: image-20250413-153205.png added

comment:1 by ontowhee, 10 days ago

Resolution: invalid
Status: newclosed

Hello! Thanks for the report.

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.

Returning a generic error message is desirable from a security perspective and is recommended by OWASP: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#login.

A clear message should be displayed indicating that the account is inactive, as raised by confirm_login_allowed.

The use case you are describing goes against the security practices. Closing this ticket because the use case is not aligned with the security practices.

If you need assistance in using Django please feel free to ask for help from a friendly community member on Discord or the Django forum

comment:2 by Ariel Souza, 10 days ago

Thank you very much for the reply!

Note: See TracTickets for help on using tickets.
Back to Top