Django Log In with Email not Username

Updated

Table of Contents

Django was first released in 2005, and since then, a lot has changed in web development. Notably, the predominant pattern of using username/email/password has been simplified to just email/password. However, due to legacy reasons around the built-in Django User model, it can take a few extra steps to implement this change in practice.

In this tutorial, we'll walk through how to implement email and password only for sign-up and log-in on a new Django project using a custom user model and the popular django-allauth package.

Getting Started

I assume you know how to start and install a new Django project using pip. If not, please see here for additional help.

On the command line, create a new directory for our code called code (it can live anywhere on your computer, but we'll use the Desktop), then activate a new virtual environment.

# Windows
$ cd onedrive\desktop\code
$ mkdir drf
$ cd drf
$ python -m venv .venv
$ .venv\Scripts\Activate.ps1
(.venv) $ 

# macOS
$ cd desktop/desktop/code
$ mkdir drf
$ cd drf
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $ 

Now install the latest version of Django

(.venv) $ python -m pip install django~=5.0.0

Custom User Model

I wrote about this in length over here, so I will give the commands in this post.

Create a accounts app.

(.venv) $ python manage.py startapp accounts

Update the settings.py file.

# config/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    # Local
    "accounts",  # new
]
...
AUTH_USER_MODEL = "accounts.CustomUser"  # new

Create our CustomUser model.

# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

class CustomUser(AbstractUser):
    # add additional fields in here

    def __str__(self):
        return self.email

Create a new accounts/forms.py file and then fill it with the following code.

# accounts/forms.py
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser

class CustomUserCreationForm(UserCreationForm):

    class Meta:
        model = CustomUser
        fields = ("username", "email")

class CustomUserChangeForm(UserChangeForm):

    class Meta:
        model = CustomUser
        fields = ("username", "email")

And update the admin.

# accounts/admin.py
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin

from .forms import CustomUserCreationForm, CustomUserChangeForm
from .models import CustomUser

class CustomUserAdmin(UserAdmin):
    add_form = CustomUserCreationForm
    form = CustomUserChangeForm
    model = CustomUser
    list_display = ["email", "username",]

admin.site.register(CustomUser, CustomUserAdmin)

Now, make a migrations file, migrate the database for the first time, and create a superuser so we can access the admin.

(.venv) $ python manage.py makemigrations accounts
(.venv) $ python manage.py migrate
(.venv) $ python manage.py createsuperuser
(.venv) $ python manage.py runserver

The local server is now running so you can visit both http://127.0.0.1:8000 for the Django welcome page and also http://127.0.0.1:8000/admin to log in with our new superuser account.

Django admin page

Make sure to log out of the admin at this point. Why? Because if you don't, then Django will automatically send all future URL requests to /accounts/profile/ and future stuff won't work.

We will configure this properly later but for now, make sure you're logged out or stuff will break.

django-allauth

Django comes with built-in authentication views, but they require manual configuration, including URLs and templates for a complete signup page. So, while we can roll our own, a better approach--and the current default for many professionals--is to use the django-allauth package instead.

Install it with pip' you've used Control+c to stop the local server.

(.venv) $ python -m pip install django-allauth~=0.63.6

Add it to our INSTALLED_APPS setting and add Django's optional sites framework, which django-allauth uses.

# config/settings.py
INSTALLED_APPS = [
        "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    # 3rd party
    "allauth", # new
    "allauth.account", # new
    "allauth.socialaccount", # new

    # Local
    "accounts",
]

Ok, here's where the magic happens. django-allauth has a lengthy list of configuration options. Here's what we need.

Add this at the bottom of our config/settings.py file.

# config/settings.py
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"

AUTHENTICATION_BACKENDS = (
    # Needed to login by username in Django admin, regardless of `allauth`
    "django.contrib.auth.backends.ModelBackend",

    # `allauth` specific authentication methods, such as login by e-mail
    "allauth.account.auth_backends.AuthenticationBackend",
)

SITE_ID = 1

ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False
ACCOUNT_SESSION_REMEMBER = True
ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_UNIQUE_EMAIL = True

Now run migrate since we've updated our INSTALLED_APPS.

(.venv) $ python manage.py migrate

URLs

We need to tell Django where to put our fancy django-allauth config, so let's use the accounts/ path.

# config/urls.py
from django.contrib import admin
from django.urls import path, include  # new

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("allauth.urls")),  # new
]

Default Log In and Sign Up Pages

If you start up the local server now with python manage.py runserver, you can navigate to our working log-in page at http://127.0.0.1:8000/accounts/login/.

Log in

Now take a look at the signup page at http://127.0.0.1:8000/accounts/signup/.

Sign up

Templates

It's time to configure our templates so things look better. django-allauth uses templates in an account folder, so we'll override its defaults there.

Create a project-level templates folder and then account within it.

(.venv) $ mkdir templates
(.venv) $ mkdir templates/account

Now tell Django to look here, so update the TEMPLATES config within settings.py.

# config/settings.py
TEMPLATES = [
    {
        ...
        'DIRS': [os.path.join(BASE_DIR, "templates")], # new
        ...
    }
]

And make our log in and sign up templates at templates/account/login.html and templates/account/signup.html. Here's the code for each.

<!-- templates/account/login.html -->
<h2>Log In</h2>
<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">Log In</button>
</form>
<!-- templates/account/signup.html -->
<h2>Sign Up</h2>
<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">Sign Up</button>
</form>

Here is the new log in page.

New log in page

And the new sign up page.

New sign up page

But are we done? Haha, of course not. If you try out the forms you'll see they result in error pages. Why? We need to tell Django where to redirect our user after they log in and sign up.

Error page

Redirects

The setting for redirects is, not surprisingly, in our settings.py file. Here are the two lines to add:

# config/settings.py
LOGIN_REDIRECT_URL = "home"
ACCOUNT_LOGOUT_REDIRECT_URL = "home"

Both reference a template with the named URL of home so we'll need to create that homepage now.

Homepage

I like to create a dedicated pages app for all static pages in my Django projects. Let's blast through that quickly which means also adding a new template, view, and URL. Here we go...make sure to stop the local server with Control+c.

(.venv) $ python manage.py startapp pages

Add the new pages app to our INSTALLED_APPS.

# config/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    # 3rd party
    "allauth",
    "allauth.account",
    "allauth.socialaccount",

    # Local
    "accounts",
    "pages",  # new
]

Add it to our config/urls.py file.

# config/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("allauth.urls")),
    path("", include("pages.urls")),  # new
]

Update the views file for it.

# pages/views.py
from django.views.generic import TemplateView


class HomePageView(TemplateView):
    template_name = "home.html"

Then make a new pages/urls.py file and fill it in like so:

# pages/urls.py
from django.urls import path

from .views import HomePageView

urlpatterns = [
    path('', HomePageView.as_view(), name='home'),
]

Now we can make our homepage template which will tell us if a user is logged in or not and provide helpful links.

Create the templates/home.html file and add some basic logic for if the user is logged in or not. Be careful not to put this within the templates/account directory, this is just in templates itself.

<!-- templates/home.html -->
{% if user.is_authenticated %}
  Hi {{ user.email }}!
  <p><a href="{% url 'account_logout' %}">Log Out</a></p>
{% else %}
  <p>You are not logged in</p>
  <a href="{% url 'account_login' %}">Log In</a> |
  <a href="{% url 'account_signup' %}">Sign Up</a>
{% endif %}

Phew! Done. Make sure you are logged out which you can confirm by heading over quickly to the Django admin http://127.0.0.1:8000/admin. The "Log out" link is in the upper right corner.

Test It All Out

Now go to our snazzy new homepage at http://127.0.0.1:8000/.

Homepage

If you click on "Log in" and use your superuser account it will redirect back to the homepage. Here's what mine looks like.

Homepage logged in

Now click on the "Log out" link and then try the "Sign up" link. I've created a new user account called [email protected]. Upon signing up I'm redirected back to the homepage.

Homepage logged in with testuser

Next Steps

If you'd like to see a more full-featured Django starter project, my book Django for Professionals covers all of this in great detail as well as using Docker, PostgreSQL, and a host of more advanced Django topics.