The goal of this article is to discuss the caveats of the default Django user model implementation and also to give you some advice on how to address them. It is important to know the limitations of the current implementation so to avoid the most common pitfalls.
Something to keep in mind is that the Django user model is heavily based on its initial implementation that is at least 16 years old. Because user and authentication is a core part of the majority of the web applications using Django, most of its quirks persisted on the subsequent releases so to maintain backward compatibility.
The good news is that Django offers many ways to override and customize its default implementation so to fit your application needs. But some of those changes must be done right at the beginning of the project, otherwise it would be too much of a hassle to change the database structure after your application is in production.
Below, the topics that we are going to cover in this article:
- User Model Limitations
- The username field is case-sensitive
- The username field validates against unicode letters
- The email field is not unique
- The email field is not mandatory
- A user without password cannot initiate a password reset
- Swapping the default user model is very difficult after you created the initial migrations
- Detailed Solutions
- Conclusions
User Model Limitations
First, let’s explore the caveats and next we discuss the options.
The username field is case-sensitive
Even though the username
field is marked as unique, by default it is not case-sensitive. That means the username
john.doe
and John.doe
identifies two different users in your application.
This can be a security issue if your application has social aspects that builds around the username
providing a
public URL to a profile like Twitter, Instagram or GitHub for example.
It also delivers a poor user experience because people doesn’t expect that john.doe
is a different username than
John.Doe
, and if the user does not type the username exactly in the same way when they created their account, they
might be unable to log in to your application.
Possible Solutions:
- If you are using PostgreSQL, you can replace the username
CharField
with theCICharField
instead (which is case-insensitive) - You can override the method
get_by_natural_key
from theUserManager
to query the database usingiexact
- Create a custom authentication backend based on the
ModelBackend
implementation
The username field validates against unicode letters
This is not necessarily an issue, but it is important for you to understand what that means and what are the effects.
By default the username field accepts letters, numbers and the characters: @
, .
, +
, -
, and _
.
The catch here is on which letters it accepts.
For example, joão
would be a valid username. Similarly, Джон
or 約翰
would also be a valid username.
Django ships with two username validators: ASCIIUsernameValidator
and UnicodeUsernameValidator
. If the intended
behavior is to only accept letters from A-Z, you may want to switch the username validator to use ASCII letters only
by using the ASCIIUsernameValidator
.
Possible Solutions:
- Replace the default user model and change the username validator to
ASCIIUsernameValidator
- If you can’t replace the default user model, you can change the validator on the form you use to create/update the user
The email field is not unique
Multiple users can have the same email address associated with their account.
By default the email is used to recover a password. If there is more than one user with the same email address, the password reset will be initiated for all accounts and the user will receive an email for each active account.
It also may not be an issue but this will certainly make it impossible to offer the option to authenticate the user using the email address (like those sites that allow you to login with username or email address).
Possible Solutions:
- Replace the default user model using the
AbstractBaseUser
to define the email field from scratch - If you can’t replace the user model, enforce the validation on the forms used to create/update
The email field is not mandatory
By default the email field does not allow null
, however it allow blank
values, so it pretty much allows users to
not inform a email address.
Also, this may not be an issue for your application. But if you intend to allow users to log in with email it may be a good idea to enforce the registration of this field.
When using the built-in resources like user creation forms or when using model forms you need to pay attention to this detail if the desired behavior is to always have the user email.
Possible Solutions:
- Replace the default user model using the
AbstractBaseUser
to define the email field from scratch - If you can’t replace the user model, enforce the validation on the forms used to create/update
A user without password cannot initiate a password reset
There is a small catch on the user creation process that if the set_password
method is called passing None
as a
parameter, it will produce an unusable password. And that also means that the user will be unable to start a password
reset to set the first password.
You can end up in that situation if you are using social networks like Facebook or Twitter to allow the user to create an account on your website.
Another way of ending up in this situation is simply by creating a user using the User.objects.create_user()
or
User.objects.create_superuser()
without providing an initial password.
Possible Solutions:
- If in you user creation flow you allow users to get started without setting a password, remember to pass a random (and lengthy) initial password so the user can later on go through the password reset flow and set an initial password.
Swapping the default user model is very difficult after you created the initial migrations
Changing the user model is something you want to do early on. After your database schema is generated and your database is populated it will be very tricky to swap the user model.
The reason why is that you are likely going to have some foreign key created referencing the user table, also Django internal tables will create hard references to the user table. And if you plan to change that later on you will need to change and migrate the database by yourself.
Possible Solutions:
- Whenever you are starting a new Django project, always swap the default user model. Even if the default
implementation fit all your needs. You can simply extend the
AbstractUser
and change a single configuration on the settings module. This will give you a tremendous freedom and it will make things way easier in the future should the requirements change.
Detailed Solutions
To address the limitations we discussed in this article we have two options: (1) implement workarounds to fix the behavior of the default user model; (2) replace the default user model altogether and fix the issues for good.
What is going to dictate what approach you need to use is in what stage your project currently is.
- If you have an existing project running in production that is using the default
django.contrib.auth.models.User
, go with the first solution implementing the workarounds; - If you are just starting your Django, start with the right foot and go with the solution number 2.
Workarounds
First let’s have a look on a few workarounds that you can implement if you project is already in production. Keep in
mind that those solutions assume that you don’t have direct access to the User model, that is, you are currently using
the default User model importing it from django.contrib.auth.models
.
If you did replace the User model, then jump to the next section to get better tips on how to fix the issues.
Making username field case-insensitive
Before making any changes you need to make sure you don’t have conflicting usernames on your database. For example,
if you have a User with the username maria
and another with the username Maria
you have to plan a data migration
first. It is difficult to tell you what to do because it really depends on how you want to handle it. One option is
to append some digits after the username, but that can disturb the user experience.
Now let’s say you checked your database and there are no conflicting usernames and you are good to go.
First thing you need to do is to protect your sign up forms to not allow conflicting usernames to create accounts.
Then on your user creation form, used to sign up, you could validate the username like this:
If you are handling user creation in a rest API using DRF, you can do something similar in your serializer:
In the previous example the mentioned ValidationError
is the one defined in the DRF.
The iexact
notation on the queryset parameter will query the database ignoring the case.
Now that the user creation is sanitized we can proceed to define a custom authentication backend.
Create a module named backends.py anywhere in your project and add the following snippet:
backends.py
Now switch the authentication backend in the settings.py module:
settings.py
Please note that 'mysite.core.backends.CaseInsensitiveModelBackend'
must be changed to the valid path, where you
created the backends.py module.
It is important to have handled all conflicting users before changing the authentication backend because otherwise it
could raise a 500 exception MultipleObjectsReturned
.
Fixing the username validation to use accept ASCII letters only
Here we can borrow the built-in UsernameField
and customize it to append the ASCIIUsernameValidator
to the list of
validators:
Then on the Meta
of your User creation form you can replace the form field class:
Fixing the email uniqueness and making it mandatory
Here all you can do is to sanitize and handle the user input in all views where you user can modify its email address.
You have to include the email field on your sign up form/serializer as well.
Then just make it mandatory like this:
You can also check a complete and detailed example of this form on the project shared together with this post: userworkarounds
Replacing the default User model
Now I’m going to show you how I usually like to extend and replace the default User model. It is a little bit verbose but that is the strategy that will allow you to access all the inner parts of the User model and make it better.
To replace the User model you have two options: extending the AbstractBaseUser
or extending the AbstractUser
.
To illustrate what that means I draw the following diagram of how the default Django model is implemented:
The green circle identified with the label User
is actually the one you import from django.contrib.auth.models
and
that is the implementation that we discussed in this article.
If you look at the source code, its implementation looks like this:
So basically it is just an implementation of the AbstractUser
. Meaning all the fields and logic are implemented in the
abstract class.
It is done that way so we can easily extend the User
model by creating a sub-class of the AbstractUser
and add other
features and fields you like.
But there is a limitation that you can’t override an existing model field. For example, you can re-define the email field to make it mandatory or to change its length.
So extending the AbstractUser
class is only useful when you want to modify its methods, add more fields or swap the
objects
manager.
If you want to remove a field or change how the field is defined, you have to extend the user model from the
AbstractBaseUser
.
The best strategy to have full control over the user model is creating a new concrete class from the PermissionsMixin
and the AbstractBaseUser
.
Note that the PermissionsMixin
is only necessary if you intend to use the Django admin or the built-in permissions
framework. If you are not planning to use it you can leave it out. And in the future if things change you can add
the mixin and migrate the model and you are ready to go.
So the implementation strategy looks like this:
Now I’m going to show you my go-to implementation. I always use PostgreSQL which, in my opinion, is the best database
to use with Django. At least it is the one with most support and features anyway. So I’m going to show an approach
that use the PostgreSQL’s CITextExtension
. Then I will show some options if you are using other database engines.
For this implementation I always create an app named accounts
:
Then before adding any code I like to create an empty migration to install the PostgreSQL extensions that we are going to use:
Inside the migrations
directory of the accounts
app you will find an empty migration called
0001_postgres_extensions.py
.
Modify the file to include the extension installation:
migrations/0001_postgres_extensions.py
Now let’s implement our model. Open the models.py
file inside the accounts
app.
I always grab the initial code directly from Django’s source on GitHub, copying the AbstractUser
implementation, and
modify it accordingly:
accounts/models.py
Let’s review what we changed here:
- We switched the
username_validator
to useASCIIUsernameValidator
- The
username
field now is usingCICharField
which is not case-sensitive - The
email
field is now mandatory, unique and is usingCIEmailField
which is not case-sensitive
On the settings module, add the following configuration:
settings.py
Now we are ready to create our migrations:
Apply the migrations:
And you should get a similar result if you are just creating your project and if there is no other models/apps:
If you check your database scheme you will see that there is no auth_user
table (which is the default one), and now
the user is stored on the table accounts_customuser
:
And all the Foreign Keys to the user model will be created pointing to this table. That’s why it is important to do it right in the beginning of your project, before you created the database scheme.
Now you have all the freedom. You can replace the first_name
and last_name
and use just one field called name
.
You could remove the username
field and identify your User model with the email
(then just make sure you change
the property USERNAME_FIELD
to email
).
You can grab the source code on GitHub: customuser
Handling case-insensitive without PostgreSQL
If you are not using PostgreSQL and want to implement case-insensitive authentication and you have direct access to the User model, a nice hack is to create a custom manager for the User model, like this:
accounts/models.py
Then you could also sanitize the username field on the clean()
method to always save it as lowercase so you don’t have
to bother having case variant/conflicting usernames:
Conclusions
In this tutorial we discussed a few caveats of the default User model implementation and presented a few options to address those issues.
The takeaway message here is: always replace the default User model.
If your project is already in production, don’t panic: there are ways to fix those issues following the recommendations in this post.
I also have two detailed blog posts on how to make the username field case-insensitive and other about how to extend the django user model:
You can also explore the source code presented in this post on GitHub: