djangodjango-formsdjango-authentication

'AnonymousUser' object has no attribute '_meta' in Login view


with custom AuthenticationForm get error when trying login with a non-existent account but when use existent account error dont raise

  File "C:\python\CTFp\.venv\Lib\site-packages\django\utils\functional.py", line 253, in inner
    return func(_wrapped, *args)
           ^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'AnonymousUser' object has no attribute '_meta'

forms.py

class AuthenticationForm(BaseAuthenticationForm):

    def clean(self):
        username = self.cleaned_data.get("username")
        password = self.cleaned_data.get("password")
        ogg = User.objects.filter(Q(email=username) or Q(username=username)).first()

        if ogg is not None and password:
            self.user_cache = authenticate(
                self.request, username=ogg.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

urls.py

urlpatterns = [path('signin', views.LoginView.as_view(authentication_form=forms.AuthenticationForm),
         {'template_name': 'users/signin.html'},
         name='signin'),]

models.py

class User(AbstractUser):
    challenges = models.ManyToManyField(Challenge)

tried to change auth form to base AuthenticationForm and error dont raises.


Solution

  • I think you are trying to fix the problem at the wrong location: the form should not be responsible on how to authenticate, that should the authentication backend do. We can write a custom authentication backend [Django-doc] to work with a username or an email:

    # app_name/authentication.py
    from django.contrib.auth.backends import BaseBackend
    from django.db.models import Q
    
    
    class UsernameEmailAuthenticationBackend(BaseBackend):
        def authenticate(self, request, username=None, password=None):
            try:
                user = User.objects.get(Q(email=username) | Q(username=username))
            except User.DoesNotExist:
                return None
            if user.check_password(password):
                return user
            return None

    and add this to the AUTHENTICATION_BACKENDS setting [Django-doc]:

    # settings.py
    
    # …
    
    AUTHENTICATION_BACKENDS = ['app_name.authentication.UsernameEmailAuthenticationBackend']

    Although probably not the most important part, it means it will fetch the user once, whereas by trying to find the username in the form would trigger a second query.