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
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