Ré-initialisation de mot de passe par numéro de téléphone, gérer une next_url signée (#78157) #68
|
@ -152,7 +152,6 @@ default_settings = dict(
|
|||
A2_REGISTRATION_REALM=Setting(
|
||||
default=None, definition='Default realm to assign to self-registrated users'
|
||||
),
|
||||
A2_REGISTRATION_GROUPS=Setting(default=(), definition='Default groups for self-registered users'),
|
||||
A2_PROFILE_FIELDS=Setting(default=(), definition='Fields to show to the user in the profile page'),
|
||||
A2_REGISTRATION_FIELDS=Setting(
|
||||
default=(), definition='Fields from the user model that must appear on the registration form'
|
||||
|
|
|
@ -18,7 +18,7 @@ import re
|
|||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import BaseUserManager, Group
|
||||
from django.contrib.auth.models import BaseUserManager
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.forms import Form
|
||||
|
@ -33,6 +33,7 @@ from .. import app_settings, models
|
|||
from . import profile as profile_forms
|
||||
from .fields import PhoneField, ValidatedEmailField
|
||||
from .honeypot import HoneypotForm
|
||||
from .utils import NextUrlFormMixin
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -163,14 +164,7 @@ class RegistrationCompletionFormNoPassword(profile_forms.BaseUserForm):
|
|||
def save(self, commit=True):
|
||||
self.instance.set_email_verified(True, source='registration')
|
||||
self.instance.is_active = True
|
||||
user = super().save(commit=commit)
|
||||
if commit and app_settings.A2_REGISTRATION_GROUPS:
|
||||
groups = []
|
||||
for name in app_settings.A2_REGISTRATION_GROUPS:
|
||||
group, dummy = Group.objects.get_or_create(name=name)
|
||||
groups.append(group)
|
||||
user.groups = groups
|
||||
return user
|
||||
return super().save(commit=commit)
|
||||
|
||||
|
||||
class RegistrationCompletionForm(RegistrationCompletionFormNoPassword):
|
||||
|
@ -195,7 +189,7 @@ class RegistrationCompletionForm(RegistrationCompletionFormNoPassword):
|
|||
return self.cleaned_data
|
||||
|
||||
|
||||
class InputSMSCodeForm(Form):
|
||||
class InputSMSCodeForm(NextUrlFormMixin, Form):
|
||||
sms_code = CharField(
|
||||
label=_('SMS code'),
|
||||
help_text=_('The code you received by SMS.'),
|
||||
|
|
|
@ -762,7 +762,7 @@ def get_registration_url(request):
|
|||
next_url, request=request, keep_params=True, include=(constants.NONCE_FIELD_NAME,), resolve=False
|
||||
)
|
||||
params = {REDIRECT_FIELD_NAME: next_url}
|
||||
return make_url('registration_register', params=params)
|
||||
return make_url('registration_register', params=params, next_url=next_url, sign_next_url=True)
|
||||
|
||||
|
||||
def get_token_login_url(user):
|
||||
|
@ -1023,21 +1023,23 @@ def get_good_origins():
|
|||
return list(urls)
|
||||
|
||||
|
||||
def good_next_url(request, next_url):
|
||||
def good_next_url(request, next_url, signature_required=False):
|
||||
'''Check if an URL is a good next_url'''
|
||||
if not next_url:
|
||||
return False
|
||||
|
||||
signature = request.POST.get(constants.NEXT_URL_SIGNATURE) or request.GET.get(
|
||||
constants.NEXT_URL_SIGNATURE
|
||||
)
|
||||
if signature_required and not signature:
|
||||
return False
|
||||
if signature:
|
||||
# the signed-next origin is considered trusted, a valid signature bypasses origin checks
|
||||
return crypto.check_hmac_url(settings.SECRET_KEY, next_url, signature)
|
||||
if next_url.startswith('/') and (len(next_url) == 1 or next_url[1] != '/'):
|
||||
return True
|
||||
if same_origin(request.build_absolute_uri(), next_url):
|
||||
return True
|
||||
signature = request.POST.get(constants.NEXT_URL_SIGNATURE) or request.GET.get(
|
||||
constants.NEXT_URL_SIGNATURE
|
||||
)
|
||||
|
||||
if signature:
|
||||
return crypto.check_hmac_url(settings.SECRET_KEY, next_url, signature)
|
||||
|
||||
for origin in get_good_origins():
|
||||
if same_origin(next_url, origin):
|
||||
return True
|
||||
|
|
|
@ -1153,6 +1153,14 @@ class BaseRegistrationView(HomeURLMixin, FormView):
|
|||
|
||||
if app_settings.A2_ACCEPT_PHONE_AUTHENTICATION:
|
||||
phone = form.cleaned_data.pop('phone')
|
||||
if phone:
|
||||
# enforce signed next_urls (only phone-based registration supported yet)
|
||||
if (next_url := self.request.GET.get(REDIRECT_FIELD_NAME)) and utils_misc.good_next_url(
|
||||
self.request, next_url, signature_required=True
|
||||
):
|
||||
self.next_url = next_url
|
||||
else:
|
||||
self.next_url = '/'
|
||||
return self.perform_phone_registration(form, phone)
|
||||
|
||||
return ValidationError(_('No means of registration provided.'))
|
||||
|
@ -1355,16 +1363,26 @@ class InputSMSCodeView(cbv.ValidateCSRFMixin, FormView):
|
|||
duration=120,
|
||||
)
|
||||
|
||||
# TODO next_url management throughout account creation process
|
||||
params = {}
|
||||
if 'next_url' in form.cleaned_data:
|
||||
params[REDIRECT_FIELD_NAME] = form.cleaned_data['next_url']
|
||||
if self.code.kind == models.SMSCode.KIND_REGISTRATION:
|
||||
return utils_misc.redirect(
|
||||
self.request,
|
||||
reverse('registration_activate', kwargs={'registration_token': token.uuid}),
|
||||
reverse(
|
||||
'registration_activate',
|
||||
kwargs={'registration_token': token.uuid},
|
||||
),
|
||||
params=params,
|
||||
)
|
||||
elif self.code.kind == models.SMSCode.KIND_PASSWORD_LOST:
|
||||
return utils_misc.redirect(
|
||||
self.request,
|
||||
reverse('password_reset_confirm', kwargs={'token': token.uuid}),
|
||||
reverse(
|
||||
'password_reset_confirm',
|
||||
kwargs={'token': token.uuid},
|
||||
),
|
||||
params=params,
|
||||
)
|
||||
|
||||
|
||||
|
@ -1396,6 +1414,10 @@ class RegistrationCompletionView(CreateView):
|
|||
url = self.token[REDIRECT_FIELD_NAME]
|
||||
if redirect_url:
|
||||
url = utils_misc.make_url(redirect_url, params={next_field: url})
|
||||
elif (next_url := self.request.GET.get(REDIRECT_FIELD_NAME)) and utils_misc.good_next_url(
|
||||
self.request, next_url
|
||||
):
|
||||
url = next_url
|
||||
else:
|
||||
if redirect_url:
|
||||
url = redirect_url
|
||||
|
|
|
@ -215,7 +215,7 @@ def test_show_condition_with_headers(db, app, settings):
|
|||
|
||||
def test_registration_url_on_login_page(db, app):
|
||||
response = app.get('/login/?next=/whatever')
|
||||
assert 'register/?next=/whatever"' in response
|
||||
assert 'register/?next=/whatever&next-signature=' in response
|
||||
|
||||
|
||||
def test_redirect_login_to_homepage(db, app, settings, simple_user, superuser):
|
||||
|
|
|
@ -1086,3 +1086,67 @@ def test_phone_registration(app, db, settings):
|
|||
|
||||
user = User.objects.get(first_name='John', last_name='Doe')
|
||||
assert user.phone == '+33612345678'
|
||||
|
||||
|
||||
def test_phone_registration_redirect_url(app, db, settings):
|
||||
settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
|
||||
settings.SMS_URL = 'https://foo.whatever.none/'
|
||||
|
||||
resp = app.get('/accounts/consents/').follow()
|
||||
resp = resp.click('Register!')
|
||||
resp.form.set('phone_1', '612345678')
|
||||
with HTTMock(sms_service_mock):
|
||||
resp = resp.form.submit().follow()
|
||||
code = SMSCode.objects.get()
|
||||
resp.form.set('sms_code', code.value)
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
resp.form.set('password1', 'Password0')
|
||||
resp.form.set('password2', 'Password0')
|
||||
resp.form.set('first_name', 'John')
|
||||
resp.form.set('last_name', 'Doe')
|
||||
resp = resp.form.submit()
|
||||
assert resp.location == '/accounts/consents/'
|
||||
resp.follow()
|
||||
user = User.objects.get(first_name='John', last_name='Doe')
|
||||
assert user.phone == '+33612345678'
|
||||
|
||||
|
||||
def test_phone_registration_redirect_url_no_signature_is_ignored(app, db, settings):
|
||||
settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
|
||||
settings.SMS_URL = 'https://foo.whatever.none/'
|
||||
|
||||
resp = app.get(reverse('registration_register') + '?next=/accounts/consents/')
|
||||
resp.form.set('phone_1', '612345678')
|
||||
with HTTMock(sms_service_mock):
|
||||
resp = resp.form.submit().follow()
|
||||
code = SMSCode.objects.get()
|
||||
resp.form.set('sms_code', code.value)
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
resp.form.set('password1', 'Password0')
|
||||
resp.form.set('password2', 'Password0')
|
||||
resp.form.set('first_name', 'John')
|
||||
resp.form.set('last_name', 'Doe')
|
||||
resp = resp.form.submit()
|
||||
assert resp.location == '/'
|
||||
|
||||
|
||||
def test_phone_registration_redirect_url_forged_signature_is_ignored(app, db, settings):
|
||||
settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
|
||||
settings.SMS_URL = 'https://foo.whatever.none/'
|
||||
|
||||
resp = app.get(reverse('registration_register') + '?next=/accounts/consents/&next-signature=abc')
|
||||
resp.form.set('phone_1', '612345678')
|
||||
with HTTMock(sms_service_mock):
|
||||
resp = resp.form.submit().follow()
|
||||
code = SMSCode.objects.get()
|
||||
resp.form.set('sms_code', code.value)
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
resp.form.set('password1', 'Password0')
|
||||
resp.form.set('password2', 'Password0')
|
||||
resp.form.set('first_name', 'John')
|
||||
resp.form.set('last_name', 'Doe')
|
||||
resp = resp.form.submit()
|
||||
assert resp.location == '/'
|
||||
|
|
Loading…
Reference in New Issue