Implementing a custom backend for email authentication

Hi everyone,

I want to create an authentication system in which the username is the user’s email adress like so :

models.py :

from django.db.models import EmailField, CharField
from django.contrib.auth.models import AbstractUser, BaseUserManager

class UserManager(BaseUserManager):
    def get_by_natural_key(self, username):
        user = UserModel.objects.get(email=username)
        return user

class UserModel(AbstractUser):
    username = None
    email = EmailField(max_length=20, unique=True, blank=False)
    password = CharField(max_length=20, blank=False)
    objects = UserManager()

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = []
    
    def __str__(self):
        return self.email

forms.py :

from django.forms import (
    ModelForm,
    Form,
    EmailField,
    CharField,
)
from .models import UserModel

class signupUserForm(ModelForm):
    password1 = CharField(max_length=20, required=True)
    password2 = CharField(max_length=20, required=True)

    class Meta:
        model = UserModel
        fields = ["email"]

class loginUserForm(Form):
    email = EmailField(max_length=20, required=True)
    password = CharField(max_length=20, required=True)

backends.py :

from django.contrib.auth.backends import BaseBackend

class EmailBackend(BaseBackend):
    def authenticate(self, username=None, password=None):
        try:
            user = UserModel.objects.get(email=username)
        except UserModel.DoesNotExist:
            return None
        return user

output :

>>> from site_app.forms import signupUserForm, loginUserForm
>>> from site_app.models import UserModel
>>> from site_app.models import UserManager
>>> from site_app.backends import EmailBackend
>>> form = signupUserForm({"email":"test@mail.com","password1":"pwd","password2":"pwd"})                                                             >>> form.is_valid()
True
>>> form.save()
<UserModel: test@mail.com>
>>> UserModel.objects.get(email="test@mail.com")
<UserModel: test@mail.com>
>>> form = loginUserForm({"email":"test@mail.com","password":"pwd"})
>>> form.is_valid()
True
>>> from django.contrib.auth import authenticate, login
>>> user = authenticate(username=form.cleaned_data["email"], password=form.cleaned_data["password"])
>>> user
None

Why does my “user” is None ?

Best Regards

I think I’d want to verify that authenticate is calling EmailBackend.authenticate. To that end, I’d toss a couple of print statements into it to verify that I’m getting the parameters passed in that I’m expecting along with verifying that user is being set by the query. I would expect this behavior if for some reason your EmailBackend isn’t wired-in correctly.

Hi @KenWhitesell !

There is an issue indeed with my custom backend :

backends.py

from django.contrib.auth.backends import BaseBackend

class EmailBackend(BaseBackend):
    def authenticate(self, username=None, password=None):
        print("AUTHENTICATION FROM CUSTOM BACKEND")
        try:
            user = UserModel.objects.get(email=username)
            print("USER = " + str(user))
        except UserModel.DoesNotExist:
            return None
        return user

output

>>> from site_app.backends import EmailBackend
>>> from django.contrib.auth import authenticate, login
>>> user = authenticate(username="test@mail.com",password="pwd")
>>> print(user)
None 

It is weird because I did write the following statements in my settings.py file :

AUTH_USER_MODEL = "site_app.UserModel"
AUTHENTICATION_BACKEND = (
    "site_app.backends.EmailBackend",
)

It’s AUTHENTICATION_BACKENDS, not AUTHENTICATION_BACKEND.

1 Like

Have you considered a third party package to do email authentication? Writing a custom authentication backend is a heavy task, IMO.

You might want to consider django-allauth which can authenticate users via email. @wsvincent even has a great tutorial for doing exactly what you’re describing at https://learndjango.com/tutorials/django-log-in-email-not-username.

1 Like

Hmm, still doesn’t work…

Seems like my EmailBackend authenticate method is not called at all

@mblayman : Thanks for the tip but I prefer to know how to tweak things myself as I practice for my own entertainment and see it as an exercise to understand what’s going under the hood :wink:

Digging into authenticate’s source (and the docs) a little deeper, it looks like the authenticate method may require the request as a first positional parameter. Try changing your signature to:

def authenticate(self, request, username=None, password=None):

and passing None as the first parameter.

@KenWhitesell : That’s it, it works now !

>>> from site_app.backends import EmailBackend 
>>> from django.contrib.auth import authenticate, login
>>> user = authenticate(None,username="test@mail.com",password="pwd")
AUTHENTICATION FROM CUSTOM BACKEND
USER = test@mail.com 
1 Like

Cool! You’ll definitely learn a ton writing your own auth backend. Good luck!

I now have another issue, when I try to login I get :

>>> login(None,user)
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "C:\Users\Théo\AppData\Local\Programs\Python\Python39\lib\site-packages\django\contrib\auth\__init__.py", line 99, in login
    if SESSION_KEY in request.session:
AttributeError: 'NoneType' object has no attribute 'session'

Does that mean that I also have to implement my own login method in my custom backend ? (Actually I think it is due to the fact that I passed None to the login call but not sure)

You are correct (passing None to login being the issue).

You would need to pass a request object to it for it to be able to access the session attributes of it.

Here is the error I get even when testing my app through a form :

AttributeError: 'AnonymousUser' object has no attribute '_meta'

Do you know what does that mean ?

<lots of guesses here>
I’m going to guess that this is being thrown in the login() call - if so, it’s saying that you’re passing the AnonymousUser into that function.

Again guessing, it looks like it might be coming from this line in that method:
request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)

</lots of guesses here>

Without more context (like the view that is running when this error occurs - you have the form and model above, but I don’t see the view(s)), I can’t really offer anything more specific.

Hi @KenWhitesell, here is the views.py file :

from django.shortcuts import render, redirect
from .forms import signupUserForm, loginUserForm
from django.contrib.auth import authenticate, login

def index(request):
    return render(request, "index.html", {})

def signupUser(request):
    if request.method == "POST":
        form = signupUserForm(request.POST)

        if form.is_valid():
            form.save()
            return redirect("site_app:index")
    else:
        form = signupUserForm()

    return render(request, "signup.html", {"form":form})

def loginUser(request):
    if request.method == "POST":
        form = loginUserForm(request.POST)

        if form.is_valid():
            user = authenticate(request,username=form.cleaned_data["email"],password=form.cleaned_data["password"])

            if user is not None:
                login(request, None)
                return redirect("site_app:index")
    else:
        form = loginUserForm()
    return render(request, "login.html", {"form":form})

I found the bug… hmm sometimes I wonder why we human beings are so focused on something that we can’t read anymore ahah, I was passing “None” to the login method.

            if user is not None:
                login(request, None)
                return redirect("site_app:index")

            if user is not None:
=>                login(request, user)
                return redirect("site_app:index")
1 Like