Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Messages for users with (user.is_active =False) flag during the login process

I am trying to add message during the log-in process , for a user who has an account, but deactivated, that he must activate it if he wants to get in.

I use LoginView controller, that uses built-in standard form called AuthenticationForm

AuthenticationForm has a following method:


def confirm_login_allowed(self, user):
    """
    Controls whether the given User may log in. This is a policy setting,
    independent of end-user authentication. This default behavior is to
    allow login by active users, and reject login by inactive users.

    If the given user cannot log in, this method should raise a
    ``forms.ValidationError``.

    If the given user may log in, this method should return None.
    """
    if not user.is_active:
        raise forms.ValidationError(
            self.error_messages['inactive'],
            code='inactive',

# and list of error messages within this class

error_messages = {
    'invalid_login': _(
        "Please enter a correct %(username)s and password. Note that both "
        "fields may be case-sensitive."
    ),
    'inactive': _("This account is inactive."),
}

So that technically if not user.is_active – it should show message 'inactive' but in my case for inactivated users with is_active = False DB table it shows the message 'invalid_login' instead. I am trying 100% correct login and password and user is not active but it shows me 'invalid_login' message. Then I just switch on is_active flag in DB to True and it lets me in easily. Do you have any idea why is that could be?

Final target is to show this message “'inactive': _("This account is inactive.")” to a user who has an account but deactivated. ( or custom message) Technically it should work but it doesn't. Thank you in advance and sorry in case you find this question elementary or dumb.

Tried:


class AuthCustomForm(AuthenticationForm):
    def clean(self):
        AuthenticationForm.clean(self)
        user = ExtraUser.objects.get(username=self.cleaned_data.get('username'))
        if not user.is_active and user:
            messages.warning(self.request, 'Please Activate your account',
                             extra_tags="", fail_silently=True)
           # return HttpResponseRedirect(' your url'))

FINALLY what helped:


class AuthCustomForm(AuthenticationForm):

    def get_invalid_login_error(self):

        user = ExtraUser.objects.get(username=self.cleaned_data.get('username'))

        if not user.is_active and user:
            raise forms.ValidationError(
                self.error_messages['inactive'],
                code='inactive',)
        else:
            return forms.ValidationError(
                self.error_messages['invalid_login'],
                code='invalid_login',
                params={'username': self.username_field.verbose_name},
            )

This is kind of wierd way to do it as DJANGO built -in code should have worked. I am not sure that i havent fixed my own mistake, made before here. perhaps i made things even worse.

like image 648
Aleksei Khatkevich Avatar asked Dec 10 '22 03:12

Aleksei Khatkevich


2 Answers

This is a long answer but hopefully it will be useful and provide some insight as to how things are working behind the scenes.

To see why the 'inactive' ValidationError isn't being raised for an inactive user, we have to start by looking at how the LoginView is implemented, specifically its post method.

def post(self, request, *args, **kwargs):
    """
    Handle POST requests: instantiate a form instance with the passed
    POST variables and then check if it's valid.
    """
    form = self.get_form()
    if form.is_valid():
        return self.form_valid(form)
    else:
        return self.form_invalid(form)

This method is called when the LoginView receives the POST request with the form data in it. get_form populates the AuthenticationForm with the POST data from the request and then the form is checked, returning a different response depending on whether it's valid or not. We're concerned with the form checking, so let's look dig into what the is_valid method is doing.

The Django docs do a good job of explaining how form and field validation works, so I won't go into too much detail. Basically, all that we need to know is that when the is_valid method of a form is called, the form first validates all of its fields individually, then calls its clean method to do any form-wide validation.

Here is where we need to look at how the AuthenticationForm is implemented, as it defines its own clean method.

def clean(self):
    username = self.cleaned_data.get('username')
    password = self.cleaned_data.get('password')

    if username is not None and password:
        self.user_cache = authenticate(self.request, username=username, password=password)
        if self.user_cache is None:
            raise self.get_invalid_login_error()
        else:
            self.confirm_login_allowed(self.user_cache)

    return self.cleaned_data

This is where the confirm_login_allowed method that you identified comes into play. We see that the username and password are passed to the authenticate function. This checks the given credentials against all of the authentication backends defined by the AUTHENTICATION_BACKENDS setting (see Django docs for more info), returning the authenticated user's User model if successful and None if not.

The result of authenticate is then checked. If it's None, then the user could not be authenticated, and the 'invalid_login' ValidationError is raised as expected. If not, then the user has been authenticated and confirm_login_allowed raises the 'inactive' ValidationError if the user is inactive.


So why isn't the 'inactive' ValidationError raised?

It's because the inactive user fails to authenticate, and so authenticate returns None, which means get_invalid_login_error is called instead of confirm_login_allowed.


Why does the inactive user fail to authenticate?

To see this, i'm going to assume that you are not using a custom authentication backend, which means that your AUTHENTICATION_BACKENDS setting is set to the default: ['django.contrib.auth.backends.ModelBackend']. This means that ModelBackend is the only authentication backend being used and we can look at its authenticate method which is what the previously seen authenticate function calls internally.

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) and self.user_can_authenticate(user):
            return user

We're interested in the last if statement.

if user.check_password(password) and self.user_can_authenticate(user):
    return user

For our inactive user, we know that the password is correct, so check_password will return True. This means that it must be the user_can_authenticate method which is returning False and causing the inactive user to not be authenticated. Hold on, because we're almost there...

def user_can_authenticate(self, user):
    """
    Reject users with is_active=False. Custom user models that don't have
    that attribute are allowed.
    """
    is_active = getattr(user, 'is_active', None)
    return is_active or is_active is None

Aha! user_can_authenticate returns False if user.is_active is False which causes the user to not authenticate.


The solution

We can subclass ModelBackend, override user_can_authenticate, and point the AUTHENTICATION_BACKENDS setting to this new subclass.

app/backends.py

from django.contrib.auth import backends


class CustomModelBackend(backends.ModelBackend):
    def user_can_authenticate(self, user):
        return True

settings.py

AUTHENTICATION_BACKENDS = [
    'app.backends.CustomModelBackend',
]


I think this solution is cleaner than changing the logic of get_invalid_login_error.

You can then override the 'inactive' ValidationError message by subclassing AuthenticationForm, overriding error_messages, and setting the authentication_form attribute of the LoginView to this new subclass.

from django.contrib.auth import forms as auth_forms, views as auth_views
from django.utils.translation import gettext_lazy as _


class CustomAuthenticationForm(auth_forms.AuthenticationForm):
    error_messages = {
        'invalid_login': _(
            "Please enter a correct %(username)s and password. Note that both "
            "fields may be case-sensitive."
        ),
        'inactive': _("CUSTOM INACTIVE MESSAGE."),
    }


class LoginView(auth_views.LoginView):
    authentication_form = CustomAuthenticationForm
like image 179
Marcus Caisey Avatar answered Dec 28 '22 07:12

Marcus Caisey




class AuthCustomForm(AuthenticationForm):

    def get_invalid_login_error(self):

        user = ExtraUser.objects.get(username=self.cleaned_data.get('username'))

        if not user.is_active and user:
            raise forms.ValidationError(
                self.error_messages['inactive'],
                code='inactive',)
        else:
            return forms.ValidationError(
                self.error_messages['invalid_login'],
                code='invalid_login',
                params={'username': self.username_field.verbose_name}, )
like image 32
Aleksei Khatkevich Avatar answered Dec 28 '22 07:12

Aleksei Khatkevich