Search code examples
pythondjangodjango-authentication

Is it bad written my custom authentication backend in Django?


I'm trying to implement a custom authentication backend in Django, mi custom Auth model and my custom user creation form works ok, but when I'm try to login using my custom authentication backend, it raise me this excepcion.

Internal Server Error: /login/
Traceback (most recent call last):
  File "C:\Users\G-FIVE\Desktop\Projects\revenue\venv\Lib\site-packages\django\core\handlers\exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\G-FIVE\Desktop\Projects\revenue\venv\Lib\site-packages\django\core\handlers\base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\Projects\revenue\authentication\views.py", line 12, in login
    usuario = UsuarioAuthBackend.authenticate(request, email=email, password=password)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: UsuarioAuthBackend.authenticate() missing 1 required positional argument: 'request'
[09/Jun/2023 10:47:15] "POST /login/ HTTP/1.1" 500 66119

It tells me that I got 1 missing required positional argument, the request.

enter image description here

But that argument was passed in the code, I'll share you my code.

The Custom User Model:

from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager

# choices
CALIFICACION_USUARIO_CHOICES = [
    (0,0),
    (1,1),
    (2,2),
    (3,3),
    (4,4)
]

# Create your models here.
class UsuarioManager(BaseUserManager):
    use_in_migrations = True

    def _create_user(self, email, password, **extra_fields):
        if not email:
            raise ValueError('El usuario debe tener un correo electrónico')
        
        email = self.normalize_email(email)
        usuario = self.model(email=email, **extra_fields)
        usuario.set_password(password)
        usuario.save(using=self._db)

        return usuario


    def create_user(self, email=None, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(email, password, **extra_fields)
    
    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superusuario debe tener la propiedad "is_staff=True"')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superusuario debe tener la propiedad "is_superuser=True"')
        
        return self._create_user(email, password, **extra_fields)

class Usuario(AbstractBaseUser):
    first_name = models.CharField(verbose_name='Nombre', max_length=255)
    last_name = models.CharField(verbose_name='Apellidos', max_length=255, blank=True, null=True)
    email = models.EmailField(verbose_name='Correo', unique=True, max_length=255)
    cedula = models.CharField(verbose_name='Cédula de Identidad', null=False, blank=False, max_length=255)
    telefono = models.CharField(verbose_name='Teléfono', max_length=255, null=False, blank=False)
    calificacion = models.SmallIntegerField(verbose_name='Calificación', null=False, blank=False, choices=CALIFICACION_USUARIO_CHOICES, default=0)
    direccion = models.TextField(verbose_name='Dirección', null=True, blank=True)
    foto_perfil = models.ImageField(verbose_name='Foto de perfil', max_length=255, null=True, blank=True, upload_to='perfil/')
    terminos_y_condiciones = models.BooleanField(verbose_name='Términos y condiciones', default=False)
    is_active = models.BooleanField(verbose_name='active',default=True)
    is_staff = models.BooleanField(verbose_name='staff status', default=False)
    is_superuser = models.BooleanField(verbose_name='superuser status', default=False)

    objects = UsuarioManager()

    USERNAME_FIELD = 'email'

    def __str__(self):
        return f'{self.first_name} {self.last_name}'
    
    def has_perm(self,perm,obj=None):
        return True
    
    def has_module_perms(self,app_label):
        return True
    
    def get_email_user(self):
        return self.email.lower()
    
    def save(self, *args, **kwargs):
        self.email = self.email.lower()
        return super(Usuario, self).save(*args, **kwargs)
    
    class Meta:
        verbose_name = 'Usuario'
        verbose_name_plural = 'Usuarios'
        ordering = ['-id']

The custom backend code:

from django.contrib.auth.backends import ModelBackend
from authentication.models import Usuario

class UsuarioAuthBackend(ModelBackend):
     def authenticate(self, request, **kwargs):
          email = kwargs['email']
          password = kwargs['password']

          try:
               usuario = Usuario.objects.get(email=email)
               if usuario.check_password(password) is True:
                    return usuario
          except Usuario.DoesNotExist:
               return None

The view that use this custom backend:

from django.shortcuts import render, redirect
from django.contrib.auth import login, logout
from authentication.authentication import UsuarioAuthBackend
from authentication.forms import FormUsuario

# Create your views here.
def login(request):
    if request.method == 'POST':
        email = request.POST['email']
        password = request.POST['password']

        usuario = UsuarioAuthBackend.authenticate(request, email=email, password=password)

        if usuario is not None:
            login(request, usuario)
            return redirect('home')
        
    return render(request, 'login.html')

I added in my settings.py these lines:

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'authentication.authentication.UsuarioAuthBackend'
]

If anyone can help me to solve this problem, I will be really grateful!!


Solution

  • You define authenticate as an instance method, but use it as a classmethod, in other words, you are using a method of an uninitialized class.

    Try the following:

    class A:
        def example(self, test):
            print(test)
    
    A.example('hello')
    

    You will get a TypeError. Now try the following:

    b = A()
    b.example('hello')
    

    You are supposed to import the authenticate function, like stated in the docs: 1. import from contrib and 2. calling authenticate

    Update:

    This is what you do:

    from authentication.authentication import UsuarioAuthBackend
    ...
    usuario = UsuarioAuthBackend.authenticate(request, email=email, password=password)
    

    This is bad. Read the documentation:

    Behind the scenes, Django maintains a list of “authentication backends” that it checks for authentication. When somebody calls django.contrib.auth.authenticate() – as described in How to log a user in – Django tries authenticating across all of its authentication backends. If the first authentication method fails, Django tries the second one, and so on, until all backends have been attempted. ... The order of AUTHENTICATION_BACKENDS matters, so if the same username and password is valid in multiple backends, Django will stop processing at the first positive match.

    You shold do this instead:

    from django.contrib.auth import authenticate
    ...
    usuario = authenticate(request, email=email, password=password)