I've spent many days diving into the ins and outs of the user model and the authentication, and to save some time for you and others, here is what I have learned.📚🙇♂️
The Django documentation strongly recommends that we use a custom user model from the very start, even before we run the first migration. This practically means that once we create the application using the Django CLI, we don't run the app, but replace the user model immediately. From my personal experience, it will cause migration conflicts if you want to change the user model halfway through the project, but you will not have problems if you run the initial migrations on a newly created project and then change the model.
That being said, let's walk through the following:
then let's introduce the two options to tailor the user model:
To get the most out of this article, have your IDE ready and open the built-in Django classes mentioned later
Built-in User Model
In your Django app, you will have to create a superuser to be able to log in to the admin page using the python manage.py createsuperuser
command. You will be prompted to provide a username, first name, last name, and password(twice).
If you are like me, you might already have a problem with this. Why on Earth would I come up with a unique username and password combination if I can barely remember my girlfriend's phone number?😅 There are a couple of good reasons actually. It is safer to use a username than an email since if the combination lands in the wrong hands, many people would have sleepless nights due to reused passwords at other websites. It is not possible to recover your password if your application is breached, since Django saves hashed passwords only(random salt is used as well). However, a user can land on a fake copy of your login page and can reveal their email and password there. Another use case is for community-centered websites like dev.to because you might not want to use your email/name for commenting or posting.
User Model
Let's take a look into the DB to understand the default user model. If you were to apply migrations on your newly created app(you don't have to), then many authentication-related tables would be created including the auth_user
. Here is the creation script for the auth_user
table:
create table auth_user
(
id integer not null primary key autoincrement,
password varchar(128) not null,
last_login datetime,
is_superuser bool not null,
username varchar(150) not null unique,
last_name varchar(150) not null,
email varchar(254) not null,
is_staff bool not null,
is_active bool not null,
date_joined datetime not null,
first_name varchar(150) not null
);
A couple of things that we can verify is that the username is indeed a unique column and the user already has a required email address.
Note: The same user model is used for the superuser, staff, and regular users, however, the model has properties to differentiate between these user types.
The built-in user model, User(AbstractUser)
, doesn't implement much on its own, but it extends the AbstractUser(AbstractBaseUser, PermissionsMixin)
, which defines most of the fields that we saw in the DB. The password field and the related saving/verification logic live in the AbstractBaseUser
, while the permission-related fields and logic, like verifying whether a user can log into the admin site, are defined in the PermissionsMixin
.
Since it is not advised by the Django documentation to reinvent password management, we should at least extend the AbstractBaseUser
in our custom user model. If we do, then we don't need to implement anything password-related and it will work properly.
ModelManager
Every Django model comes with a ModelManager
that is assigned to the model's objects
property by default. The model manager provides DB capabilities to the specific model, which allows us, for example, to call YourModel.objects.all()
.
The default manager provides the APIs for the most common use cases, however, sometimes you need custom logic to make the desired change in the DB. This is the case for the AbstractUser
as well, and it uses the UserManager
implementation to communicate with the DB. The UserManager
implements the create user/superuser methods to hash the received password, normalizes(lowercase) the username and email, and sets extra properties for superusers before saving the user instance to the DB. It also provides a method to look up users with certain permissions.
Authentication Process
Let's create a superuser and log in to the admin site while walking through the code.
Creating a Superuser
When you execute python manage.py createsuperuser
, you will be prompted to provide information about the user. What you have to provide depends on the User
model, specifically the User.USERNAME_FIELD
, User.PASSWORD_FIELD
, and the fields defined in User.REQUIRED_FIELDS
(the email field is added to the REQUIRED_FIELDS
in the AbstractUser
model).
If you are interested in why the above fields are required and how the superuser creation is implemented, you can dig deeper in this file django/contrib/auth/management/commands/createsuperuser.py
.
Once you created a user you can run the server(python manage.py runserver
) and open the login page(localhost:8000/admin/login
).
Login Page
What page is rendered at this point is defined in the AdminSite
(django/contrib/admin/sites.py). In the login
function, the authentication form and the template name are defined to be AdminAuthenticationForm
and admin/login.html
(django/contrib/admin/templates) respectively. The form and template settings are passed into a LoginView
instance which is returned and serves the request at the end.
When you fill out the fields and submit the form, the LoginView
will take care of the authentication and it will redirect you to the admin landing page after successful login.
User Creation Logic on the Admin Page
ModelAdmin
The models listed after logging in are the models that are registered to the admin site. You can register a model by either calling
admin.site.register(YourModel, YourModelAdmin)
or decorating an admin model with
@admin.register(YourModel)
class YourModelAdmin(admin.ModelAdmin):
pass
If you use the former approach, the YourModelAdmin
class is an optional parameter, and if it is omitted, a default will be added by Django. The YourModelAdmin
is used to set how you can interact with the model on the admin page, so you can restrict the available fields, override the forms used to create/edit model instances, and set the templates used to display these forms.
When you click on Add users, a new page will be loaded that is configured by the UserAdmin(admin.ModelAdmin)
, which is registered to the default User
model. In the UserAdmin
, you can find the forms and templates used to render the pages related to the User
model.
Note: The forms used in the
UserAdmin
model extend the forms.ModelForm which is a convenience model in Django. It enables automatic form field generation for the model specified in theMeta
class of the form class.
UserCreationForm
After filling out the form and submitting it, the UserAdmin
calls the save method(defined in ModelAdmin
) of the UserCreationForm
with the commit=False
parameter. This is important because the form will not automatically save the User
instance to the DB, but instead, it will hash the raw password and create an object in memory. Once this object is returned to the UserAdmin
, the save method of the user object(defined in AbstractBaseUser.save
), will be called which stores the user instance to the DB. This flow provides the possibility to customize the user saving mechanism since we can easily override the AbstractBaseUser.save
method in the User
model.
Now that we understand how users are created in detail, let's implement our custom user model.👷♀️🏗️
CustomUser(AbstractUser) Implementation
In this series we will develop an e-learning platform, therefore, I want to make registration as simple as possible so we will use the email/password combination in our user model.
Create 'users' App
Most of the time you will want to override the AstractUser
because many functionality will be provided to you and you will still be able to set your custom fields including replacing the username with an email.
Let's create a new app called users
python manage.py startapp users
Note: You could create the folder structure manually, but you will potentially forget to create the migrations folder(or others) and then when we have models, the migration command will not include your app, unless you include the app name in the command. So, just use the command above to make your life easier.
You can check your structure against this commit. 📏 ✍️
Then add the users
app to the top of the INTALLED_APPS
list.
# e_learning/settings.py
...
INSTALLED_APPS = [
"users", # users app added
"django.contrib.admin",
...
]
...
Create CustomUser
Now let's create our CustomUser
model, which uses the email for the USERNAME_FIELD
and it also requires a date of birth.
# users/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import gettext_lazy as _
class CustomUser(AbstractUser):
"""
Custom user model which doesn't have a username,
but has a unique email and a date_of_birth.
This model is used for both superusers and
regular users as well.
"""
# The inherited field 'username' is nullified, so it will
# neither become a DB column nor will it be required.
username = None
email = models.EmailField(_("email address"), unique=True)
date_of_birth = models.DateField(
verbose_name="Birthday",
null=True
)
# Set up the email field as the unique identifier for users.
# This has nothing to do with the username
# that we nullified above.
USERNAME_FIELD = "email"
REQUIRED_FIELDS = [
"first_name",
"last_name",
"date_of_birth",
] # The USERNAME_FIELD aka 'email' cannot be included here
objects = CustomUserManager()
def __str__(self):
return self.email
To better understand the above class, I advise you to open the AbstractUser
class and examine the code. You can see that the username
is defined here as a CharField
, therefore if we want to remove it from our model, we have to set it to None
.
The email
field is defined as well, but notice that in the AbstractUser
, it can be blank. We want the email to be the unique identifier so we override this property with the unique=True
parameter.
The other inherited fields like first_name
, is_active
etc. are quite useful, so we don't touch them in the CustomUser
class.
The date of birth of the user is also useful information, so I added that field under the email.
Next up, we have to set the USERNAME_FIELD="email"
because during authentication or form rendering the value of the USERNAME_FIELD
will be used. Django could have called it 'unique identifier' as well because it doesn't have anything to do with the original username
field that was assigned to it.
On the following line, we override the REQUIRED_FIELDS
. The fields included here will be required in related forms. Notice that the email
and the password
fields are not added. This is because the USERNAME_FIELD
and the password are added by default on a deeper layer.
In the next line, we set objects=CustomUserManager()
because we will have to override the user creation methods.
Create CustomUserManager
Let's define the CustomUserManager
for the CustomUser
.
# users/models.py
from django.contrib.auth.base_user import BaseUserManager
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import gettext_lazy as _
class CustomUserManager(BaseUserManager):
"""
Defines how the User(or the model to which attached)
will create users and superusers.
"""
def create_user(
self,
email,
password,
date_of_birth,
**extra_fields
):
"""
Create and save a user with the given email, password,
and date_of_birth.
"""
if not email:
raise ValueError(_("The Email must be set"))
email = self.normalize_email(email) # lowercase the domain
user = self.model(
date_of_birth=date_of_birth,
email=email,
**extra_fields
)
user.set_password(password) # hash raw password and set
user.save()
return user
def create_superuser(
self,
email,
password,
date_of_birth,
**extra_fields
):
"""
Create and save a superuser with the given email,
password, and date_of_birth. Extra fields are added
to indicate that the user is staff, active, and indeed
a superuser.
"""
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
extra_fields.setdefault("is_active", 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,
date_of_birth,
**extra_fields
)
We inherit from the BaseUserManager
so that we have the normalize_email
method and other base methods from the Manager
class to interact with the DB. We defined additional methods to create users which are used by the createsuperuser
command, and we can also use them later in the application. We have to use the CustomUserManager
in the CustomUser
class because the inherited manager relies on the username
field to create a user and we had to replace that with the email
field.
Display User Model on the Admin Page
Now, let's define the CustomUserAdmin
to be able to customize how the user instances are displayed on the admin site.
# users/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from users.forms import CustomUserChangeForm, CustomUserCreationForm
from users.models import CustomUser
class CustomUserAdmin(UserAdmin):
model = CustomUser
list_display = (
"email",
"first_name",
"last_name",
"date_of_birth",
"is_staff",
"is_active",
)
list_filter = (
"email",
"first_name",
"last_name",
"date_of_birth",
"is_staff",
"is_active",
)
fieldsets = (
(None, {"fields": (
"first_name",
"last_name",
"email",
"password",
"date_of_birth")}
),
("Permissions", {"fields": (
"is_staff",
"is_active",
"groups",
"user_permissions")}
),
)
add_fieldsets = (
( None, {"fields": (
"first_name",
"last_name",
"email",
"password1",
"password2",
"date_of_birth",
"is_staff",
"is_active",
"groups",
"user_permissions")}
),
)
search_fields = ("email",)
ordering = ("email",)
The list_display
, list_filter
, search_fields
, and ordering
properties specify what you see on the admin site when you click the 'Users' link.
The fieldsets
and add_fieldsets
properties determine which properties are shown when you open the user details and when you add a user respectively. You can also group the fields like how we did for the 'Permissions'.
You can check the status of the project using this commit. 📏 ✍️
User Creation and Edition Forms
According to the documentation, we have to override the UserCreationForm
and the UserChangeForm
if we use a custom user model, however, based on my experience it is not needed if we extend the AbstractUser
. In both forms, the Meta
class sets the model to be User
:
class UserChangeForm(forms.ModelForm):
...
class Meta:
model = User
fields = "__all__" # for UserCreationForm -> fields = ("username",)
field_classes = {"username": UsernameField}
However, when we add a print statement in the form init method,
print("UserChangeForm: ", self.Meta.model)
then the output is
UserChangeForm: <class 'users.models.CustomUser'>
, so the model is swapped to our custom model in the background.
It is still a good idea to follow the docs, so here is the required implementation.
# users/forms.py
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from users.models import CustomUser
class CustomUserCreationForm(UserCreationForm):
"""
Specify the user model created while adding a user
on the admin page.
"""
class Meta:
model = CustomUser
fields = [
"first_name",
"last_name",
"email",
"password",
"date_of_birth",
"is_staff",
"is_active",
"groups",
"user_permissions"
]
class CustomUserChangeForm(UserChangeForm):
"""
Specify the user model edited while editing a user on the
admin page.
"""
class Meta:
model = CustomUser
fields = [
"first_name",
"last_name",
"email",
"password",
"date_of_birth",
"is_staff",
"is_active",
"groups",
"user_permissions"
]
Here we override the model
and the fields
properties in the Meta
class. The model
is used to render the forms in the UI, while the fields
property specifies which model fields are going to be available for the form. You must specify the required fields because otherwise all fields will be rendered automatically. This can cause a security issue when you later add an internal field to the model that you don't want to expose to the user. So, it is highly recommended to explicitly include the allowed fields.
Note: If you exclude a field in the
Meta
class, but you include that field in theCustomUserAdmin
in one of the properties likefieldsets
, then you will receive an error since the admin model expects a field that is not accessible in the model.
Now let's tell the admin model to use these forms:
# users/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from users.forms import CustomUserChangeForm, CustomUserCreationForm
from users.models import CustomUser
class CustomUserAdmin(UserAdmin):
add_form = CustomUserCreationForm
form = CustomUserChangeForm
...
Lastly, register your CustomUser
model so that it shows up on the admin page, and include the CustomUserAdmin
to override the default display.
"""
Register the 'CustomUser' model to the admin site, so that new
'CustomUser' model instances can be added to the page.
The 'CustomUserAdmin' determines which fields and how those
fields show up in the editor.
If the 'CustomUserAdmin' is left out, a 'ModelAdmin' is added
which will include all model fields on the editor page.
"""
admin.site.register(CustomUser, CustomUserAdmin)
Now you can make and run migrations
python manage.py makemigrations
python manage.py migrate
to create the users_customuser
table with the following definition:
create table users_customuser
(
id integer not null
primary key autoincrement,
password varchar(128) not null,
last_login datetime,
is_superuser bool not null,
first_name varchar(150) not null,
last_name varchar(150) not null,
is_staff bool not null,
is_active bool not null,
date_joined datetime not null,
email varchar(254) not null unique,
date_of_birth date
);
You can check out the project at this stage with this git commit. 📏 ✍️
Customizing the Login Page
Since we already come this far, let's cover how we can customize the admin login page. The code required for our use case is minimal, however, understanding how it works is a bit more involved.
In short, the AdminSite
has a login
method that handles the request to the localhost:8000/admin/login
path. By default, the built-in AdminAuthenticationForm
and the django/contrib/admin/templates/admin/login.html
template are used to render the page. The AdminAuthenticationForm
specifies what fields are available for the template, while the template specifies how those fields are shown to the user.
The AdminAuthenticationForm
inherits the actual field definitions from its parent class, AuthenticationForm
. If you open the parent class, you will notice that a username
however in the init
method that field is overridden by the USERNAME_FIELD
of our custom user model. Thus, the username
property will hold our email field. Therefore, it shouldn't be surprising that if you open the login.html
you will see form.username
, which will render our email field as a text input.
To enable client-side validation and increase the accessibility of the email field, we should render the input field with the email type.
We can create a CustomAdminAuthenticationForm
that addresses this issue:
class CustomAdminAuthenticationForm(AuthenticationForm):
username = UsernameField(widget=forms.EmailInput(attrs={"autofocus": True}))
Notice that the widget is using EmailInput
instead of TextInput
which will solve our rendering problem.
Next up let's create our template, login.html
, in the users/template/authenticate
folder. We can place here a copy of the original file(django/contrib/admin/templates/admin/login.html
). The only thing that we have to update is the path to the CSS file.
<-- users/templates/authentication/login.html -->
...
<link rel="stylesheet" href="{% static "css/login.css" %}">
...
Since we are using the static
tag, Django will look into the folder holding static code, which is called 'static' by default. Let's put a copy of the original CSS file(django/contrib/admin/static/admin/css/login.css
) into users/static/css/login.html
. (You will have to create this folder structure)
Now let's put the pieces together in the settings.py
file and update the URLs.
# settings.py
...
urlpatterns = [
path('admin/login/', LoginView.as_view(
authentication_form=CustomAdminAuthenticationForm,
template_name="authentication/login.html"),
name='admin_login'), # this path was added
path("admin/", admin.site.urls),
]
We added a new binding to the path admin/login/
, which overrides the Django implementation. The LoginView
is a built-in view that will display the login form and handle the login action. At this point, we can specify to use our own CustomAdminAuthenticationForm
, and pass in the path to our template.
Note: The default template path is
registration/login.html
, therefore, if we renamedauthentication
toregistration
we wouldn't need to pass in the template path. However, I want to demonstrate how we can specify a template in a custom location.
You can view the project at this status using this commit. 📏 ✍️
CustomUser(AbstractBaseUser) Implementation
If you prefer a bare-bone user model, then you might prefer replacing the AbstractUser
and inheriting straight from the AbstractBaseUser
. In this case, everything that we discussed in the previous section still holds. The only difference is the implementation of the CustomUserModel
and if the user fields are different, then we also have to update the references.
# users/models.py
from django.contrib.auth.base_user import BaseUserManager, AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.core.mail import send_mail
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
...
class CustomUser(AbstractBaseUser, PermissionsMixin):
"""
Custom user model with unique email as the user
identifier. This model is used for both superusers and
regular users as well.
"""
# The inherited field 'username' is nullified, so it will
# neither become a DB column nor will it be required.
email = models.EmailField(_("email address"), unique=True)
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
date_joined = models.DateTimeField(default=timezone.now)
date_of_birth = models.DateField(
verbose_name="Birthday",
null=True
)
# Set up the email field as the unique identifier for users.
# This has nothing to do with the username that we nullified.
USERNAME_FIELD = "email"
# The USERNAME_FIELD aka 'email' cannot be included here
REQUIRED_FIELDS = ["date_of_birth"]
objects = CustomUserManager()
def __str__(self):
return self.email
class Meta:
verbose_name = _("user")
verbose_name_plural = _("users")
abstract = False
def clean(self):
super().clean()
self.email =
self.__class__.objects.normalize_email(self.email)
def get_date_of_birth(self): # This method is optional
return self.date_of_birth
def email_user(
self,
subject,
message,
from_email=None,
**kwargs):
"""Email this user."""
send_mail(
subject,
message,
from_email,
[self.email],
**kwargs
)
Notice that now we have to inherit from the PermissionsMixin
class as well to preserve Django's permissions functionality.
Regarding the fields, we have to keep the is_staff
,
and is_active
fields to preserve functionality. I removed the first and last name as well as the username fields since we don't need this information anymore, however, I kept the date of birth and the email fields.
As you can see the USERNAME_FIELD
is untouched, but I had to update the REQUIRED_FIELDS
.
The Meta
class, clean
, and send_email
methods are copied from the AbstractUser
to keep functionality.
Now let's clean up the references to the first and last names from the CustomUserAdmin
:
# users/admin.py
class CustomUserAdmin(UserAdmin):
add_form = CustomUserCreationForm
form = CustomUserChangeForm
list_display = (
"email",
"date_of_birth",
"is_staff",
"is_active",
)
list_filter = (
"email",
"date_of_birth",
"is_staff",
"is_active",
)
fieldsets = (
(None, {"fields": (
"email",
"password",
"date_of_birth")}
),
("Permissions", {"fields": (
"is_staff",
"is_active",
"groups",
"user_permissions")}
),
)
add_fieldsets = (
(None, {"fields": (
"email",
"password1",
"password2",
"date_of_birth",
"is_staff",
"is_active",
"groups",
"user_permissions")}
),
)
search_fields = ("email",)
ordering = ("email",)
...
and from the CustomUserCreationForm
and CustomUserChangeForm
# users/forms.py
...
class CustomUserCreationForm(UserCreationForm):
"""
Specify the user model/form to be used while
adding a user on the admin page.
"""
class Meta:
model = CustomUser
fields = [
"email",
"password",
"date_of_birth",
"is_staff",
"is_active",
"groups",
"user_permissions",
]
class CustomUserChangeForm(UserChangeForm):
"""
Specify the user model/form to be used while editing
a user on the admin page.
"""
class Meta:
model = CustomUser
fields = [
"email",
"password",
"date_of_birth",
"is_staff",
"is_active",
"groups",
"user_permissions",
]
You can view the project at this status using this commit. 📏 ✍️
And that's it! Now pat yourself on the back! 🎉 🔥
I hope you enjoyed the article and that you now understand the intricacies of the user model and the related topics. If you have any questions or suggestions, leave a comment below or contact me through LinkedIn. If you liked the content, don't forget to share it with others. If you would like to fuel my next study sessions, then you are welcome to sponsor my next bubble tea😁🧋
Top comments (0)