provide generic input code logic (#69890)
gitea/authentic/pipeline/head This commit looks good
Details
gitea/authentic/pipeline/head This commit looks good
Details
Suited to both registration & password-change actions.
This commit is contained in:
parent
af8adcef71
commit
ba9550511e
|
@ -195,9 +195,9 @@ class RegistrationCompletionForm(RegistrationCompletionFormNoPassword):
|
|||
return self.cleaned_data
|
||||
|
||||
|
||||
class InputRegistrationCodeForm(Form):
|
||||
registration_code = CharField(
|
||||
label=_('Registration code'),
|
||||
help_text=_('The registration code you received by SMS.'),
|
||||
class InputSMSCodeForm(Form):
|
||||
sms_code = CharField(
|
||||
label=_('SMS code'),
|
||||
help_text=_('The code you received by SMS.'),
|
||||
max_length=settings.SMS_CODE_LENGTH,
|
||||
)
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
{% block content %}
|
||||
<form method="post" action=".">
|
||||
<p>{% blocktrans trimmed %}Input your account activation code.{% endblocktrans %}</p>
|
||||
<p>{% blocktrans trimmed %}Input the code you received by SMS.{% endblocktrans %}</p>
|
||||
<p>
|
||||
{% blocktrans count counter=duration %}
|
||||
Your code is valid for the next minute.
|
|
@ -116,8 +116,8 @@ urlpatterns = [
|
|||
),
|
||||
re_path(
|
||||
'^register/input_code/(?P<token>[A-Za-z0-9_ -]+)/$',
|
||||
views.input_registration_code,
|
||||
name='input_registration_code',
|
||||
views.input_sms_code,
|
||||
name='input_sms_code',
|
||||
),
|
||||
# Password reset
|
||||
re_path(
|
||||
|
|
|
@ -854,7 +854,7 @@ class PasswordResetView(FormView):
|
|||
if not app_settings.A2_ACCEPT_PHONE_AUTHENTICATION or not self.code: # user input is email
|
||||
return reverse('password_reset_instructions')
|
||||
else: # user input is phone number
|
||||
return reverse('input_registration_code', kwargs={'token': self.code.url_token})
|
||||
return reverse('input_sms_code', kwargs={'token': self.code.url_token})
|
||||
|
||||
def get_template_names(self):
|
||||
return [
|
||||
|
@ -1208,7 +1208,7 @@ class BaseRegistrationView(HomeURLMixin, FormView):
|
|||
messages.warning(
|
||||
self.request,
|
||||
_(
|
||||
'Something went wrong while trying to send the SMS registration code to you.'
|
||||
'Something went wrong while trying to send the SMS code to you.'
|
||||
' Please contact your administrator and try again later.'
|
||||
),
|
||||
)
|
||||
|
@ -1217,7 +1217,7 @@ class BaseRegistrationView(HomeURLMixin, FormView):
|
|||
self.request.session['registered_phone'] = phone
|
||||
return utils_misc.redirect(
|
||||
self.request,
|
||||
reverse('input_registration_code', kwargs={'token': code.url_token}),
|
||||
reverse('input_sms_code', kwargs={'token': code.url_token}),
|
||||
params={REDIRECT_FIELD_NAME: self.next_url, 'token': code.url_token},
|
||||
)
|
||||
|
||||
|
@ -1296,20 +1296,18 @@ class BaseRegistrationView(HomeURLMixin, FormView):
|
|||
return context
|
||||
|
||||
|
||||
class InputRegistrationCodeView(cbv.ValidateCSRFMixin, FormView):
|
||||
template_name = 'registration/sms_input_registration_code.html'
|
||||
form_class = registration_forms.InputRegistrationCodeForm
|
||||
class InputSMSCodeView(cbv.ValidateCSRFMixin, FormView):
|
||||
template_name = 'registration/sms_input_code.html'
|
||||
form_class = registration_forms.InputSMSCodeForm
|
||||
success_url = '/accounts/'
|
||||
title = _('Account activation')
|
||||
title = _('SMS code validation')
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
token = kwargs.get('token')
|
||||
try:
|
||||
self.code = models.SMSCode.objects.get(url_token=token)
|
||||
except models.SMSCode.DoesNotExist:
|
||||
return HttpResponseBadRequest(_('Invalid account activation request'))
|
||||
if not self.code.sent:
|
||||
return HttpResponseBadRequest(_('Invalid account activation code'))
|
||||
return HttpResponseBadRequest(_('Invalid request'))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -1326,35 +1324,43 @@ class InputRegistrationCodeView(cbv.ValidateCSRFMixin, FormView):
|
|||
@atomic(savepoint=False)
|
||||
def form_valid(self, form):
|
||||
super().form_valid(form)
|
||||
registration_code = form.cleaned_data.pop('registration_code')
|
||||
if self.code.value != registration_code:
|
||||
sms_code = form.cleaned_data.pop('sms_code')
|
||||
if self.code.value != sms_code or self.code.fake:
|
||||
# TODO ratelimit on erroneous code inputs(?)
|
||||
# (code expires after 120 seconds)
|
||||
form.add_error('registration_code', _('Wrong registration code.'))
|
||||
form.add_error('sms_code', _('Wrong SMS code.'))
|
||||
return self.form_invalid(form)
|
||||
if self.code.expires < timezone.now():
|
||||
form.add_error('registration_code', _('The code has expired.'))
|
||||
form.add_error('sms_code', _('The code has expired.'))
|
||||
return self.form_invalid(form)
|
||||
Lock.lock_identifier(self.code.phone)
|
||||
content = {
|
||||
# TODO missing ou registration management
|
||||
'authentication_method': 'phone',
|
||||
'phone': self.code.phone,
|
||||
'user': self.code.user.pk if self.code.user else None,
|
||||
}
|
||||
# create token to process final account activation and user-defined attributes
|
||||
token = models.Token.create(
|
||||
kind='registration',
|
||||
kind=self.code.CODE_TO_TOKEN_KINDS[self.code.kind],
|
||||
content=content,
|
||||
duration=120,
|
||||
)
|
||||
return utils_misc.redirect(
|
||||
# TODO next_url management throughout account creation process
|
||||
self.request,
|
||||
reverse('registration_activate', kwargs={'registration_token': token.uuid}),
|
||||
)
|
||||
|
||||
# TODO next_url management throughout account creation process
|
||||
if self.code.kind == models.SMSCode.KIND_REGISTRATION:
|
||||
return utils_misc.redirect(
|
||||
self.request,
|
||||
reverse('registration_activate', kwargs={'registration_token': token.uuid}),
|
||||
)
|
||||
elif self.code.kind == models.SMSCode.KIND_PASSWORD_LOST:
|
||||
return utils_misc.redirect(
|
||||
self.request,
|
||||
reverse('password_reset_confirm', kwargs={'token': token.uuid}),
|
||||
)
|
||||
|
||||
|
||||
input_registration_code = InputRegistrationCodeView.as_view()
|
||||
input_sms_code = InputSMSCodeView.as_view()
|
||||
|
||||
|
||||
class RegistrationView(cbv.ValidateCSRFMixin, BaseRegistrationView):
|
||||
|
|
|
@ -13,15 +13,33 @@
|
|||
#
|
||||
# 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/>.
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from django.contrib.auth import authenticate
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from httmock import HTTMock, remember_called, urlmatch
|
||||
|
||||
from authentic2.models import SMSCode, Token
|
||||
from authentic2.utils.misc import send_password_reset_mail
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
@urlmatch(netloc='foo.whatever.none')
|
||||
@remember_called
|
||||
def sms_service_mock(url, request):
|
||||
return {
|
||||
'content': {},
|
||||
'headers': {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
'status_code': 200,
|
||||
}
|
||||
|
||||
|
||||
def test_send_password_reset_email(app, simple_user, mailoutbox):
|
||||
assert len(mailoutbox) == 0
|
||||
with utils.run_on_commit_hooks():
|
||||
|
@ -41,6 +59,112 @@ def test_send_password_reset_email(app, simple_user, mailoutbox):
|
|||
utils.assert_event('user.password.reset', user=simple_user, session=app.session)
|
||||
|
||||
|
||||
def test_send_password_reset_by_sms_code_improperly_configured(app, nomail_user, settings):
|
||||
settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
|
||||
settings.SMS_URL = 'https://foo.whatever.none/'
|
||||
|
||||
assert not SMSCode.objects.count()
|
||||
assert not Token.objects.count()
|
||||
|
||||
url = reverse('password_reset')
|
||||
resp = app.get(url, status=200)
|
||||
resp.form.set('phone_1', '0123456789')
|
||||
resp = resp.form.submit().follow().maybe_follow()
|
||||
assert 'Something went wrong while trying to send' in resp.pyquery('li.error').text()
|
||||
|
||||
|
||||
def test_send_password_reset_by_sms_code(app, nomail_user, settings):
|
||||
settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
|
||||
settings.SMS_URL = 'https://foo.whatever.none/'
|
||||
|
||||
code_length = settings.SMS_CODE_LENGTH
|
||||
assert not SMSCode.objects.count()
|
||||
assert not Token.objects.count()
|
||||
|
||||
url = reverse('password_reset')
|
||||
resp = app.get(url, status=200)
|
||||
resp.form.set('phone_1', '0123456789')
|
||||
with HTTMock(sms_service_mock):
|
||||
resp = resp.form.submit().follow().maybe_follow()
|
||||
body = json.loads(sms_service_mock.call['requests'][0].body)
|
||||
assert body['message'].startswith('Your code is')
|
||||
code = SMSCode.objects.get()
|
||||
assert body['message'][-code_length:] == code.value
|
||||
assert ("Your code is valid for the next %s minute" % (SMSCode.CODE_DURATION // 60)) in resp.text
|
||||
assert "The code you received by SMS." in resp.text
|
||||
resp.form.set('sms_code', code.value)
|
||||
resp = resp.form.submit().follow()
|
||||
assert Token.objects.count() == 1
|
||||
|
||||
assert authenticate(username='user', password='1234==aA') is None
|
||||
resp.form.set('new_password1', '1234==aA')
|
||||
resp.form.set('new_password2', '1234==aA')
|
||||
resp.form.submit()
|
||||
# verify user is logged
|
||||
assert str(app.session['_auth_user_id']) == str(nomail_user.pk)
|
||||
user = authenticate(username='user', password='1234==aA')
|
||||
assert user == nomail_user
|
||||
|
||||
with override_settings(A2_USER_CAN_RESET_PASSWORD=False):
|
||||
url = reverse('password_reset')
|
||||
app.get(url, status=404)
|
||||
|
||||
|
||||
def test_password_reset_empty_form(app, db, settings):
|
||||
settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
|
||||
settings.SMS_URL = 'https://foo.whatever.none/'
|
||||
|
||||
url = reverse('password_reset')
|
||||
resp = app.get(url, status=200)
|
||||
resp = resp.form.submit()
|
||||
assert 'There were errors processing your form.' in resp.pyquery('div.errornotice').text()
|
||||
assert (
|
||||
'Please provide an email address or a mobile phone number.' in resp.pyquery('div.errornotice').text()
|
||||
)
|
||||
|
||||
|
||||
def test_password_reset_both_fields_filled_email_precedence(app, simple_user, settings, mailoutbox):
|
||||
settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
|
||||
settings.SMS_URL = 'https://foo.whatever.none/'
|
||||
|
||||
url = reverse('password_reset')
|
||||
resp = app.get(url, status=200)
|
||||
resp.form.set('email', simple_user.email)
|
||||
resp.form.set('phone_1', '0123456789')
|
||||
resp = resp.form.submit()
|
||||
utils.assert_event('user.password.reset.request', user=simple_user, email=simple_user.email)
|
||||
assert resp['Location'].endswith('/instructions/')
|
||||
resp = resp.follow()
|
||||
assert len(mailoutbox) == 1
|
||||
assert not SMSCode.objects.count()
|
||||
|
||||
|
||||
def test_send_password_reset_by_sms_code_erroneous_phone_number(app, nomail_user, settings):
|
||||
settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
|
||||
settings.SMS_URL = 'https://foo.whatever.none/'
|
||||
|
||||
assert not SMSCode.objects.count()
|
||||
assert not Token.objects.count()
|
||||
|
||||
url = reverse('password_reset')
|
||||
resp = app.get(url, status=200)
|
||||
resp.form.set('phone_1', '0111111111')
|
||||
resp = resp.form.submit().follow().maybe_follow()
|
||||
assert 'Something went wrong while trying to send' not in resp.text
|
||||
assert 'error' not in resp.text
|
||||
assert resp.pyquery('title').text() == 'Authentic2 - testserver - SMS code validation'
|
||||
code = SMSCode.objects.get()
|
||||
assert code.fake
|
||||
resp.form.set('sms_code', 'whatever')
|
||||
resp = resp.form.submit()
|
||||
assert resp.pyquery('ul.errorlist').text() == 'Wrong SMS code.'
|
||||
# even if the correct value is guessed, the code is still fake & not valid whatsoever
|
||||
resp.form.set('sms_code', code.value)
|
||||
resp = resp.form.submit()
|
||||
assert resp.pyquery('ul.errorlist').text() == 'Wrong SMS code.'
|
||||
assert not Token.objects.count()
|
||||
|
||||
|
||||
def test_reset_by_email(app, simple_user, mailoutbox, settings):
|
||||
url = reverse('password_reset')
|
||||
resp = app.get(url, status=200)
|
||||
|
|
|
@ -978,10 +978,10 @@ def test_phone_registration_wrong_code(app, db, settings):
|
|||
resp.form.set('phone_1', '612345678')
|
||||
with HTTMock(sms_service_mock):
|
||||
resp = resp.form.submit().follow()
|
||||
resp.form.set('registration_code', 'abc')
|
||||
resp.form.set('sms_code', 'abc')
|
||||
resp = resp.form.submit()
|
||||
assert not Token.objects.count()
|
||||
assert resp.pyquery('li')[0].text_content() == 'Wrong registration code.'
|
||||
assert resp.pyquery('li')[0].text_content() == 'Wrong SMS code.'
|
||||
|
||||
|
||||
def test_phone_registration_expired_code(app, db, settings, freezer):
|
||||
|
@ -993,7 +993,7 @@ def test_phone_registration_expired_code(app, db, settings, freezer):
|
|||
with HTTMock(sms_service_mock):
|
||||
resp = resp.form.submit().follow()
|
||||
code = SMSCode.objects.get()
|
||||
resp.form.set('registration_code', code.value)
|
||||
resp.form.set('sms_code', code.value)
|
||||
freezer.move_to(timedelta(hours=1))
|
||||
resp = resp.form.submit()
|
||||
assert not Token.objects.count()
|
||||
|
@ -1009,7 +1009,7 @@ def test_phone_registration_cancel(app, db, settings, freezer):
|
|||
with HTTMock(sms_service_mock):
|
||||
resp = resp.form.submit().follow()
|
||||
code = SMSCode.objects.get()
|
||||
resp.form.set('registration_code', code.value)
|
||||
resp.form.set('sms_code', code.value)
|
||||
resp.form.submit('cancel').follow()
|
||||
assert not Token.objects.count()
|
||||
assert not SMSCode.objects.count()
|
||||
|
@ -1026,7 +1026,7 @@ def test_phone_registration_improperly_configured(app, db, settings, freezer, ca
|
|||
assert not Token.objects.count()
|
||||
assert not SMSCode.objects.count()
|
||||
assert (
|
||||
"Something went wrong while trying to send the SMS registration code to you"
|
||||
"Something went wrong while trying to send the SMS code to you"
|
||||
in resp.pyquery('li.warning')[0].text_content()
|
||||
)
|
||||
assert caplog.records[0].message == 'settings.SMS_URL is not set'
|
||||
|
@ -1047,7 +1047,7 @@ def test_phone_registration_connection_error(app, db, settings, freezer, caplog)
|
|||
mock_send.return_value = mock_response
|
||||
resp = resp.form.submit().follow().maybe_follow()
|
||||
assert (
|
||||
"Something went wrong while trying to send the SMS registration code to you"
|
||||
"Something went wrong while trying to send the SMS code to you"
|
||||
in resp.pyquery('li.warning')[0].text_content()
|
||||
)
|
||||
assert (
|
||||
|
@ -1072,8 +1072,8 @@ def test_phone_registration(app, db, settings):
|
|||
code = SMSCode.objects.get()
|
||||
assert body['message'][-code_length:] == code.value
|
||||
assert ("Your code is valid for the next %s minute" % (SMSCode.CODE_DURATION // 60)) in resp.text
|
||||
assert "The registration code you received by SMS." in resp.text
|
||||
resp.form.set('registration_code', code.value)
|
||||
assert "The code you received by SMS." in resp.text
|
||||
resp.form.set('sms_code', code.value)
|
||||
resp = resp.form.submit().follow()
|
||||
assert Token.objects.count() == 1
|
||||
|
||||
|
|
Loading…
Reference in New Issue