authn: let the model backend decide which phone attribute to use (#78046)
This commit is contained in:
parent
eef2e168a7
commit
f9b9d9ac88
|
@ -19,10 +19,12 @@ import functools
|
|||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend as BaseModelBackend
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from phonenumbers import PhoneNumberFormat, format_number, is_valid_number
|
||||
|
||||
from authentic2.backends import get_user_queryset
|
||||
from authentic2.models import AttributeValue
|
||||
from authentic2.user_login_failure import user_login_failure, user_login_success
|
||||
from authentic2.utils.misc import get_password_authenticator, parse_phone_number
|
||||
|
||||
|
@ -46,12 +48,19 @@ class ModelBackend(BaseModelBackend):
|
|||
username_field = 'username'
|
||||
queries = []
|
||||
password_authenticator = get_password_authenticator()
|
||||
user_ct = ContentType.objects.get_for_model(get_user_model())
|
||||
if password_authenticator.accept_email_authentication:
|
||||
queries.append(models.Q(**{'email__iexact': username}))
|
||||
if password_authenticator.is_phone_authn_active:
|
||||
# try with the phone number as user identifier
|
||||
if (pn := parse_phone_number(username)) and is_valid_number(pn):
|
||||
query = {'phone': format_number(pn, PhoneNumberFormat.E164)}
|
||||
user_ids = AttributeValue.objects.filter(
|
||||
multiple=False,
|
||||
content_type=user_ct,
|
||||
attribute=password_authenticator.phone_identifier_field,
|
||||
content=format_number(pn, PhoneNumberFormat.E164),
|
||||
).values_list('object_id', flat=True)
|
||||
query = {'id__in': user_ids}
|
||||
queries.append(models.Q(**query))
|
||||
|
||||
if realm is None:
|
||||
|
|
|
@ -3232,3 +3232,53 @@ def test_api_basic_authz_user_phone_number(app, settings, superuser, phone_activ
|
|||
# wrong phone number
|
||||
headers = basic_authorization_header('+33499985644', superuser.username)
|
||||
app.get('/api/users/', headers=headers, status=401)
|
||||
|
||||
|
||||
def test_api_basic_authz_user_phone_number_nondefault_attribute(app, settings, superuser):
|
||||
phone, dummy = Attribute.objects.get_or_create(
|
||||
name='another_phone',
|
||||
kind='phone_number',
|
||||
defaults={'label': 'Another phone'},
|
||||
)
|
||||
LoginPasswordAuthenticator.objects.update(
|
||||
accept_phone_authentication=True,
|
||||
phone_identifier_field=phone,
|
||||
)
|
||||
|
||||
headers = {'Authorization': 'Basic abc'}
|
||||
app.get('/api/users/', headers=headers, status=401)
|
||||
|
||||
headers = basic_authorization_header(superuser)
|
||||
app.get('/api/users/', headers=headers, status=200)
|
||||
|
||||
superuser.attributes.another_phone = '+33499985643'
|
||||
superuser.save()
|
||||
|
||||
# authn valid
|
||||
headers = basic_authorization_header('+33499985643', superuser.username)
|
||||
app.get('/api/users/', headers=headers, status=200)
|
||||
|
||||
headers = basic_authorization_header('+33499985643 ', superuser.username)
|
||||
app.get('/api/users/', headers=headers, status=200)
|
||||
|
||||
headers = basic_authorization_header('+33-4/99/985643', superuser.username)
|
||||
app.get('/api/users/', headers=headers, status=200)
|
||||
|
||||
headers = basic_authorization_header('0499985643', superuser.username)
|
||||
app.get('/api/users/', headers=headers, status=200)
|
||||
|
||||
# wrong phone number
|
||||
headers = basic_authorization_header('+33499985644', superuser.username)
|
||||
app.get('/api/users/', headers=headers, status=401)
|
||||
|
||||
# having another known phone does not change anything
|
||||
superuser.phone = '+33122334455'
|
||||
superuser.save()
|
||||
|
||||
# authn valid
|
||||
headers = basic_authorization_header('+33499985643', superuser.username)
|
||||
app.get('/api/users/', headers=headers, status=200)
|
||||
|
||||
# wrong phone number
|
||||
headers = basic_authorization_header('+33499985644', superuser.username)
|
||||
app.get('/api/users/', headers=headers, status=401)
|
||||
|
|
|
@ -14,7 +14,9 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
|
||||
from authentic2.backends import is_user_authenticable
|
||||
from authentic2.models import Attribute
|
||||
from authentic2.utils.misc import authenticate
|
||||
|
||||
|
||||
|
@ -51,6 +53,40 @@ def test_model_backend_phone_number(settings, db, simple_user, nomail_user, ou1,
|
|||
assert is_user_authenticable(nomail_user)
|
||||
|
||||
|
||||
def test_model_backend_phone_number_nondefault_attribute(settings, db, simple_user, nomail_user, ou1):
|
||||
phone, dummy = Attribute.objects.get_or_create(
|
||||
name='another_phone',
|
||||
kind='phone_number',
|
||||
defaults={'label': 'Another phone'},
|
||||
)
|
||||
LoginPasswordAuthenticator.objects.update(
|
||||
accept_phone_authentication=True,
|
||||
phone_identifier_field=phone,
|
||||
)
|
||||
|
||||
nomail_user.phone = ''
|
||||
nomail_user.attributes.another_phone = '+33123456789'
|
||||
nomail_user.save()
|
||||
simple_user.phone = ''
|
||||
simple_user.attributes.another_phone = '+33123456789'
|
||||
simple_user.save()
|
||||
assert authenticate(username=simple_user.attributes.another_phone, password=simple_user.username)
|
||||
assert is_user_authenticable(simple_user)
|
||||
assert authenticate(username=nomail_user.attributes.another_phone, password=nomail_user.username)
|
||||
assert is_user_authenticable(nomail_user)
|
||||
|
||||
nomail_user.attributes.another_phone = ''
|
||||
nomail_user.phone = '+33123456789'
|
||||
nomail_user.save()
|
||||
simple_user.attributes.another_phone = ''
|
||||
simple_user.phone = '+33123456789'
|
||||
simple_user.save()
|
||||
assert not authenticate(username=simple_user.phone, password=simple_user.username)
|
||||
assert is_user_authenticable(simple_user)
|
||||
assert not authenticate(username=nomail_user.phone, password=nomail_user.username)
|
||||
assert is_user_authenticable(nomail_user)
|
||||
|
||||
|
||||
def test_model_backend_phone_number_email(settings, db, simple_user, phone_activated_authn):
|
||||
simple_user.attributes.phone = '+33123456789'
|
||||
simple_user.save()
|
||||
|
|
|
@ -21,6 +21,7 @@ from django.contrib.auth import get_user_model
|
|||
|
||||
from authentic2 import models
|
||||
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
|
||||
from authentic2.apps.journal.models import Event
|
||||
from authentic2.utils.misc import get_authenticators, get_token_login_url
|
||||
|
||||
from .utils import assert_event, login, set_service
|
||||
|
@ -44,6 +45,23 @@ def test_success_email_with_phone_authn_activated(db, app, simple_user, settings
|
|||
assert_event('user.logout', user=simple_user, session=session)
|
||||
|
||||
|
||||
def test_success_email_with_phone_authn_nondefault_attribute(db, app, simple_user, settings):
|
||||
phone, dummy = models.Attribute.objects.get_or_create(
|
||||
name='another_phone',
|
||||
kind='phone_number',
|
||||
defaults={'label': 'Another phone'},
|
||||
)
|
||||
LoginPasswordAuthenticator.objects.update(
|
||||
accept_phone_authentication=True,
|
||||
phone_identifier_field=phone,
|
||||
)
|
||||
login(app, simple_user)
|
||||
assert_event('user.login', user=simple_user, session=app.session, how='password-on-https')
|
||||
session = app.session
|
||||
app.get('/logout/').form.submit()
|
||||
assert_event('user.logout', user=simple_user, session=session)
|
||||
|
||||
|
||||
def test_success_phone_authn_nomail_user(db, app, nomail_user, settings, phone_activated_authn):
|
||||
nomail_user.attributes.phone = '+33123456789'
|
||||
nomail_user.save()
|
||||
|
@ -54,6 +72,38 @@ def test_success_phone_authn_nomail_user(db, app, nomail_user, settings, phone_a
|
|||
assert_event('user.logout', user=nomail_user, session=session)
|
||||
|
||||
|
||||
def test_success_phone_authn_nomail_user_nondefault_attribute(
|
||||
db, app, nomail_user, settings, phone_activated_authn
|
||||
):
|
||||
phone, dummy = models.Attribute.objects.get_or_create(
|
||||
name='another_phone',
|
||||
kind='phone_number',
|
||||
defaults={'label': 'Another phone'},
|
||||
)
|
||||
LoginPasswordAuthenticator.objects.update(
|
||||
accept_phone_authentication=True,
|
||||
phone_identifier_field=phone,
|
||||
)
|
||||
nomail_user.phone = ''
|
||||
nomail_user.attributes.another_phone = '+33123456789'
|
||||
nomail_user.save()
|
||||
login(app, nomail_user, login='123456789', phone_authn=True)
|
||||
assert_event('user.login', user=nomail_user, session=app.session, how='password-on-https')
|
||||
session = app.session
|
||||
app.get('/logout/').form.submit()
|
||||
assert_event('user.logout', user=nomail_user, session=session)
|
||||
|
||||
Event.objects.all().delete()
|
||||
|
||||
nomail_user.phone = '+33122334455'
|
||||
nomail_user.save()
|
||||
login(app, nomail_user, login='123456789', phone_authn=True)
|
||||
assert_event('user.login', user=nomail_user, session=app.session, how='password-on-https')
|
||||
session = app.session
|
||||
app.get('/logout/').form.submit()
|
||||
assert_event('user.logout', user=nomail_user, session=session)
|
||||
|
||||
|
||||
def test_success_phone_authn_simple_user(db, app, simple_user, settings, phone_activated_authn):
|
||||
simple_user.attributes.phone = '+33123456789'
|
||||
simple_user.save()
|
||||
|
@ -64,6 +114,36 @@ def test_success_phone_authn_simple_user(db, app, simple_user, settings, phone_a
|
|||
assert_event('user.logout', user=simple_user, session=session)
|
||||
|
||||
|
||||
def test_success_phone_authn_simpler_user_nondefault_attribute(db, app, simple_user, settings):
|
||||
phone, dummy = models.Attribute.objects.get_or_create(
|
||||
name='another_phone',
|
||||
kind='phone_number',
|
||||
defaults={'label': 'Another phone'},
|
||||
)
|
||||
LoginPasswordAuthenticator.objects.update(
|
||||
accept_phone_authentication=True,
|
||||
phone_identifier_field=phone,
|
||||
)
|
||||
simple_user.phone = ''
|
||||
simple_user.attributes.another_phone = '+33123456789'
|
||||
simple_user.save()
|
||||
login(app, simple_user, login='123456789', phone_authn=True)
|
||||
assert_event('user.login', user=simple_user, session=app.session, how='password-on-https')
|
||||
session = app.session
|
||||
app.get('/logout/').form.submit()
|
||||
assert_event('user.logout', user=simple_user, session=session)
|
||||
|
||||
Event.objects.all().delete()
|
||||
|
||||
simple_user.phone = '+33122334455'
|
||||
simple_user.save()
|
||||
login(app, simple_user, login='123456789', phone_authn=True)
|
||||
assert_event('user.login', user=simple_user, session=app.session, how='password-on-https')
|
||||
session = app.session
|
||||
app.get('/logout/').form.submit()
|
||||
assert_event('user.logout', user=simple_user, session=session)
|
||||
|
||||
|
||||
def test_failure(db, app, simple_user):
|
||||
login(app, simple_user, password='wrong', fail=True)
|
||||
assert_event('user.login.failure', user=simple_user, username=simple_user.username)
|
||||
|
|
Loading…
Reference in New Issue