How to Setup Login With Email in Django

Django default authentication supports username and password only. In this tutorial we will setup email authentication which will enable us to login with email and password instead of username.

Since we will be creating our own custom User model, this tutorial is helpful only when you are creating a new project.

Authentication is process by which we identify the users and give them access to specific sections in our websites. In this tutorial, we will also attach specific attributes to users in our website to identify if they are staff users, normal users or administrators.

Common authentication methods used in websites and mobile apps are:

  • Email Authentication (email and password)
  • Email Authentication with Magic link
  • Social signin (login with facebook, google and others)
  • Login with mobile phone and OTP

We will cover the first authentication method (Email Authentication) where users will enter email and password to login to our website.

Disclaimer: This tutorial includes best practices and approaches necessary when working in any production grade project. This may seem complicated and unnecessary for beginners. I will try to explain all these points below.

Installing specific python version

It is best practice to pin our project to a specific python version. As we add dependencies and new code in our project, we might encounter issues when the project automatically picks the latest python version. With each version new of python new features are added and sometimes old features are removed or deprecated.

If we specify only python3 as the requirement, the developers could install python3.6, python3.7, python3.8, python3.9, python3.10 or python3.11. When multiple developers are working with multiple python versions, the code changes might become incompatible between python versions.

To fix this, we specify a specific python version and every developer uses the same version. Since multiple projects can have different python version requirement, we need a way to install multiple python versions in our machine.

We are using pyenv for creating specific python version (e.g. python 3.8.0 for this project). More details on using pyenv

We first create the base directory which will contain all our code

1
mkdir django-email-auth

Now, inside django-email-auth we will configure specific python version.

1
2
3
4
5
cd django-email-auth
   	  
pyenv install 3.8.0
pyenv local 3.8.0

This will create .python-version file in this directory (django-email-auth). Let's verify that our local python installation is working correctly.

1
2
3
4

python -V
Python 3.8.0
  	  

Now, we have configured a specific python version, the next step would be to configure virtual environment.

Creating virtual environment

We will create virtual environment before setting up the project. As per 12 Factor App dependencies should be declared explicitly. We will make sure every dependency of the project is pinned to specific version (including python too). Virtual environment helps us to separate system level python and it's packages with our project specific packages.

Make sure we are in django-email-auth directory where we setup python 3.8.0. We are using python inbuilt feature to create a virtual environment in venv directory.

1
2
3
4
5
6
7
8

python -m venv venv
  	  
# this will create virtual environment in venv directory
 
venv/bin/python -V
Python 3.8.0

After setting up the virtual environment, we will upgrade pip to specific version

1
2
venv/bin/pip install -U pip==22.3.1

Installing Django

1
2
3

venv/bin/pip install Django==4.1.5

Since we are using virtual environemnt, this gets installed in venv/lib/python3.8/site-packages/.

1
2
3

ls venv/lib/python3.8/site-packages/

We will add this specific Django version to a text file so that other developers working on the project know the specific version that we are using. For this, we generally create a requirements.txt file which specifies all the packages and their specific version. All these dependencies can be installed later by using pip install -r requirements.txt

Let's create a new requirements.txt file in django-email-auth and add below content.

1
2
3

Django==4.1.5

Creating demo project

Since we have already created the directory, django-email-auth, we will create a new project and specify . so that it doesn't create nested directory

1
2
   venv/bin/django-admin startproject project .
       

Below is the directory structure created

		  .
		  ├── manage.py
		  ├── project
		  │   ├── __init__.py
		  │   ├── asgi.py
		  │   ├── settings.py
		  │   ├── urls.py
		  │   └── wsgi.py
		  ├── requirements.txt
		  └── venv
		      ├── bin
		      ├── include
		      ├── lib
		      └── pyvenv.cfg

Project structure

We will be creating all our custom apps inside a specific directory apps in django-email-auth. This will help us to organized our project files in a cleaner way. This approach is optional.

Creating custom User model for email instead of username

We will create a new app custom_auth and a model User.


mkdir -p  apps/custom_auth
venv/bin/django-admin startapp custom_auth apps/custom_auth
	  

Since we have created app inside apps directory, will have to update apps/custom_auth/apps.py and change name = 'custom_auth' to name = 'apps.custom_auth'

1
2
3
4
5
6
7
# open apps/custom_auth/apps.py 

change 
    name = 'custom_auth' 
to 
    name = 'apps.custom_auth'

We will add this new app to our settings file at project/settings.py. Find INSTALLED_APPS in this file and append below code.

# project/settings.py -> INSTALLED_APPS 

'apps.custom_auth.apps.CustomAuthConfig'

We create a new user by extending AbstractBaseUser

  • we are adding PermissionsMixin as this will provide us out of the box django group and permissions functionality
  • is_staff is to identify users who can login to django admin backend
  • PermissionsMixin provides is_superuser field which enables admin account. This account will have all the permissions on all models in django

Add below code in custom_auth/models.py. Below code contains both User model and UserManager.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

from django.contrib.auth.base_user import BaseUserManager
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.db import models


class UserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError('Users must have an email address')

        user = self.model(
            email=self.normalize_email(email),
            is_staff=False, is_superuser=False, **extra_fields
        )

        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password=None):
        """
      Creates and saves a superuser with the given email, date of
      birth and password.
      """
        user = self.create_user(
            email,
            password=password
        )
        user.is_staff = True
        user.is_active = True
        user.is_superuser = True
        user.save(using=self._db)
        return user


class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(_("email address"), unique=True)

    first_name = models.CharField(_("first name"), max_length=150, blank=True)
    last_name = models.CharField(_("last name"), max_length=150, 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)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    objects = UserManager()

    def __str__(self):
        return self.email

    			      

Add below code in custom_auth/admin.py. This will enable admin functionality for the User model which we created earlier.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
from django.utils.translation import gettext_lazy as _
from .models import User


class UserAdmin(DjangoUserAdmin):
    fieldsets = (
        (None, {"fields": ("email", "password")}),
        (_("Personal info"), {"fields": ("first_name", "last_name")}),
        (
            _("Permissions"),
            {
                "fields": (
                    "is_active",
                    "is_staff",
                    "is_superuser",
                    "groups",
                    "user_permissions",
                ),
            },
        ),
        (_("Important dates"), {"fields": ("last_login", "date_joined")}),
    )

    add_fieldsets = (
        (
            None,
            {
                "classes": ("wide",),
                "fields": ("email", "password1", "password2"),
            },
        ),
    )

    list_display = ("email", "first_name", "last_name", "is_active", "is_staff", "is_superuser")
    ordering = ("-id",)


admin.site.register(User, UserAdmin)



Since we have create our own User model, we will have to configure Django to use our model instead of the default User model. For this, we add below configuration in project/settings.py.

# append below code at the end of file project/settings.py

AUTH_USER_MODEL = 'custom_auth.User'

Our new User model is configured. Now we create migrations.

venv/bin/python manage.py makemigrations custom_auth

Applying all migrations along with the new User migration.

venv/bin/python manage.py migrate

Create a super user which we will use for login.

venv/bin/python manage.py createsuperuser
        		  

Run server and access http://localhost:8000/admin/ to verify

venv/bin/python manage.py runserver
		  

After login, we can access the admin console for User model http://localhost:8000/admin/custom_auth/user/ and create new users.