Email Authentication System

Unfortunately django hasn't provided a builtin email authentication back end, this is while nowadays most websites are using this kinda systems as their user validation methods and now we have to build it by our selves
in this article will represent how to create an email authentication system for a django application, you can find the project source in my github.
NOTE : presumed that you will start the app by a custom user model(because we rewrite User email) and if you hadn't, make sure each USER use a unique email address.
Create a project
we call project folder to email_authetication
django-admin startproject email_authentication .
start app
let's call the app authenticate
python3 manage.py startapp authenticate
NOTE : do not makemigrations untill we create a custom User model
now add the app to INSTALLED_APPS in ./email_authetication/settings.py
Built Custom User Model :
let's create a custom User model, it is helpful to always create a User model even if you won't need it at the time.
but now we need to rewrite the email_field to make sure we won't get two users with the same email address
1- in ./authenticate/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import gettext_lazy as _
class User(AbstractUser):
email = models.EmailField(
_("email address"),
unique=True,
blank=True,
error_messages={
"unique": _("A user with that email already exists."),
},
)
2- in ./authenticate/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User
admin.site.register(User, UserAdmin)
3- in ./email_authentication/settings.py we need to tell django to use our custom User model
AUTH_USER_MODEL = 'authenticate.User'
django will look to authenticate app then in models look for User model.
now ... let's do the first makemigrations and migrate the changes
python3 manage.py makemigrations
python3 manage.py migrate
congratulations, now we built our custom model ... for making sure we use the right User model, you can have access to User model by the following path:
# not recommended
from authentication.models import User
# recommended
from django.contrib.auth import get_user_model # a function that return the current user model
from django.conf import settings # get settings.AUTH_USER_MODEL
the two recommended ways are kinda the same
Build Authentication Backend
we need to create a new authentication backend to tell django to also use this backend to authentify users
create a new file and name it ./authenticate/backends.py
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
class EmailAuthentication(ModelBackend):
def authenticate(self, request, email=None, password=None, **kwargs):
UserModel = get_user_model()
try:
user = UserModel.objects.get(email=email)
except UserModel.DoesNotExist:
return None
except UserModel.MultipleObjectsReturned:
return None # in case multiple users with the same email exist none of them will authentify
else:
if user.check_password(password):
return user
return None
in ./email_authentication/settings.py
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend', # keep username and password authentication
'authenticate.backends.EmailAuthentication', # add email and password authentication
]
add django and our custom backends to settings to tell Django use this backend too, then our email authentication system is complete
now you can authenticate the user in both username and passsword or email and password
you can also use the same method above to create an authentication backend to authentify with a secure key or etc
e.g
from django.contrib.auth import authenticate
# authenticate username and password
authenticate(request, username = username, password = password)
# authenticate email and password
authenticate(request, email = email, password = password)
let's create a superuser and test :
$ python3 manage.py shell
>>> from django.contrib.auth import authenticate
>>> authenticate(email = 'hamidipour97@gmail.com', password = 'my password')
<User: amir-mohammad-HP>
see the user model returned
build Authentication Form ( use with Django LoginView )
unfortunately Django hadn't provide a built in email authentication system and so there is no default login by email but we can do the trick by our selves
we had just built out custom authentication backend and now we can use it to create our custom email authentication form to use with builtin django.contrib.auth.views.LoginView
let's create a file and name it ./authenticate/forms.py
from django import forms
from django.utils.translation import gettext_lazy as _
from django.contrib.auth import authenticate, get_user_model
from django.utils.text import capfirst
from django.core.exceptions import ValidationError
UserModel = get_user_model()
class EmailAuthenticationForm(forms.Form):
"""
Base class for authenticating users by email. Extend this to get a form that accepts
email/password logins.
"""
email = forms.EmailField(
label = _("Email"),
widget = forms.EmailInput(),
)
password = forms.CharField(
label=_("Password"),
strip=False,
widget=forms.PasswordInput(attrs={"autocomplete": "current-password"}),
)
error_messages = {
"invalid_login": _(
"Please enter a correct %(email)s and password. Note that both "
"fields may be case-sensitive."
),
"inactive": _("This account is inactive."),
}
def __init__(self, request=None, *args, **kwargs):
"""
The 'request' parameter is set for custom auth use by subclasses.
The form data comes in via the standard 'data' kwarg.
"""
self.request = request
self.user_cache = None
super().__init__(*args, **kwargs)
# Set the max length and label for the "emial" field.
self.email_field = UserModel._meta.get_field(UserModel.EMAIL_FIELD)
email_max_length = self.email_field.max_length or 254
self.fields["email"].max_length = email_max_length
self.fields["email"].widget.attrs["maxlength"] = email_max_length
if self.fields["email"].label is None:
self.fields["email"].label = capfirst(self.email_field.verbose_name)
def clean(self):
email = self.cleaned_data.get("email")
password = self.cleaned_data.get("password")
if email is not None and password:
self.user_cache = authenticate(
self.request, email=email, password=password
)
if self.user_cache is None:
raise self.get_invalid_login_error()
else:
self.confirm_login_allowed(self.user_cache)
return self.cleaned_data
def confirm_login_allowed(self, user):
"""
Controls whether the given User may log in. This is a policy setting,
independent of end-user authentication. This default behavior is to
allow login by active users, and reject login by inactive users.
If the given user cannot log in, this method should raise a
``ValidationError``.
If the given user may log in, this method should return None.
"""
if not user.is_active:
raise ValidationError(
self.error_messages["inactive"],
code="inactive",
)
def get_user(self):
return self.user_cache
def get_invalid_login_error(self):
return ValidationError(
self.error_messages["invalid_login"],
code="invalid_login",
params={"email": self.email_field.verbose_name},
)
this is just a copy of django.contrib.auth.forms.AuthenticationForm that overwritten for email
to use this form by django,need to include django.contrib.auth.views in urlpatterns:
from django.urls import path
from django.contrib.auth.views import LoginView
from authenticate.forms import EmailAuthenticationForm
urlpatterns = [
path("login/",
LoginView.as_view(authentication_form = EmailAuthenticationForm),
name = 'login'
),
...,
]
NOTE : if you're not familiar with authentication views you can read URLs & views authentication customizing