Enable Login with Email in Django


Django is currently my favorite framework for building web applications. Despite its simplicity and delightful development experience, one of the things that I personally don’t like about Django is its default authentication system. I don’t know why they’re doing this, but for me, log in with username doesn’t meet the web industry standards.

Apparently, this problem can be fixed solved easily with some hacks. Let’s take a look at the steps on how to enable your users to login with their emails in your Django apps.

Getting Started

Create a new Django project. This can be done by creating a new virtual environment, install Django, and generate a new project with django-admin command.

$ python -m venv env
$ source env/bin/activate
$ pip instal django
$ django-admin startproject django_email_login .

I have published the entire source code of this project on my GitHub. Feel free to poke around or clone it onto your computer with the command below.

$ git clone https://github.com/rahmanfadhil/django_email_login.git

Also, don’t forget to install the dependencies.

$ pip install -r requirements.txt

Custom user model

We can start by creating a new application in our Django project which called accounts. This app will contain everything we need for our user authentication features.

$ python manage.py startapp accounts

Just to remind you guys, make sure to install your app in your settings.py each time you start a new one.

settings.py
# ...

INSTALLED_APPS = [
  # other apps...

  'accounts.apps.AccountsConfig'
]

In order to make our email field required in our database, we need to define a custom user model. Let’s do it inside our accounts application.

accounts/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils.translation import gettext_lazy as _


class CustomUser(AbstractUser):
    email = models.EmailField(_('email address'), unique=True)

It’s also important to make our email field unique.

Then, add the AUTH_USER_MODEL variable in our settings.py to tell Django we’re going to use a custom model for our users.

settings.py
# ...

AUTH_USER_MODEL = 'accounts.CustomUser' # new

Once we created a new model, make sure to generate the migrations and run them.

$ python manage.py makemigrations
$ python manage.py migrate

Authentication backend

The next step is to create a new custom authentication backend for our app. This allows us to override the default logic of our authentication system since we’re trying to allow our users to log in with their email or username.

accounts/backends.py
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from django.db.models import Q


UserModel = get_user_model()


class EmailBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        try:
            user = UserModel.objects.get(Q(username__iexact=username) | Q(email__iexact=username))
        except UserModel.DoesNotExist:
            UserModel().set_password(password)
            return
        except UserModel.MultipleObjectsReturned:
            user = UserModel.objects.filter(Q(username__iexact=username) | Q(email__iexact=username)).order_by('id').first()

        if user.check_password(password) and self.user_can_authenticate(user):
            return user

Now, let’s modify the settings.py to register our custom authentication backend.

settings.py
# ...

AUTH_USER_MODEL = 'accounts.CustomUser'
AUTHENTICATION_BACKENDS = ['accounts.backends.EmailBackend'] # new

Authentication forms

Once we have modified the authentication system, we also need to adjust our forms to make sure our views ask the right data.

accounts/forms.py
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth import get_user_model
from django import forms


class RegisterForm(UserCreationForm):
    class Meta:
        model = get_user_model()
        fields = ('email', 'username', 'password1', 'password2')


class LoginForm(AuthenticationForm):
    username = forms.CharField(label='Email / Username')

By default, the UserCreationForm only takes the username and password fields (with its verification input). So, we can modify this by creating a new class called RegisterForm which extends the default form. Then, we specify the required fields to create a new user (email, username, and password).

The login form on the other hand only needs a little adjustment. We need to change the field type of our username field so that it can accept both email and username.

From now on, we will use these forms to signup a new user, and log in an existing one.

Views

This part is optional, but in case you still confused on how to implement this feature in your view, you can follow these steps.

Create the views for login and register. Here, we can use our custom authentication forms a and pass the template where we render those forms.

accounts/views.py
from django.contrib.auth import views as auth_views
from django.views import generic
from django.urls import reverse_lazy

from .forms import LoginForm, RegisterForm


class LoginView(auth_views.LoginView):
    form_class = LoginForm
    template_name = 'accounts/login.html'


class RegisterView(generic.CreateView):
    form_class = RegisterForm
    template_name = 'accounts/register.html'
    success_url = reverse_lazy('login')

We also want to redirect the users to our login page right after the signed up a new account by defining success_url property in our RegisterView.

Let’s create some templates that show the login and register form.

accounts/templates/accounts/login.html
<h1>Login</h1>

<form action="{% url 'login' %}" method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit" id="submit">Login</button>
</form>
accounts/templates/accounts/register.html
<h1>Register</h1>

<form action="{% url 'register' %}" method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit" id="submit">Register</button>
</form>

Then, we can register our views in urls.py.

urls.py
from django.contrib import admin
from django.urls import path

from accounts.views import LoginView, RegisterView


urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/', LoginView.as_view(), name='login'),
    path('register/', RegisterView.as_view(), name='register')
]

That’s it!

Run your app with the command below, and here is what you should get.

$ python manage.py runserver

Login:

login

Register:

register

Tags: Python, Django, Authentication