Django is a great framework for pretty much any major BE project. However, it uses username as authentication method. While it works great, it is a little bit outdated... Let's fix that!
We are going to use Django Allauth. This is a great package that allow us to easily setup authentication using email (and much more).
You can find the final source code on my Github
We start by creating a virtual environment. This will help us to keep our dependencies nice and clean:
mkdir django-allauth
cd django-allauth
python -m venv env
# If you use linux and mac:
env/scripts/activate
# If you use windows:
source env/bin/activate
We now install django and setup our project:
pip install django
django-admin startproject _project .
python manage.py runserver
If you visit http://localhost:8000 you should see our dev server up and running:
Changing the user model inside a ongoing project can be very difficult.
That is why Django's documentation highly recommends to setup a custom user model at the beginning of a project.
It saves a lot of time later on if we ever have to modify that user model.
It is very important to do these steps before your first database migration. If you migrated your database already, then you better start over:
python manage.py startapp users
### _project/settings.py
...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
#local
'users',
]
...
For our custom user model, we simply extend Django default user model:
### users/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
pass
We then tell Django to use that custom user model:
### _project/settings.py
...
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
AUTH_USER_MODEL = 'users.CustomUser'
...
We finally apply our migrations so the database uses our custom user model:
python manage.py makemigrations users
python manage.py migrate
If we create a superuser and check our custom user model in the admin interface, we notice that it is missing:
python manage.py createsuperuser
That is because we no longer use the default user model. And thus, it is not displayed by default in the admin interface.
Let's fix this:
### users/forms.py
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
class CustomUserCreationForm(UserCreationForm):
class Meta:
model = get_user_model()
fields = ('email', 'username',)
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = get_user_model()
fields = ('email', 'username',)
### users/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
CustomUser = get_user_model()
class CustomUserAdmin(UserAdmin):
add_form = CustomUserCreationForm
form = CustomUserChangeForm
model = CustomUser
list_display = ['email', 'username',]
admin.site.register(CustomUser, CustomUserAdmin)
We now see the cusotm user model displayed correctly:
It is time to create our homepage! We create a new app for this:
python manage.py startapp pages
...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.sites',
'django.contrib.staticfiles',
#local
'users',
'pages',
]
...
We place our html templates inside a folder in the root of our projects. I will use Bootstrap 5 to style these html templates:
mkdir templates
touch templates/_base.html
touch templates/header.html
mkdir templates/pages
touch templates/pages/home.html
### templates/_base.html
<!DOCTYPE html>
<html>
<head>
<title>Django Allauth</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
crossorigin="anonymous"
/>
</head>
<body>
{% include "header.html" %}
<div class="container mt-3">
<div class="card">
<div class="card-header">
<h1>{% block title %}{% endblock title %}</h1>
</div>
<div class="card-body">{% block content %}{% endblock content %}</div>
</div>
</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"
></script>
</body>
</html>
### templates/header.html
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="/">Django Allauth</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
{% if user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" aria-current="page" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/accounts/logout">Sign Out</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
### templates/pages/home.html {% extends "_base.html" %} {% block title %}Home
Page{% endblock title %} {% block content %}
<p>Welcome to the homepage <strong>{{ user.email }}</strong></p>
{% endblock content %}
We also need to tell Django to use this new templates
directory:
### _project/settings.py
from pathlib import Path
import os
...
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
...
We now create our home page view and route:
### pages/views.py
from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin
class HomeView(LoginRequiredMixin, TemplateView):
template_name="pages/home.html"
def get_context_data(self):
return {'pageTitle': 'Home Page'}
touch pages/urls.py
### pages/urls.py
from django.urls import path
from .views import HomeView
urlpatterns = [
path('', HomeView.as_view(), name="home")
]
# _project/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('pages.urls')),
]
If we visit http://localhost:8000/, we should now see our new home page rendered!
It is now time to switch to Allauth for email login! Let's install it and set it up:
pip install django-allauth
### _project/settings.py
...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.sites',
'django.contrib.staticfiles',
#local
'users',
'pages',
#third party
'allauth',
'allauth.account',
'allauth.socialaccount',
]
SITE_ID = 1
ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_EMAIL_VERIFICATION = 'none'
LOGIN_REDIRECT_URL = 'home'
...
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
]
...
### _project/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('pages.urls')),
path('accounts/', include('allauth.urls')),
]
We can now (using email and password) sign up a new user here http://localhost:8000/accounts/signup/, log them in here http://localhost:8000/accounts/login/ and log them out here http://localhost:8000/logout/:
You can see here http://localhost:8000/accounts/ the different Allauth urls accessible (with DEBUG=True only):
For email verification, we need an email backend. We will not cover it in this article. We will instead fake an email backend by outputting the email in the console:
### _project/settings.py
...
SITE_ID = 1
ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
LOGIN_REDIRECT_URL = 'home'
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
...
If we now sign up a new user (make sure to enter a valid email) and go to your console, we should see the confirmation link sent to that user in our console:
We will overide Allauth default template by creating our own templates inside templates/account/
folder (No "s" to account). We will also use Crispy-form to style our Allauth forms according to our Bootstrap style:
pip install django-crispy-forms
mkdir templates/account
touch templates/account/signup.html
touch templates/account/login.html
touch templates/account/logout.html
touch templates/account/email_comfirm.html
touch templates/account/verification_sent.html
### _project/settings.py
...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.sites',
'django.contrib.staticfiles',
#local
'users',
'pages',
#third party
'allauth',
'allauth.account',
'allauth.socialaccount',
'crispy_forms',
]
CRISPY_TEMPLATE_PACK = 'bootstrap4'
...
### templates/account/signup.html {% extends "_base.html" %} {% load
crispy_forms_tags %} {% block title %}Sign Up{% endblock title %} {% block
content %}
<form method="post">
{% csrf_token %} {{ form|crispy }}
<button class="btn btn-primary" type="submit">Sign Up</button>
<a class="btn btn-secondary" href="/accounts/login">Log In instead</a>
</form>
{% endblock content %}
### templates/account/login.html {% extends "_base.html" %} {% load
crispy_forms_tags %} {% block title %}Log In{% endblock title %} {% block
content %}
<form method="post">
{% csrf_token %} {{ form|crispy }}
<button class="btn btn-primary" type="submit">Log In</button>
<a class="btn btn-secondary" href="/accounts/signup">Sign Up instead</a>
</form>
{% endblock content %}
### templates/account/logout.html {% extends "_base.html" %} {% load
crispy_forms_tags %} {% block title %}Log Out{% endblock title %} {% block
content %}
<form method="post">
{% csrf_token %} {{ form|crispy }}
<p>You are about to log out. Are you sure?</p>
<button class="btn btn-danger" type="submit">Log Out</button>
<a class="btn btn-secondary" href="/">Home</a>
</form>
{% endblock content %}
### templates/account/verification_sent.html {% extends "_base.html" %} {% load
crispy_forms_tags %} {% block title %}Comfirm Your Email{% endblock title %} {%
block content %}
<p>
We have sent you an email. Please follow the link provided to finalize the
signup process
</p>
{% endblock content %}
### templates/account/email_confirm.html {% extends "_base.html" %} {% load
account %} {% load crispy_forms_tags %} {% block title %}Email Confirmation{%
endblock title %} {% block content %}
<form method="post">
{% csrf_token %} {{ form|crispy }}
<p>
Please confirm that
<strong>{{ confirmation.email_address.email }}</strong> is your email
address
</p>
<button class="btn btn-primary" type="submit">Comfirm</button>
</form>
{% endblock content %}
Allauth view should now be rendered with our new style:
If you want to further customize these templates, I highly recommand you check the source code of Allauth to see how these templates are built (link below).
https://github.com/pennersr/django-allauth/tree/master/allauth/templates/account
As you can see, Django Allauth makes email authentication super easy for us! Templates are also relatively easy to customize as well.
You can find the complete source code of the python project on my github
If you have any question or just want to chat, feel free to email me florian.bigot321@gmail.com