How to Log Users In With Their Email
Table of contents
- Project Set Up
- What we are going to do
- What we are not going to do
- Why do we need to start with a fresh database?
- Create a Custom User Model
- Tell Django Where to Find the User Model
- Create & Run Migrations
- Customise the User Model
- Create a Custom User Manager
- Migrate your changes to the database
- Create a form
- Update the View
- Add the User model to the admin site
- Conclusion
What makes Django different to other web frameworks is it includes a model for users. This means it is possible to add user registration to your project without having to create a table to store user data in the database yourself.
I recently wrote a tutorial on basic user registration showing how little custom code it takes to add a registration form to your project.
You can use Django's built in user model for convenience but it does mean you have to use the username
field to identify users. You may ask, can users identify themselves with their email instead?
This is what I am going to show you today.
We are not going to create a user model from scratch. Django's built-in user model is very useful. It would be a shame to lose it just because we want to make a small tweak.
Instead, we going to extend the default user model with our own Custom User Model. This means we can change what ever we need and keep the rest.
Project Set Up
I have created a repository in GitHub with the code for this tutorial. To follow along, clone the START branch. The main branch contains the finished code.
Or start with a blank project. I have a tutorial on setting up a Django project.
I also have a tutorial on creating a basic user registration system with a username and password. The end result is the same.
Because we are going to override the default user model, I recommend starting with a new project. This is because there are some changes we need to make before running migrations for the first time. If you want to retrofit an existing project, then I recommend dropping your database before removing migrations from your apps.
This is what we are starting with.
A simple homepage telling the user if they are logged in.
The ability to register users with a username and password. This makes use of Django's UserCreationForm
.
A login form that makes use of Django's built-in LoginView
.
What we are going to do
Our goal is to authenticate users using their email address. Users should not have to provide a username.
This involves:
Creating a custom user model
Creating a custom user manager
Creating a new user creation form
This is more complicated than authenticating with a username because Django's default user manager and user creation forms both expect a username and not an email. However, by the end of this tutorial, you will have a deeper understanding of how Django works.
Tip
If you don't already refer to the Django source code, this is the perfect opportunity to start. Look at the classes we inherit from, including AbstractUser
, UserManager
and UserCreationForm
. This will help you understand how Django works and why each step is required.
What we are not going to do
We are not going to write anything from scratch. Django's default user model and user creation forms have lots of useful functionality such as validating passwords. Rather than write completely new classes, we are going to extend these classes to tailor it to our needs.
Our approach is to write a little custom code as possible.
Let's get started.
Why do we need to start with a fresh database?
We are going to create a custom user model that replaces Django's default user model.
The problem is, Django creates the User model when you run migrations for the first time. Once that User table has been created in the database, it is difficult to replace it with a custom model.
The migration users.0001_initial
creates the User table.
Before we run those migrations, we need to make some changes.
Create a Custom User Model
If you have cloned the START branch of the repository, you can skip this step as the code already has a custom model defined.
We need to extend Django's default user model, which we can do by inheritance.
Go to users/models.py
and add a class called User
. It must inherit from AbstractUser
which is imported from django.contrib.auth.models
.
# users/models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
pass
For now, we will just add pass
, which means we're keeping all the functionality of the AbstractUser model. We will customise it to use email instead of username later in the tutorial.
By inheriting from AbstractUser
, we give ourselves the ability to customise the model without having to write a new one from scratch. After we run migrations for the first time, we will still be able to customise it how we like.
Tell Django Where to Find the User Model
So far, we have inherited the AbstractUser
model but Django doesn't know it's intended to be the user model.
We need to update settings.py
to state the location of the user model.
# settings.py
AUTH_USER_MODEL = "users.User"
Make sure you also add users
to the list of installed apps:
# settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"users",
"core",
]
Create & Run Migrations
Now that we have created a custom user model, we need to create and run the migrations. Migrations are what Django uses to interpret your Python models and convert it into SQL operations for the database. They can be really confusing for beginners, which is why I have written this guide on understanding migrations.
Create migrations by running this line. If you have any problems with your settings, they will appear when you attempt to run this command. If you're stuck, refer to the settings in my GitHub repository.
python manage.py makemigrations
You should get an output similar to this:
Migrations for 'users':
users/migrations/0001_initial.py
- Create model User
Now run the migrations:
python manage.py migrate
Customise the User Model
Right now, our User model only inherits from Django's Abstract User model.
class User(AbstractUser):
pass
We want to customise this model so that the email field is used as the unique identifier of users.
We can do this by overriding the USERNAME_FIELD
of the Abstract User model.
The problem is, Django expects to the value of the USERNAME_FIELD
to be unique. In the source code for AbstractUser
, the username field is required to be unique, but not the email field. We need to override the email field to make sure every user has a unique email address.
We also need to remove "email" from the REQUIRED_FIELDS
list. Django will give you an error if you forget to do that.
This is the code for my model.
# users/models.py
class User(AbstractUser):
email = models.EmailField(unique=True, blank=False, null=False)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
objects = CustomUserManager()
At this point, you should get an error to say the CustomUserManager
is not defined. We will define it in the next step.
Create a Custom User Manager
Why is a custom user manager required?
If you use User.objects.create
to create users, passwords would be stored in plain text. We need to hash and salt passwords before saving them to the database.
A model manager lets you define your own interface for creating users. By default, the AbstractUser
model uses a class called UserManager
to provide methods called create_user
and create_superuser
.
Instead of calling User.objects.create(...
) to create users, you can use User.objects.create_user(...)
instead.
We can't use the UserManager
class as it expects a username. We need to create our own manager that will handle email instead.
CustomUserManager
The manager needs two public methods:
create_user
create_superuser
The two methods will be almost identical. The only difference is that superusers will have the is_superuser
and is_staff
set to "True", which is needed to access the admin site.
The Custom User Manager class needs to go in models.py
above the User class.
from django.db import models
from django.contrib.auth.models import AbstractUser, UserManager
from django.core.validators import validate_email
class CustomUserManager(UserManager):
def _get_email(self, email: str):
validate_email(email)
return self.normalize_email(email)
def _create_user(
self,
email: str,
password: str,
commit: bool,
is_staff: bool = False,
is_superuser: bool = False
):
email = self._get_email(email)
user = User(email=email, username=email, is_staff=is_staff, is_superuser=is_superuser)
user.set_password(password)
if commit:
user.save()
return user
def create_superuser(self, email: str, password: str, commit: bool = True):
return self._create_user(email, password, is_staff=True, is_superuser=True, commit=commit)
def create_user(self, email: str, password: str, commit: bool = True):
return self._create_user(email, password, commit=commit)
To avoid writing the same code twice, I have created a private method called _create_user
that checks the email and creates the user. This is called by both create_user
and create_superuser
.
You can define private methods on a class by prefixing the name with an underscore. This means that other methods of the class can access _create_user
but you can't access it from outside the class. This means User.objects._create_user
won't work.
Username workaround
Our user model inherited a username field from AbstractUser
. When you inherit models, it is easy to add or modify fields but hard to take them away.
I want users to be able to sign up without providing a username. As a workaround, we will save the email address to the username field. This will satisfy the requirement for the username to be unique.
If you don't want to have a username column in your user model at all, then you will need to declare a user model that inherits from AbstractBaseUser
. This will give you full control over what fields go into the model, but requires more work.
For now, this workaround lets us keep the benefits of the AbstractUser
model.
Checking the email
In most cases, users will be created using a form or using python manage.py createsuperuser
. Both of these ways will do their own validation of the email address before User.objects.create_user
or User.objects.create_superuser
is called.
However, if your application has a third way to create users that doesn't have its own validation, then it makes sense to check emails before saving them to the database.
I have used validate_email
, a function imported from django.core.validators
. If you try to provide an invalid email or an empty email, then Django will raise a Validation Error.
I have also used the normalize_email
method to convert the domain part of the name to lower case.
Protecting Passwords
Notice how we don't call User.objects.create
to create the user. If you do this, the password would be stored in plain text for all the admin users to see.
Instead, we first create an instance of the User model. This means you create an object without saving it to the database.
Our user model has inherited a method called set_password
from the AbstractBaseUser
class.
The best way to set a password is by calling the set_password
method on the user instance. The set_password
method will encode the password provided by the user which means admins and database users will not be able to see users' passwords.
What is commit for?
The create_user
and create_superuser
methods have an argument called commit
. This gives us the option to not commit the object to the database. Sometimes, you want to get an instance of a user without saving it to the database. You would usually do this if you wanted to modify the instance before saving it.
You can leave it out if you like. However, we do make use of it later in this tutorial when we override the save
method of the registration form.
Migrate your changes to the database
Now that we have updated the User model and added a custom user manager, we are now ready to make migrations and apply them to the database.
Run python manage.py makemigrations
.
You should see a similar output to the snippet below.
Migrations for 'users':
users/migrations/0002_alter_user_managers_alter_user_email.py
- Change managers on user
- Alter field email on user
Create a form
Our next step is to create a new form.
We can't use Django's UserCreationForm
on its own as it displays a username field and not an email field.
To start, create a new file in the users
app called forms.py
.
Inside forms.py
, create a new class that inherits from Django's UserCreationForm
.
The UserCreationForm
inherits from Django's ModelForm
, which means all the fields on the User model are available to include on the form. This means, we just need to hide the username field and display the email field.
# users/form.py
from django.contrib.auth.forms import UserCreationForm
from .models import User
class UserRegistrationForm(UserCreationForm):
class Meta:
model = User
fields = ("email",)
You don't need to include "password" in the fields tuple because they have already been defined in the UserCreationForm
.
Override the save method
Our form comes with a method called save
, which we inherited from Django's ModelForm
via UserCreationForm
.
If you call save on a form instance (e.g. form.save()
), then Django will create an object in the database.
The problem is, the save
method on the UserCreationForm
doesn't make use of the model manager we created in the previous step.
If you don't override the save method, then Django will attempt to assign an empty string to the username field. This will work for the first user you create, but you will get an IntegrityError
when creating a second user. This is because the username field must be unique.
Your options are:
Override the username field on the model to remove the uniqueness constraint
Don't use
form.save()
. Configure your view to useUser.objects.create_user()
instead.Override the save method to use the model manager.
We are going to override the save method.
Here is our updated form.
class UserRegistrationForm(UserCreationForm):
class Meta:
model = User
fields = ("email",)
def save(self, commit: bool = True) -> User:
email = self.cleaned_data["email"]
password = self.cleaned_data["password1"]
return User.objects.create_user(email, password, commit=commit)
The save method accepts a boolean argument called commit
. If commit
is True, then the user will be saved to the database. If commit
is False, then the save method will just return an instance of User
. If you do this, you can call user.save()
later to save the user to the database.
Update the View
The view needs to be updated to use our new User Registration form.
We can remove the import of UserCreationForm
and replace it with our custom form.
from django.contrib.auth.views import LoginView
from django.views.generic import CreateView
from django.urls import reverse
from .forms import UserRegistrationForm
class UserRegistration(CreateView):
template_name = "user_registration.html"
form_class = UserRegistrationForm
def get_success_url(self):
return reverse("login")
We can now reload our registration page to see the results
Same form, except it now accepts an email instead of a username.
Login Form
We don't need to make any changes to our login form. This is because we set the USERNAME_FIELD
on our User model to email
. Django's default login form is smart enough to render the email field instead of the username field.
Once logged in, we are redirected to the homepage.
Add the User model to the admin site
Our final step is to register the User model, so that we can view and update users on the admin site.
To do this, go to users/admin.py
and register the model.
# users/models.py
from django.contrib import admin
from .models import User
admin.site.register(User)
Conclusion
We have created a user registration system that uses an email instead of a username to identify the user.
There are several ways to go about this, but I have chosen the method that minimises the amount of custom code we have to write.
First, we created a custom user model that inherits from AbstractUser
. We save time by choosing AbstractUser
over AbstractBaseUser
. The latter only includes basic authentication and we would have to write the rest from scratch.
Next, we created a custom user manager. This provides us an interface to create users that handles passwords properly.
Then, we created a custom user creation form. We have to do this because the default form displays the username and not the email. Using a class-based view, we only have to update the form_class
attribute of our view to use our new form.
We didn't have to update the Login form because we set the USERNAME_FIELD
on our model. This tells Django to display the email field instead of the username field.
Our final step was to register the User model in admin.py
so we could view and edit users in the admin site.
If you have made it this far, then congratulations. This is not an easy topic. It requires an understanding of authentication and how Django's models, managers, views and forms fit together.