authn: provide a single field for username and phone number (#72449) #41
|
@ -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:
|
||||
|
||||
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}))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
bdauvergne
commented
`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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue
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à).