This website is made possible by displaying online advertisements to our visitors.
Please consider supporting us by disabling your ad blocker. Thank you for your support.
This website is made possible by displaying online advertisements to our visitors.
Please consider supporting us by disabling your ad blocker.

Django: Custom User Model Extending AbstractUser

May 23 2021 Yacine Rouizi
Authentication Django
Django: Custom User Model Extending AbstractUser

Introduction

The authentication system that comes with Django works fine for most cases, but it is recommended to use a custom user model with every project you start as it just requires a bit of configuration, and you'll be able to customize it as you see fit.

When you want to extend the built-in user model you can either substitute it with your own user model or stay with the original one.

If you don't want to use your own user model, then you have 2 options:

  1. You can use a proxy model.
  2. You can create an OneToOneField relationship with the built-in User Model.

On the other hand, if you want to create your own user model you also have 2 options:

  1. Make your custom user model a subclass of the AbstractBaseUser model.
  2. Make your custom user model a subclass of the AbstractUser model.

Note that the AbstractUser model inherits from the AbstractBaseUser model, so if you want a custom user model, but you want a base to start from, go with the AbstractUser model.

On the other hand, sub-classing the AbstractBaseUser model will give you full control over the user model, but you'll have to start from scratch (redefine username, email, is_staff, is_active, is_superuser ... fields, and so on). Generally, you don't need that much freedom.

In this tutorial, we will see how to make our user model inherit from the AbstractUser model to make the email address the unique identifier instead of the username.

If you are looking for the source code to this post, you can find it on GitHub at: https://github.com/Rouizi/django-custom-user-model

So let's get started.

Project Setup

Let's first create a new Django project by running the following commands:

$ mkdir mysite && cd $_
$ python3 -m venv venv
$ source venv/bin/activate

(venv)$ pip install django
(venv)$ django-admin startproject app .
(venv)$ python manage.py startapp users

 We need to update the settings.py file to include the users app and override the default user model by pointing to our custom user model in the  AUTH_USER_MODEL setting:

# app/settings.py
# ...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'users', # Add this
]
# ...
AUTH_USER_MODEL = 'users.CustomUser' # Add this

Now we need to add our CustomUser model in the models.py file:

# users/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):
    pass # For now we do nothinng

    def __str__(self):
        return self.username

Note that we have to wait at least until this step to run the migrations (until we create our own user model).

But why?

Well, to put it simply, this is due to the fact that the first migration has a dependency on the user model, and you can run into a circular dependency.

As per the documentation:

Due to limitations of Django’s dynamic dependency feature for swappable models, the model referenced by AUTH_USER_MODEL must be created in the first migration of its app (usually called 0001_initial); otherwise, you’ll have dependency issues.

Below, you can see what the first migration looks like when using the built-in user model:

# 0001_initial.py
class Migration(migrations.Migration):

    initial = True

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]
    # ...

and when using a custom user model:

# 0001_initial.py
class Migration(migrations.Migration):

    initial = True

    dependencies = [
        ('auth', '0012_alter_user_first_name_max_length'),
    ]
    # ...

Hopefully, now you have a better understanding of what's going on under the hood. So let's run the migrations:

(venv)$ python manage.py makemigrations users
Migrations for 'users':
  users/migrations/0001_initial.py
    - Create model CustomUser

(venv)$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, users
Running migrations:
  Applying contenttypes.0001_initial... OK
...

Custom Manager Extending BaseUserManager

Since our user model defines a different field (we are going to make the email field unique to be able to use it as the unique identifier), we have to define a custom manager that extends BaseUserManager providing it two additional methods: create_user and create_superuser:

# users/models.py
from django.contrib.auth.base_user import BaseUserManager

class CustomUserManager(BaseUserManager):

    def _create_user(self, email, password, **extra_fields):
        """
        Create and save a User with the given email and password.
        """
        if not email:
            raise ValueError("The given email must be set")
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, email, password=None, **extra_fields):
        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(
                "Superuser must have is_staff=True."
            )
        if extra_fields.get("is_superuser") is not True:
            raise ValueError(
                "Superuser must have is_superuser=True."
            )

        return self._create_user(email, password, **extra_fields)

User Model Extending AbstractUser

Before customizing our user model, let's take a look at the source code for the AbstractUser class:

# django/contrib/auth/models.py
# ...
class AbstractUser(AbstractBaseUser, PermissionsMixin):
    username_validator = UnicodeUsernameValidator()

    username = models.CharField(
        _('username'),
        max_length=150,
        unique=True,
        help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
        validators=[username_validator],
        error_messages={
            'unique': _("A user with that username already exists."),
        },
    )
    first_name = models.CharField(_('first name'), max_length=150, blank=True)
    last_name = models.CharField(_('last name'), max_length=150, blank=True)
    email = models.EmailField(_('email address'), blank=True)
    is_staff = models.BooleanField(
        _('staff status'),
        default=False,
        help_text=_('Designates whether the user can log into this admin site.'),
    )
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'
        ),
    )
    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)

    objects = UserManager()

    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email']
    # ...
# ...

By sub-classing this class, there a few things we want to do:

  1. Make the email field unique.
  2. Use our CustomUserManager.
  3. The USERNAME_FIELD should be equal to 'email' (this represents the name of the field that is used as the unique identifier).
  4. The REQUIRED_FIELDS should be equal to ['username'] because this variable represents field names that will be prompted for when creating a user via the createsuperuser management command.

So here is how we would subclass the AbstractUser class:

# users/models.py
# ...
class CustomUser(AbstractUser):
    email = models.EmailField("email address", unique=True)

    USERNAME_FIELD = "email" # make the user log in with the email
    REQUIRED_FIELDS = ["username"]

    objects = CustomUserManager()

We need to run the migrations again since we edited the model:

(venv)$ python manage.py makemigrations
(venv)$ python manage.py migrate

Sign Up and Log In Forms

UserCreationForm and UserChangeForm need to be extended as well to use our custom user model. Here I am not going to extend the UserChangeForm because we don't need it in this tutorial (This form is used in the admin to change a user’s information).

So let's first create the forms.py file:

(venv)$ touch users/forms.py

and add the following code:

# users/forms.py
from django.contrib.auth.forms import UserCreationForm
from django import forms

from .models import CustomUser

class SignUpForm(UserCreationForm):
    class Meta:
        model = CustomUser
        fields = ("username", "email")

class LogInForm(forms.Form):
    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)

Custom User Admin

We also need to register our custom user model with the admin:

# users/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from .models import CustomUser

class CustomUserAdmin(UserAdmin):
    model = CustomUser
    list_display = ['email', 'username', 'first_name', 'last_name', 'is_staff']

admin.site.register(CustomUser, CustomUserAdmin)

Create a Basic Authentication System

In this last part, we are going to integrate our user model with the views and templates to have a fully working authentication functionality.

Here is what goes in the views.py file:

# users/views.py
from django.shortcuts import render, redirect
from django.contrib.auth import login, authenticate, logout
from django.urls import reverse

from .forms import SignUpForm, LogInForm

def signup(request):
    if request.method == 'POST':
        form = SignUpForm(request.POST)
        if form.is_valid():
            user = form.save()
            login(request, user)
            return redirect('home')
    else:
        form = SignUpForm()   
    return render(request, 'users/signup.html', {'form': form})


def log_in(request):
    error = False
    if request.user.is_authenticated:
        return redirect('home')
    if request.method == "POST":
        form = LogInForm(request.POST)
        if form.is_valid():
            email = form.cleaned_data["email"]
            password = form.cleaned_data["password"]
            user = authenticate(email=email, password=password)
            if user:
                login(request, user)  
                return redirect('home')
            else:
                error = True
    else:
        form = LogInForm()

    return render(request, 'users/login.html', {'form': form, 'error': error})


def log_out(request):
    logout(request)
    return redirect(reverse('users:login'))

Now, open the app/urls.py file and include the users app and the home page:

# app/urls.py
from django.contrib import admin
from django.urls import path, include # Add this
from django.views.generic.base import TemplateView # Add this

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', TemplateView.as_view(template_name='home.html'), name='home'), # Add this
    path('users/', include(('users.urls', 'users'), namespace='users')) # Add this
]

Create the urls.py inside the users app:

(venv)$ touch users/urls.py

 and add the following URLs:

# users/urls.py
from django.urls import path
from .views import signup, log_in, log_out

urlpatterns = [
    path('signup/', signup, name='signup'),
    path('login/', log_in, name='login'),
    path('logout/', log_out, name='logout'),
]

For the templates, we first need to edit the settings.py file to use the templates directory:

# app/settings.py
# ...
TEMPLATES = [
    {
        # ...
        'DIRS': [BASE_DIR / 'templates'],
        # ...
    },
]
# ...

Now let's create the templates directory and all the .html files:

mkdir -p templates/users
touch templates/users/login.html
touch templates/users/signup.html
touch templates/home.html
touch templates/base.html

and add the following HTML code:

<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Authentication system</title>
</head>
<body>
  {% block content %}
  {% endblock %}
</body>
</html>
<!-- templates/home.html -->
{% extends 'base.html' %}

{% block content %}
{% if user.is_authenticated %}
  Your are logged in {{ user.username }}!
  <p><a href="{% url 'users:logout' %}">Log Out</a></p>
{% else %}
  <p>You are not logged in</p>
  <a href="{% url 'users:login' %}">Log In</a> |
  <a href="{% url 'users:signup' %}">Sign Up</a>
{% endif %}
{% endblock %}
<!-- templates/users/signup.html -->
{% extends 'base.html' %}

{% block content %}
<h2>Sign Up</h2>
<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">Sign Up</button>
</form>
{% endblock content %}
<!-- templates/users/login.html -->
{% extends 'base.html' %}

{% block content %}
{% if error %}
<p><strong>Invalid email or password.</strong></p>
{% endif %}
<h2>Log In</h2>
<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">Log In</button>
</form>
{% endblock content %}

Below you can see the different pages of the authentication system:

home.html

home page

login.html

log in page

signup.html

sign up page

Redirection to the home page after log in:

home page after lo in

Further Reading

Summary

In this tutorial, we learned why it is recommended to use a custom user model and how to do it. You have now a fully functional authentication system that you can customize to meet your needs.

I hope this has given you a better understanding of the inner workings of Django's authentication system.

The final code is available on GitHub at: https://github.com/Rouizi/django-custom-user-model

Comments 2
Avatar Jalen S. said
Great tutorial, thank you.

July 31, 2022, 1:51 a.m.

Avatar Yacine said
Thank you @Jalen

July 31, 2022, 11:54 p.m.

Leave a comment

(Your email address will not be published)