feat: add custom authentication backend to support login with email or username
All checks were successful
Build and Push Docker Image / build (push) Successful in 28s

- Implemented `EmailOrUsernameBackend` to allow authentication via email or username
- Updated settings to include the new authentication backend
- Modified tests to cover email-based login scenarios
- Fixed user lookup logic in forms to handle case-insensitive email matching
This commit is contained in:
2026-04-21 18:09:52 +02:00
parent dab33a03a1
commit d0651274d6
4 changed files with 57 additions and 2 deletions

View File

@@ -192,6 +192,10 @@ INSTALLED_APPS = [
'users',
]
AUTHENTICATION_BACKENDS = [
"users.auth_backends.EmailOrUsernameBackend",
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',

35
users/auth_backends.py Normal file
View File

@@ -0,0 +1,35 @@
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
UserModel = get_user_model()
class EmailOrUsernameBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
if username is None or password is None:
return None
user = self._get_user_by_username_or_email(username)
if user is None:
return None
if user.check_password(password) and self.user_can_authenticate(user):
return user
return None
def _get_user_by_username_or_email(self, value):
try:
return UserModel._default_manager.get_by_natural_key(value)
except UserModel.DoesNotExist:
email_field = UserModel.get_email_field_name()
try:
return UserModel._default_manager.get(**{f"{email_field}__iexact": value})
except UserModel.DoesNotExist:
return None
except UserModel.MultipleObjectsReturned:
return None

View File

@@ -36,7 +36,7 @@ class LoginForm(forms.Form):
if self.user_cache is not None:
return cleaned_data
user = User.objects.filter(username=email).first()
user = User.objects.filter(email__iexact=email).first()
if user and not user.is_active and user.check_password(password):
raise ValidationError(self.error_messages["inactive"])

View File

@@ -55,7 +55,7 @@ class UserAuthFlowTests(TestCase):
def test_login_with_email_works_for_active_user(self):
user = User.objects.create_user(
username="oleg@example.com",
username="oleg",
email="oleg@example.com",
password="StrongPassword123",
is_active=True,
@@ -69,6 +69,22 @@ class UserAuthFlowTests(TestCase):
self.assertRedirects(response, reverse("users:profile"))
self.assertEqual(int(self.client.session["_auth_user_id"]), user.pk)
def test_login_with_email_works_when_username_differs(self):
user = User.objects.create_user(
username="oleg-old",
email="oleg2@example.com",
password="StrongPassword123",
is_active=True,
)
response = self.client.post(
reverse("users:login"),
data={"email": "oleg2@example.com", "password": "StrongPassword123"},
)
self.assertRedirects(response, reverse("users:profile"))
self.assertEqual(int(self.client.session["_auth_user_id"]), user.pk)
def test_password_reset_sends_email_for_existing_user(self):
User.objects.create_user(
username="maria@example.com",