Authentication & Custom User Model In Django

Authentication & Custom User Model In Django

Today, we will talk about the User model in Django.

One of the most beautiful things about Django, frankly, is the authentication. Django literally handles everything for you while you're sitting back and relaxing.

However, sometimes we need to make modifications to the user model. For instance, you might want to add a field like a profile photo or implement login using an email instead of a username.

So, how do we go about it? We have four ways to do this:

1- Using a proxy model:

We use this when we don't want to manipulate the database. We don't want to create a new table or add new fields.

All we want here is to modify things like ordering or add new methods. For example:

from django.contrib.auth.models import User

class Person(User):
    def say_hello(self):
        return f'Hello, {self.first_name}!'

    class Meta:
        proxy = True
        ordering = ('first_name', )

2- One-To-One relationship between User & Profile:

In this case, you have two tables: the first is the Django User table, and the second is the Profile table that you create.

In this scenario, you add all the fields you need in your new table without touching the Django User at all. Let's add a few fields to the Profile:

from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver

# Create your models here.
class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    about = models.TextField(null=True, blank=True)
    dob = models.DateField(null=True, blank=True)
    def __str__(self):
        return self.user.username

@receiver (post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        UserProfile.objects.create(user=instance)

@receiver (post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.userprofile.save()

Simple, right? A new model with a few signals, and there you have created the Profile.

Now, let's see how we can modify the Profile:

user = User.objects.get(id=user_id)
user.profile.about = "About Me"
user.save()

Indeed, be mindful when fetching users because you might run into a significant issue, the N+1 problem. We can discuss it later, but its solution involves using select_related.

users = User.objects.all().select_related("userprofile")

3- Custom User Model Extending AbstractUser :

We have finally reached my favorite method.

So, when do we use AbstractUser? We use this approach when we want to add new fields to the User model and, at the same time, don't want to create a new table.

Let's see how:

from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    about = models.TextField(null=True, blank=True)
    dob = models.DateField(null=True, blank=True)

This way, we have created the model that should be added to the settings.py file:

AUTH_USER_MODEL = "authentication.User"

A small note: It is advisable, if you are going to create a relation with any model and the User, not to use the User directly

  • Do not use:

      from authentication.models import User
    
      class Book(models.Model):
          auther = models.ForeignKey(User, on_delete=models.CASCADE)
    
  • Use this instead:

      from django.conf import settings
    
      class Book(models.Model):
          auther = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    
  • Or This:

      from django.contrib.auth import get_user_model()
    
      class Book(models.Model):
          auther = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
    

4- Custom User Model Extending AbstractBaseUser :

You use this method when you want to add new fields to the User model and, at the same time, you don't want to create a new table.

Oh... oh!! So, what is the difference between AbstractBaseUser and AbstractUser? Well, my friend, they are quite similar, but there is, of course, a difference.

  • AbstractUser: Here, you can add new fields, but you want to use Django's authentication system without modifying it. In other words, you like the way things are, but you just want to add a few fields to go with your flow. 🙂

  • AbstractBaseUser: Now, here, you are not satisfied with something, and you want to modify the authentication process. For example, you might want to implement login with an email instead of a username. This is a bit trickier and something I'd usually resort to only if absolutely necessary because it involves more complexities. So, you wouldn't choose this unless you really need to, like if you want to change the login field from a username to an email, for instance.

Let's give it a try, shall we? 🤷

First thing, we need to create a Custom User Manager:

from django.contrib.auth.models import BaseUserManager


class CustomUserManager(BaseUserManager):
    def create_user(self, email, username, password=None, **extra_fields):
        if not email:
            raise ValueError('The Email field must be set')
        email = self.normalize_email(email)
        user = self.model(email=email, username=username, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, username, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)

        return self.create_user(email, username, password, **extra_fields)

And after that, we will create our User Model:

from django.db import models
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.utils import timezone
from .managers import CustomUserManager


class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True)
    username = models.CharField(max_length=30, unique=True)
    first_name = models.CharField(max_length=30, blank=True)
    last_name = models.CharField(max_length=30, blank=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    is_superuser = models.BooleanField(default=False)
    date_joined = models.DateTimeField(default=timezone.now)

    # Add your custom fields here

    objects = CustomUserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username']

    def __str__(self):
        return self.email

Let's explain the important things in the Model:

  • USERNAME_FIELD: This is the unique identifier that Django will use for login. It must have unique=True.

  • REQUIRED_FIELDS: This is a set of fields that will be required when creating a new user from the terminal using the createsuperuser command.

Of course, don't forget to add the User model in the settings.py:

AUTH_USER_MODEL = "authentication.User"

Also, don't forget that when there's a relation between the User and any other model, you should retrieve the User model in the best way:

  • Do not use:

      from authentication.models import User
    
      class Book(models.Model):
          auther = models.ForeignKey(User, on_delete=models.CASCADE)
    
  • Use this instead:

      from django.conf import settings
    
      class Book(models.Model):
          auther = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    
  • Or This:

      from django.contrib.auth import get_user_model()
    
      class Book(models.Model):
          auther = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
    

A final note: We should use AbstractUser or AbstractBaseUser when starting the project to avoid complications and data loss.

That's it! 🤷‍♂️

May I have succeeded in explaining the topic in a simple way. If you have any advice or corrections, I would be very grateful."