Question :
I am trying to create an auth backend to allow my users to log in using either their email address or their username in Django 1.6 with a custom user model. The backend works when I log in with a user name but for some reason does not with an email. Is there something I am forgetting to do?
from django.conf import settings
from django.contrib.auth.models import User
class EmailOrUsernameModelBackend(object):
"""
This is a ModelBacked that allows authentication with either a username or an email address.
"""
def authenticate(self, username=None, password=None):
if '@' in username:
kwargs = {'email': username}
else:
kwargs = {'username': username}
try:
user = User.objects.get(**kwargs)
if user.check_password(password):
return user
except User.DoesNotExist:
return None
def get_user(self, username):
try:
return User.objects.get(pk=username)
except User.DoesNotExist:
return None
Edit: As suggested I have inherited from ModelBackend and installed it in my settings
In my settings I have this
AUTHENTICATION_BACKENDS = (
‘users.backends’,
‘django.contrib.auth.backends.ModelBackend’,
)
And I have changed the backend to this:
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
"""
This is a ModelBacked that allows authentication with either a username or an email address.
"""
def authenticate(self, username=None, password=None):
if '@' in username:
kwargs = {'email': username}
else:
kwargs = {'username': username}
try:
user = User.objects.get(**kwargs)
if user.check_password(password):
return user
except User.DoesNotExist:
return None
def get_user(self, username):
try:
return User.objects.get(pk=username)
except User.DoesNotExist:
return None
Now I get an Module "users" does not define a "backends" attribute/class
error.
Answer #1:
Yet another solution:
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
class EmailOrUsernameModelBackend(ModelBackend):
"""
Authentication backend which allows users to authenticate using either their
username or email address
Source: https://stackoverflow.com/a/35836674/59984
"""
def authenticate(self, request, username=None, password=None, **kwargs):
# n.b. Django <2.1 does not pass the `request`
user_model = get_user_model()
if username is None:
username = kwargs.get(user_model.USERNAME_FIELD)
# The `username` field is allows to contain `@` characters so
# technically a given email address could be present in either field,
# possibly even for different users, so we'll query for all matching
# records and test each one.
users = user_model._default_manager.filter(
Q(**{user_model.USERNAME_FIELD: username}) | Q(email__iexact=username)
)
# Test whether any matched user has the provided password:
for user in users:
if user.check_password(password):
return user
if not users:
# Run the default password hasher once to reduce the timing
# difference between an existing and a non-existing user (see
# https://code.djangoproject.com/ticket/20760)
user_model().set_password(password)
Fixes:
- By default,
@
is not prohibited in the username field, so unless custom User model prohibits@
symbol, it can’t be used to distinguish between username and email. - Technically, there can be two users using the same email, one in the email field, the other in the username. Unless such possibility is restricted, it can lead to either user not being able to authenticate, or unhandled
MultipleObjectsReturned
exception ifUserModel._default_manager.get(Q(username__iexact=username) | Q(email__iexact=username))
is used. - Catching any exception with
except:
is generally bad practice
Downside – if there are two users, using the same email, one in the username, the other in email, and they have the same password, then it’s prone to authenticating the first match. I guess the chances of this is highly unlikely.
Also note: any of the approaches should enforce unique email
field in the User model, since the default User model does not define unique email, which would lead to either unhandled exception in case User.objects.get(email__iexact="...")
is used, or authenticating the first match. In any case, using email to login assumes that email is unique.
Answer #2:
After following the advice given to me above and changing AUTHENTICATION_BACKENDS = ['yourapp.yourfile.EmailOrUsernameModelBackend']
I was getting the error Manager isn't available; User has been swapped for 'users.User'
. This was caused because I was using the default User model instead of my own custom one. Here is the working code.
from django.conf import settings
from django.contrib.auth import get_user_model
class EmailOrUsernameModelBackend(object):
"""
This is a ModelBacked that allows authentication with either a username or an email address.
"""
def authenticate(self, username=None, password=None):
if '@' in username:
kwargs = {'email': username}
else:
kwargs = {'username': username}
try:
user = get_user_model().objects.get(**kwargs)
if user.check_password(password):
return user
except User.DoesNotExist:
return None
def get_user(self, username):
try:
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
return None
Answer #3:
I thought I’d chuck my simpler approach in for anyone else who comes across this:
# -*- coding: utf-8 -*-
from django.contrib.auth import backends, get_user_model
from django.db.models import Q
class ModelBackend(backends.ModelBackend):
def authenticate(self, username=None, password=None, **kwargs):
UserModel = get_user_model()
try:
user = UserModel.objects.get(Q(username__iexact=username) | Q(email__iexact=username))
if user.check_password(password):
return user
except UserModel.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a non-existing user (#20760).
UserModel().set_password(password)
Note:
- disregards
USERNAME_FIELD
, although you could add it back in pretty easily - case insensitive (you could just remove the
__iexact
‘s though to make it not)
Answer #4:
I know this is already answered, however I have found a real neat way to implement login with both e-mail and username using the Django auth views. I did not see anyone use this type of method so I thought I’d share it for simplicity’s sake.
from django.contrib.auth.models import User
class EmailAuthBackend():
def authenticate(self, username=None, password=None):
try:
user = User.objects.get(email=username)
if user.check_password(raw_password=password):
return user
return None
except User.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
Then in your settings.py add this
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'myapp.authentication.EmailAuthBackend',
)
Answer #5:
I wrote the code for this in 2 simple steps :
VIEWS.py
if request.method == 'POST':
userinput = request.POST['username']
try:
username = userbase.objects.get(email=userinput).username
except userbase.DoesNotExist:
username = request.POST['username']
password = request.POST['password']
-
INDEX.html
I created 2 input fields from which 1st is for username/email. I take whatever input is given and try to search the same data in email column of db, if it matches, I return the username and then try to authenticate , if it doesn’t , I use input directly as Username.
I’m using Django 2.2
Answer #6:
Updated version of the same snippet, with improved security. Also, it allow you to enable or disable case sensitive authentication. If you prefer, you can install it directly from pypi.
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from django.conf import settings
###################################
""" DEFAULT SETTINGS + ALIAS """
###################################
try:
am = settings.AUTHENTICATION_METHOD
except:
am = 'both'
try:
cs = settings.AUTHENTICATION_CASE_SENSITIVE
except:
cs = 'both'
#####################
""" EXCEPTIONS """
#####################
VALID_AM = ['username', 'email', 'both']
VALID_CS = ['username', 'email', 'both', 'none']
if (am not in VALID_AM):
raise Exception("Invalid value for AUTHENTICATION_METHOD in project "
"settings. Use 'username','email', or 'both'.")
if (cs not in VALID_CS):
raise Exception("Invalid value for AUTHENTICATION_CASE_SENSITIVE in project "
"settings. Use 'username','email', 'both' or 'none'.")
############################
""" OVERRIDDEN METHODS """
############################
class DualAuthentication(ModelBackend):
"""
This is a ModelBacked that allows authentication
with either a username or an email address.
"""
def authenticate(self, username=None, password=None):
UserModel = get_user_model()
try:
if ((am == 'email') or (am == 'both')):
if ((cs == 'email') or cs == 'both'):
kwargs = {'email': username}
else:
kwargs = {'email__iexact': username}
user = UserModel.objects.get(**kwargs)
else:
raise
except:
if ((am == 'username') or (am == 'both')):
if ((cs == 'username') or cs == 'both'):
kwargs = {'username': username}
else:
kwargs = {'username__iexact': username}
user = UserModel.objects.get(**kwargs)
finally:
try:
if user.check_password(password):
return user
except:
# Run the default password hasher once to reduce the timing
# difference between an existing and a non-existing user.
UserModel().set_password(password)
return None
def get_user(self, username):
UserModel = get_user_model()
try:
return UserModel.objects.get(pk=username)
except UserModel.DoesNotExist:
return None
Answer #7:
if you are using django-rest-auth then the option to authenticate with email address is built in, and may conflict with the other methods proposed. You just need to add the following to settings.py:
ACCOUNT_AUTHENTICATION_METHOD = 'username_email'
ACCOUNT_EMAIL_REQUIRED = False
ACCOUNT_USERNAME_REQUIRED = False
#Following is added to enable registration with email instead of username
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",
)
Django rest auth email instead of username
https://django-allauth.readthedocs.io/en/latest/configuration.html
Note that unless you want to have a single box into which the user can type a username or an email address, you’ll have to do some work in the front end to decide whether to send the login request as email, password or username, password. I did a simple test whether the user’s entry contained an ‘@’ with a ‘.’ further on. I think somebody deliberately creating a username that looks like an email address – but isn’t their email address – is unlikely enough that I’m not supporting it.
Answer #8:
You can simply have a Try block to do this..
use:
email = request.POST['username']
raw_password = request.POST['password']
try:
account = authenticate(username=MyUserAccount.objects.get(email=email).username,password=raw_password)
if account is not None:
login(request, account)
return redirect('home')
except:
account = authenticate(username=email, password=raw_password)
if account is not None:
login(request, account)
return redirect('home')