Search code examples
djangodjango-formsdjango-authentication

Django CustomAuthForm and default LoginForm not raising validation error


I extended the Django AuthenticationForm and created two backends to allow users to sign in with either (or both) email and username. This works perfectly if I try to sign in to the Django admin using any of the credentials. Although I can sign in on the frontend using any of these credentials, validation errors fail to show anytime the wrong credentials are used.

forms.py

class CustomAuthenticationForm(AuthenticationForm):
    username = forms.CharField(widget=TextInput(
        attrs={'placeholder': 'Enter your email address'}))
    password = forms.CharField(widget=PasswordInput(attrs={'placeholder': 'Password'}))
    remember_me = forms.BooleanField(required=False, label="Keep me signed in", initial=False)

views.py

class CustomLoginView(LoginView):
    form_class = CustomAuthenticationForm
    redirect_field_name = REDIRECT_FIELD_NAME
    template_name = 'login.html'

    def get(self, request, *args, **kwargs):
        if request.user.is_authenticated:
            if request.user.is_personal:
                return HttpResponseRedirect(reverse(...))
            elif request.user.is_business:
                return HttpResponseRedirect(reverse(...))
            else:
                return HttpResponseRedirect(reverse(...))
        return super(LoginView, self).get(request, *args, **kwargs)

    def form_valid(self, form):
        remember_me = form.cleaned_data['remember_me']
        if not remember_me:
            self.request.session.set_expiry(0)
            self.request.session.modified = True
        auth_login(self.request, form.get_user())
        return HttpResponseRedirect(self.get_success_url())

    def get_success_url(self):
        if self.request.user.is_personal:
            return reverse(...)
        elif self.request.user.is_business:
            return reverse(...)
        else:
            return reverse(...)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        current_site = get_current_site(self.request)
        context.update({
            self.redirect_field_name: self.get_redirect_url(),
            'site': current_site,
            'site_name': current_site.name,
            'title': _('Log in to your account'),
            **(self.extra_context or {})
        })
        return context

backends.py

from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model

User = get_user_model()


class CaseInsensitiveModelBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        UserModel = get_user_model()
        if username is None:
            username = kwargs.get(UserModel.USERNAME_FIELD)
        try:
            case_insensitive_username_field = '{}__iexact'.format(UserModel.USERNAME_FIELD)
            user = UserModel._default_manager.get(**{case_insensitive_username_field: username})
            UserModel().set_password(password)
        else:
            if user.check_password(password) and self.user_can_authenticate(user):
                return user


class CustomEmailAuthenticationBackend(ModelBackend):

    def authenticate(self, request, **kwargs):
        email = kwargs['username'].lower()
        password = kwargs['password']
        try:
            user_with_email = User.objects.get(email=email)
        except User.DoesNotExist:
            return None
        else:
            if user_with_email.is_active and user_with_email.check_password(password):
                return user_with_email
        return None

    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

settings.py

AUTHENTICATION_BACKENDS = (
    'accounts.backends.CustomEmailAuthenticationBackend',
    'accounts.backends.CaseInsensitiveModelBackend',
    'django.contrib.auth.backends.AllowAllUsersModelBackend',
)

login.html

<form class="form" method="post" enctype="multipart/form-data" role="form" 
action="{% URL:'accounts:login' %}">
     {% csrf_token %}
     {% bootstrap_messages %}
     {% bootstrap_field form.username show_label=False %}
     {% bootstrap_field form.password show_label=False %}
     {% bootstrap_field form.remember_me show_label=False %}

     <button class="btn block-btn" type="submit" role="button">{% trans 'Sign in' %}</button>
</form>

urls.py

path(_('login/'), CustomLoginView.as_view(), name='login'),

Solution

  • You don't render the errors so they don't show up. If you render the fields individually the forms errors won't be rendered automatically, you need to render them yourselves. You can do this by looping over form.non_field_errors (for non-field errors) and form.<field_name>.errors (for errors on specific fields):

    <form class="form" method="post" enctype="multipart/form-data" role="form" 
    action="{% URL:'accounts:login' %}">
        {% csrf_token %}
        {% bootstrap_messages %}
        {% for error in form.non_field_errors %}
            {{ error }}
        {% endfor %}
        {% bootstrap_field form.username show_label=False %}
        {% for error in form.username.errors %}
            {{ error }}
        {% endfor %}
        {% bootstrap_field form.password show_label=False %}
        {% for error in form.password.errors %}
            {{ error }}
        {% endfor %}
        {% bootstrap_field form.remember_me show_label=False %}
        {% for error in form.remember_me.errors %}
            {{ error }}
        {% endfor %}
    
        <button class="btn block-btn" type="submit" role="button">{% trans 'Sign in' %}</button>
    </form>