authn: provide a single field for username and phone number (#72449) #41

Merged
pmarillonnet merged 1 commits from wip/72449-login-authn-single-identifier-field into main 2023-05-02 11:04:16 +02:00
6 changed files with 34 additions and 47 deletions

View File

@ -20,9 +20,11 @@ import functools
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend as BaseModelBackend
from django.db import models
from phonenumbers import PhoneNumberFormat, format_number, is_valid_number
from authentic2.backends import get_user_queryset
from authentic2.user_login_failure import user_login_failure, user_login_success
from authentic2.utils.misc import parse_phone_number
from .. import app_settings
@ -41,20 +43,15 @@ class ModelBackend(BaseModelBackend):
"""
def get_query(self, username, realm=None, ou=None):
UserModel = get_user_model()
username_field = 'username'
queries = []
try:
if app_settings.A2_ACCEPT_EMAIL_AUTHENTICATION and UserModel._meta.get_field('email'):
queries.append(models.Q(**{'email__iexact': username}))
except models.FieldDoesNotExist:

Tu peux virer les deux FieldDoesNotExist ça doit date d'une époque où je pensais réutiliser authentic avec un autre model User. Ce train est parti. (ou alors j'ai oublié un truc mais normalement le champ email et phone sont toujours là).

Tu peux virer les deux FieldDoesNotExist ça doit date d'une époque où je pensais réutiliser authentic avec un autre model User. Ce train est parti. (ou alors j'ai oublié un truc mais normalement le champ email et phone sont toujours là).
pass
try:
if app_settings.A2_ACCEPT_PHONE_AUTHENTICATION and UserModel._meta.get_field('phone'):
# try with the phone number as user identifier
queries.append(models.Q(phone=username))
except models.FieldDoesNotExist:
pass
if app_settings.A2_ACCEPT_EMAIL_AUTHENTICATION:
queries.append(models.Q(**{'email__iexact': username}))
if app_settings.A2_ACCEPT_PHONE_AUTHENTICATION:
# 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)}
queries.append(models.Q(**query))
if realm is None:
queries.append(models.Q(**{username_field: username}))

View File

@ -20,14 +20,13 @@ import math
from django import forms
from django.conf import settings
from django.contrib.auth import forms as auth_forms
from django.contrib.auth import get_user_model
from django.forms.widgets import Media
from django.utils import html
from django.utils.encoding import force_str
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from authentic2.forms.fields import PasswordField, PhoneField
from authentic2.forms.fields import PasswordField
from authentic2.utils.lazy import lazy_label
from .. import app_settings
@ -39,12 +38,7 @@ from ..utils import misc as utils_misc
class AuthenticationForm(auth_forms.AuthenticationForm):
username = auth_forms.UsernameField(
widget=forms.TextInput(attrs={'autofocus': True}),
required=False,
)
phone = PhoneField(
label=_('Phone number'),
help_text=_('Your mobile phone number if declared in your user account.'),
required=False,
required=True,
)
password = PasswordField(label=_('Password'))
remember_me = forms.BooleanField(
@ -59,7 +53,7 @@ class AuthenticationForm(auth_forms.AuthenticationForm):
queryset=OU.objects.all(),
)
field_order = ['username', 'phone', 'password', 'ou', 'remember_me']
field_order = ['username', 'password', 'ou', 'remember_me']
def __init__(self, *args, **kwargs):
preferred_ous = kwargs.pop('preferred_ous', [])
@ -73,10 +67,6 @@ class AuthenticationForm(auth_forms.AuthenticationForm):
factor=app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR,
)
if not app_settings.A2_ACCEPT_PHONE_AUTHENTICATION or not get_user_model()._meta.get_field('phone'):
del self.fields['phone']
self.fields['username'].required = True
if not self.authenticator.remember_me:
del self.fields['remember_me']
@ -103,16 +93,6 @@ class AuthenticationForm(auth_forms.AuthenticationForm):
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
if (
app_settings.A2_ACCEPT_PHONE_AUTHENTICATION
and get_user_model()._meta.get_field('phone')
and not username
):
# Django's ModelBackend only understands a single field as 'username' identifier
# for authentication purposes. In authentic it is already used for authn using the
# email address. Below is the addition of the phone number as authn identifier.
self.cleaned_data['username'] = username = self.cleaned_data.get('phone')
keys = None
if username and password:
keys = self.exp_backoff_keys()

View File

@ -747,6 +747,10 @@ def login_password_login(request, authenticator, *args, **kwargs):
form.fields['username'].widget.attrs['autofocus'] = not (bool(context.get('block_index')))
if app_settings.A2_ACCEPT_EMAIL_AUTHENTICATION:

get_user_model()._meta.get_field('phone') c'est toujours vrai.

`get_user_model()._meta.get_field('phone')` c'est toujours vrai.
form.fields['username'].label = _('Username or email')
if app_settings.A2_ACCEPT_PHONE_AUTHENTICATION:
form.fields['username'].label = _('Username, email or phone number')
elif app_settings.A2_ACCEPT_PHONE_AUTHENTICATION:
form.fields['username'].label = _('Username or phone number')
if app_settings.A2_USERNAME_LABEL:
form.fields['username'].label = app_settings.A2_USERNAME_LABEL
is_secure = request.is_secure

View File

@ -3221,16 +3221,15 @@ def test_api_basic_authz_user_phone_number(app, settings, superuser):
headers = basic_authorization_header('+33499985643', superuser.username)
app.get('/api/users/', headers=headers, status=200)
# non E.164 representations
headers = basic_authorization_header('+33499985643 ', superuser.username)
app.get('/api/users/', headers=headers, status=401)
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=401)
app.get('/api/users/', headers=headers, status=200)
headers = basic_authorization_header('0499985643', superuser.username)
app.get('/api/users/', headers=headers, status=401)
app.get('/api/users/', headers=headers, status=200)
# E.164 yet wrong phone number
# wrong phone number
headers = basic_authorization_header('+33499985644', superuser.username)
app.get('/api/users/', headers=headers, status=401)

View File

@ -90,10 +90,22 @@ def test_failure_no_means_of_authentication(db, app, nomail_user, settings):
def test_required_username_identifier(db, app, settings, caplog):
response = app.get('/login/')
assert not response.pyquery('span.optional')
assert response.pyquery('label[for="id_username"]').text() == 'Username or email:'
settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
response = app.get('/login/')
assert response.pyquery('span.optional')
assert not response.pyquery('span.optional')
assert response.pyquery('label[for="id_username"]').text() == 'Username, email or phone number:'
settings.A2_ACCEPT_EMAIL_AUTHENTICATION = False
response = app.get('/login/')
assert not response.pyquery('span.optional')
assert response.pyquery('label[for="id_username"]').text() == 'Username or phone number:'
settings.A2_ACCEPT_PHONE_AUTHENTICATION = False
response = app.get('/login/')
assert not response.pyquery('span.optional')
assert response.pyquery('label[for="id_username"]').text() == 'Username:'
def test_login_form_fields_order(db, app, settings, ou1, ou2):
@ -111,8 +123,6 @@ def test_login_form_fields_order(db, app, settings, ou1, ou2):
assert list(response.form.fields.keys()) == [
'csrfmiddlewaretoken',
'username',
'phone_0',
'phone_1',
'password',
'login-password-submit',
]
@ -126,8 +136,6 @@ def test_login_form_fields_order(db, app, settings, ou1, ou2):
assert list(response.form.fields.keys()) == [
'csrfmiddlewaretoken',
'username',
'phone_0',
'phone_1',
'password',
'ou',
'remember_me',

View File

@ -90,8 +90,7 @@ def login(
phone = login
else:
phone = user.phone
form.set('phone_0', '33')
form.set('phone_1', phone)
form.set('username', phone)
# password is supposed to be the same as username
form.set('password', password or (user.clear_password if hasattr(user, 'clear_password') else username))
if remember_me is not None: