How to Log Users In With Their Email

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.

Screenshot of the homepage showing that the user is not logged in.

The ability to register users with a username and password. This makes use of Django's UserCreationForm.

A screenshot of the sign up form with a username field

A login form that makes use of Django's built-in LoginView.

A screenshot of the login view with a username field.

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.

A screenshot of migrations showing that Django creates a user model when migrations are run for the first time.

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:

  1. create_user

  2. 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 use User.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

A signup form with a field for email and not username.

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.

A login form with an email field and not a username field.

Once logged in, we are redirected to the homepage.

Screenshot of the homepage showing the user they are logged in.

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)

A screenshot of the admin site showing the user update form.

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.