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:
On the other hand, if you want to create your own user model you also have 2 options:
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.
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
...
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)
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:
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
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)
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)
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
login.html
signup.html
Redirection to the home page after log in:
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
Thank you @Jalen
July 31, 2022, 11:54 p.m.
July 31, 2022, 1:51 a.m.