Skip to content

Commit

Permalink
[feature] Allowed authentication with email or phone number #206
Browse files Browse the repository at this point in the history
Closes #206

Co-authored-by: Federico Capoano <federico.capoano@gmail.com>
  • Loading branch information
NoumbissiValere and nemesifier authored Dec 30, 2020
1 parent 4569b60 commit 69cd57a
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 2 deletions.
24 changes: 23 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,14 @@ Setup (integrate in an existing django project)
'drf_yasg',
]
also add ``AUTH_USER_MODEL`` and ``SITE_ID`` to your ``settings.py``::
also add ``AUTH_USER_MODEL``, ``SITE_ID`` and ``AUTHENTICATION_BACKENDS``
to your ``settings.py``::

AUTH_USER_MODEL = 'openwisp_users.User'
SITE_ID = 1
AUTHENTICATION_BACKENDS = [
'openwisp_users.backends.UsersAuthenticationBackend',
]

``urls.py``:

Expand Down Expand Up @@ -491,6 +495,24 @@ to avoid generating database queries each time is called.
>>> user.has_permission('openwisp_users.add_user')
... True
Authentication Backend
----------------------

The authentication backend in ``openwisp_users.backends.UsersAuthenticationBackend``
allows users to authenticate using their
``email`` or ``phone_number`` instead of their ``username``.
Authenticating with the ``username`` is still allowed,
but ``email`` and ``phone_number`` have precedence.

It can be used as follows:

.. code-block:: python
from openwisp_users.backends import UsersAuthenticationBackend
backend = UsersAuthenticationBackend()
backend.authenticate(request, phone_number, password)
Django REST Framework Permission Classes
----------------------------------------

Expand Down
24 changes: 24 additions & 0 deletions openwisp_users/backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q

User = get_user_model()


class UsersAuthenticationBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
queryset = self.get_users(username)
try:
# can not use queryset.first() because it orders the queryset
# by pk before returning the first object which is not what we want
user = queryset[0]
except IndexError:
return None
if user.check_password(password) and self.user_can_authenticate(user):
return user
return None

def get_users(self, identifier):
return User.objects.filter(
Q(email=identifier) | Q(phone_number=identifier) | Q(username=identifier)
)
2 changes: 1 addition & 1 deletion openwisp_users/tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1585,7 +1585,7 @@ def test_multitenant_admin_manager_only(self):
staff.groups.add(admin_group)
self._create_org_user(organization=other_org, user=staff, is_admin=False)
self._create_org_user(organization=staff_org, user=staff, is_admin=True)
self._login(staff)
self._login(staff.username)
user1 = self._create_user(username='user1__otherorg', email='user1@user1.org')
self._create_org_user(organization=other_org, user=user1, is_admin=False)
self._test_multitenant_admin(
Expand Down
106 changes: 106 additions & 0 deletions openwisp_users/tests/test_backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from uuid import UUID

from django.test import TestCase
from django.test.utils import override_settings

from openwisp_users.backends import UsersAuthenticationBackend

from .utils import TestOrganizationMixin

auth_backend = UsersAuthenticationBackend()


class TestBackends(TestOrganizationMixin, TestCase):
def _test_user_auth_backend_helper(self, username, password, pk):
self.client.login(username=username, password=password)
self.assertIn('_auth_user_id', self.client.session)
self.assertEqual(
UUID(self.client.session['_auth_user_id'], version=4), pk,
)
self.client.logout()
self.assertNotIn('_auth_user_id', self.client.session)

@override_settings(
AUTHENTICATION_BACKENDS=('openwisp_users.backends.UsersAuthenticationBackend',)
)
def test_user_auth_backend(self):
user = self._create_user(
username='tester',
email='tester@gmail.com',
phone_number='+237675579231',
password='tester',
)
with self.subTest('Test login with username'):
self._test_user_auth_backend_helper('tester', 'tester', user.pk)

with self.subTest('Test login with email'):
self._test_user_auth_backend_helper('tester@gmail.com', 'tester', user.pk)

with self.subTest('Test login with phone_number'):
self._test_user_auth_backend_helper('+237675579231', 'tester', user.pk)

@override_settings(
AUTHENTICATION_BACKENDS=('openwisp_users.backends.UsersAuthenticationBackend',)
)
def test_user_with_email_as_username_auth_backend(self):
user = self._create_user(
username='tester',
email='tester@gmail.com',
phone_number='+237675579231',
password='tester',
)
self._create_user(
username='tester@gmail.com',
email='tester1@gmail.com',
phone_number='+237675579232',
password='tester1',
)
self._test_user_auth_backend_helper(user.email, 'tester', user.pk)

@override_settings(
AUTHENTICATION_BACKENDS=('openwisp_users.backends.UsersAuthenticationBackend',)
)
def test_user_with_phone_number_as_username_auth_backend(self):
user = self._create_user(
username='tester',
email='tester@gmail.com',
phone_number='+237675579231',
password='tester',
)
self._create_user(
username='+237675579231',
email='tester1@gmail.com',
phone_number='+237675579232',
password='tester1',
)
self._test_user_auth_backend_helper(user.phone_number, 'tester', user.pk)

def test_auth_backend_get_user(self):
user = self._create_user(
username='tester',
email='tester@gmail.com',
phone_number='+237675579231',
password='tester',
)
user1 = self._create_user(
username='tester1',
email='tester1@gmail.com',
phone_number='+237675579232',
password='tester1',
)

with self.subTest('get user with invalid identifier'):
self.assertEqual(len(auth_backend.get_users('invalid')), 0)

with self.subTest('get user with email'):
user1.username = user.email
user1.save()
self.assertEqual(auth_backend.get_users(user.email)[0], user)

with self.subTest('get user with phone_number'):
user1.username = user.phone_number
user1.save()
self.assertEqual(auth_backend.get_users(user.phone_number)[0], user)

with self.subTest('get user with username'):
self.assertEqual(auth_backend.get_users(user.username)[0], user)
6 changes: 6 additions & 0 deletions tests/openwisp2/sample_users/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from openwisp_users.tests.test_api.test_views import (
TestRestFrameworkViews as BaseTestRestFrameworkViews,
)
from openwisp_users.tests.test_backends import TestBackends as BaseTestBackends
from openwisp_users.tests.test_models import TestUsers as BaseTestUsers

additional_fields = [
Expand Down Expand Up @@ -98,10 +99,15 @@ class RatelimitTests(BaseRatelimitTests):
pass


class TestBackends(BaseTestBackends):
pass


del BaseTestUsersAdmin
del BaseTestBasicUsersIntegration
del BaseTestMultitenantAdmin
del BaseTestUsers
del BaseAuthenticationTests
del BaseRatelimitTests
del BaseTestRestFrameworkViews
del BaseTestBackends
4 changes: 4 additions & 0 deletions tests/openwisp2/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@
}
]

AUTHENTICATION_BACKENDS = [
'openwisp_users.backends.UsersAuthenticationBackend',
]

if not TESTING and SHELL:
LOGGING = {
'disable_existing_loggers': False,
Expand Down

0 comments on commit 69cd57a

Please sign in to comment.