forms: include user attributes in password strength check (#79807)
This commit is contained in:
parent
ad918e4114
commit
5b852457c7
|
@ -1569,6 +1569,7 @@ validate_password = ValidatePasswordAPI.as_view()
|
|||
|
||||
class PasswordStrengthSerializer(serializers.Serializer):
|
||||
password = serializers.CharField(required=True, allow_blank=True)
|
||||
inputs = serializers.DictField(child=serializers.CharField(allow_blank=True), default={})
|
||||
|
||||
|
||||
class PasswordStrengthAPI(BaseRpcView):
|
||||
|
@ -1577,7 +1578,11 @@ class PasswordStrengthAPI(BaseRpcView):
|
|||
serializer_class = PasswordStrengthSerializer
|
||||
|
||||
def rpc(self, request, serializer):
|
||||
report = get_password_strength(serializer.validated_data['password'])
|
||||
report = get_password_strength(
|
||||
serializer.validated_data['password'],
|
||||
user=request.user,
|
||||
inputs=serializer.validated_data['inputs'],
|
||||
)
|
||||
result = {
|
||||
'result': 1,
|
||||
'strength': report.strength,
|
||||
|
|
|
@ -38,8 +38,6 @@ from authentic2.forms.widgets import (
|
|||
ProfileImageInput,
|
||||
)
|
||||
from authentic2.manager.utils import label_from_role
|
||||
from authentic2.passwords import get_password_checker, get_password_strength
|
||||
from authentic2.utils.misc import get_password_authenticator
|
||||
from authentic2.validators import email_validator
|
||||
|
||||
|
||||
|
@ -63,25 +61,6 @@ class NewPasswordField(CharField):
|
|||
|
||||
min_strength = property(_get_min_strength, _set_min_strength)
|
||||
|
||||
def validate(self, value):
|
||||
super().validate(value)
|
||||
if value == '':
|
||||
return
|
||||
|
||||
min_strength = self.min_strength
|
||||
if min_strength is not None:
|
||||
if get_password_strength(value).strength < min_strength:
|
||||
raise ValidationError(_('This password is not strong enough.'))
|
||||
|
||||
min_length = get_password_authenticator().password_min_length
|
||||
if min_length > len(value):
|
||||
raise ValidationError(_('Password must be at least %s characters.') % min_length)
|
||||
else:
|
||||
password_checker = get_password_checker()
|
||||
errors = [not check.result for check in password_checker(value)]
|
||||
if any(errors):
|
||||
raise ValidationError(_('This password is not accepted.'))
|
||||
|
||||
|
||||
class CheckPasswordField(CharField):
|
||||
widget = CheckPasswordInput
|
||||
|
|
|
@ -27,6 +27,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from authentic2.backends.ldap_backend import LDAPUser
|
||||
from authentic2.journal import journal
|
||||
from authentic2.passwords import validate_password
|
||||
from authentic2.utils.misc import get_password_authenticator
|
||||
|
||||
from .. import app_settings, models, validators
|
||||
|
@ -259,13 +260,16 @@ class SetPasswordForm(NotifyOfPasswordChange, PasswordResetMixin, auth_forms.Set
|
|||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
super().__init__(user, *args, **kwargs)
|
||||
self.fields['new_password1'].min_strength = get_password_authenticator().min_password_strength
|
||||
self.authenticator = get_password_authenticator()
|
||||
self.fields['new_password1'].min_strength = self.authenticator.min_password_strength
|
||||
|
||||
def clean_new_password1(self):
|
||||
new_password1 = self.cleaned_data.get('new_password1')
|
||||
if new_password1 and self.user.check_password(new_password1):
|
||||
raise ValidationError(_('New password must differ from old password'))
|
||||
|
||||
validate_password(new_password1, user=self.user, authenticator=self.authenticator)
|
||||
|
||||
return new_password1
|
||||
|
||||
|
||||
|
@ -280,7 +284,8 @@ class PasswordChangeForm(
|
|||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
super().__init__(user, *args, **kwargs)
|
||||
self.fields['new_password1'].min_strength = get_password_authenticator().min_password_strength
|
||||
self.authenticator = get_password_authenticator()
|
||||
self.fields['new_password1'].min_strength = self.authenticator.min_password_strength
|
||||
|
||||
def clean_new_password1(self):
|
||||
new_password1 = self.cleaned_data.get('new_password1')
|
||||
|
@ -288,6 +293,8 @@ class PasswordChangeForm(
|
|||
if new_password1 and new_password1 == old_password:
|
||||
raise ValidationError(_('New password must differ from old password'))
|
||||
|
||||
validate_password(new_password1, user=self.user, authenticator=self.authenticator)
|
||||
|
||||
return new_password1
|
||||
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from authentic2.a2_rbac.models import OrganizationalUnit
|
||||
from authentic2.forms.fields import CharField, CheckPasswordField, NewPasswordField
|
||||
from authentic2.passwords import validate_password
|
||||
|
||||
from .. import app_settings, models
|
||||
from ..utils import misc as utils_misc
|
||||
|
@ -172,7 +173,25 @@ class RegistrationCompletionForm(RegistrationCompletionFormNoPassword):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['password1'].min_strength = utils_misc.get_password_authenticator().min_password_strength
|
||||
self.authenticator = utils_misc.get_password_authenticator()
|
||||
self.fields['password1'].min_strength = self.authenticator.min_password_strength
|
||||
|
||||
self.strength_input_attributes = set()
|
||||
for attribute in models.Attribute.objects.all():
|
||||
kind = attribute.get_kind()
|
||||
if issubclass(kind.get('field_class'), CharField):
|
||||
self.strength_input_attributes.add(attribute.name)
|
||||
|
||||
for name, field in self.fields.items():
|
||||
if name in self.strength_input_attributes:
|
||||
field.widget.attrs['data-password-strength-input'] = True
|
||||
|
||||
def clean_password1(self):
|
||||
password = self.cleaned_data['password1']
|
||||
|
||||
inputs = {k: v for k, v in self.cleaned_data.items() if k in self.strength_input_attributes}
|
||||
validate_password(password, user=self.instance, inputs=inputs, authenticator=self.authenticator)
|
||||
return password
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
|
|
|
@ -44,7 +44,7 @@ from authentic2.forms.fields import (
|
|||
from authentic2.forms.mixins import SlugMixin
|
||||
from authentic2.forms.profile import BaseUserForm
|
||||
from authentic2.models import APIClient, PasswordReset, Service, Setting
|
||||
from authentic2.passwords import generate_password
|
||||
from authentic2.passwords import generate_password, validate_password
|
||||
from authentic2.utils.misc import (
|
||||
RUNTIME_SETTINGS,
|
||||
get_password_authenticator,
|
||||
|
@ -192,6 +192,11 @@ class UserChangePasswordForm(CssClass, forms.ModelForm):
|
|||
notification_template_prefix = 'authentic2/manager/change-password-notification'
|
||||
require_password = True
|
||||
|
||||
def clean_password1(self):
|
||||
password = self.cleaned_data.get("password1")
|
||||
validate_password(password, user=self.instance)
|
||||
return password
|
||||
|
||||
def clean_password2(self):
|
||||
password1 = self.cleaned_data.get('password1')
|
||||
password2 = self.cleaned_data.get('password2')
|
||||
|
|
|
@ -19,6 +19,7 @@ import random
|
|||
import re
|
||||
import string
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.module_loading import import_string
|
||||
from django.utils.translation import gettext as _
|
||||
from zxcvbn import zxcvbn
|
||||
|
@ -119,15 +120,33 @@ class StrengthReport:
|
|||
self.hint = hint
|
||||
|
||||
|
||||
def get_password_strength(password):
|
||||
min_length = get_password_authenticator().password_min_length
|
||||
def get_password_strength(password, user=None, inputs=None, authenticator=None):
|
||||
authenticator = authenticator or get_password_authenticator()
|
||||
|
||||
user_inputs = {}
|
||||
if user is not None and hasattr(user, 'attributes'):
|
||||
user_inputs = {name: attr.content for name, attr in user.attributes.values.items() if attr.content}
|
||||
user_inputs['email'] = user.email
|
||||
|
||||
if inputs is not None:
|
||||
user_inputs.update(inputs)
|
||||
|
||||
splitted_inputs = set()
|
||||
for input in user_inputs.values():
|
||||
# check against full inputs and each word of it
|
||||
if not input:
|
||||
continue
|
||||
splitted_inputs.add(input)
|
||||
splitted_inputs.update(input.split(' '))
|
||||
|
||||
min_length = authenticator.password_min_length
|
||||
|
||||
hint = _('add more words or characters.')
|
||||
strength = 0
|
||||
if min_length and len(password) < min_length:
|
||||
hint = _('use at least %s characters.') % min_length
|
||||
elif password:
|
||||
report = zxcvbn(password)
|
||||
report = zxcvbn(password, user_inputs=splitted_inputs)
|
||||
strength = report['score']
|
||||
hint = get_hint(report['sequence'])
|
||||
|
||||
|
@ -166,7 +185,9 @@ def get_hint_for_match(match):
|
|||
hint = _('avoid dates and years that are associated with you.')
|
||||
|
||||
if pattern == 'dictionary':
|
||||
if match['l33t'] or match['reversed']:
|
||||
if match['dictionary_name'] == 'user_inputs':
|
||||
hint = _('avoid "{token}" : it\'s similar to one of your personal informations.')
|
||||
elif match['l33t'] or match['reversed']:
|
||||
hint = _('avoid "{token}" : it\'s similar to a commonly used password')
|
||||
else:
|
||||
hint = _('avoid "{token}" : it\'s a commonly used password.')
|
||||
|
@ -175,3 +196,26 @@ def get_hint_for_match(match):
|
|||
return hint.format(token=match['token'])
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def validate_password(password, user=None, inputs=None, authenticator=None):
|
||||
if password == '':
|
||||
return
|
||||
|
||||
authenticator = authenticator or get_password_authenticator()
|
||||
|
||||
min_strength = authenticator.min_password_strength
|
||||
min_length = authenticator.password_min_length
|
||||
|
||||
if min_strength is not None:
|
||||
if get_password_strength(password, user=user, inputs=inputs).strength < min_strength:
|
||||
raise ValidationError(_('This password is not strong enough.'))
|
||||
|
||||
if min_length > len(password):
|
||||
raise ValidationError(_('Password must be at least %s characters.') % min_length)
|
||||
else:
|
||||
# legacy password policy
|
||||
password_checker = get_password_checker()
|
||||
errors = [not check.result for check in password_checker(password)]
|
||||
if any(errors):
|
||||
raise ValidationError(_('This password is not accepted.'))
|
||||
|
|
|
@ -26,15 +26,20 @@ a2_password_check_equality = (function () {
|
|||
})();
|
||||
|
||||
function update_password_strength($input, password, min_strength) {
|
||||
var $parent_form = $input.parents('form')
|
||||
var $feedback = $input.parent().find('.a2-password-feedback')
|
||||
var $hint = $feedback.find('.a2-password-hint');
|
||||
var $hint_content = $feedback.find('.a2-password-hint--content');
|
||||
var $strength = $feedback.find('.a2-password-strength');
|
||||
var $strength_name = $feedback.find('.a2-password-strength--name');
|
||||
|
||||
const $inputs = $parent_form.find('input[data-password-strength-input]')
|
||||
const input_values = Object.fromEntries(Array.from($inputs).map(elt => [elt.name, elt.value]))
|
||||
|
||||
$.ajax({
|
||||
method: 'POST',
|
||||
url: '/api/password-strength/',
|
||||
data: JSON.stringify({'password': password}),
|
||||
data: JSON.stringify({'password': password, 'inputs': input_values}),
|
||||
dataType: 'json',
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
success: function(data) {
|
||||
|
@ -101,15 +106,20 @@ a2_password_validate = (function () {
|
|||
}
|
||||
});
|
||||
}
|
||||
function validate_password(event) {
|
||||
var $input = $(event.target);
|
||||
setTimeout(function () {
|
||||
get_validation($input);
|
||||
}, 0);
|
||||
}
|
||||
return function (id) {
|
||||
var $input = $('#' + id);
|
||||
|
||||
function validate_password() {
|
||||
setTimeout(function () {
|
||||
get_validation($input);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
$input.on('input.a2-password-validate', validate_password);
|
||||
|
||||
const $parent_form = $input.parents('form')
|
||||
const $user_inputs = $parent_form.find('input[data-password-strength-input]')
|
||||
$user_inputs.on('input.a2-password-validate', validate_password)
|
||||
}
|
||||
})();
|
||||
|
||||
|
|
|
@ -1808,45 +1808,53 @@ def test_validate_password_regex(app, settings):
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'min_length, password,strength,label',
|
||||
'min_length, password,strength,label,inputs',
|
||||
[
|
||||
(0, '', 0, 'Very Weak'),
|
||||
(0, '?', 0, 'Very Weak'),
|
||||
(0, '?JR!', 1, 'Weak'),
|
||||
(0, '?JR!p4A', 2, 'Fair'),
|
||||
(0, '?JR!p4A2i', 3, 'Good'),
|
||||
(0, '?JR!p4A2i:#', 4, 'Strong'),
|
||||
(12, '?JR!p4A2i:#', 0, 'Very Weak'),
|
||||
(0, '', 0, 'Very Weak', {}),
|
||||
(0, '?', 0, 'Very Weak', {}),
|
||||
(0, '?JR!', 1, 'Weak', {}),
|
||||
(0, '?JR!p4A', 2, 'Fair', {}),
|
||||
(0, '?JR!p4A2i', 3, 'Good', {}),
|
||||
(0, '?JR!p4A2i:#', 4, 'Strong', {}),
|
||||
(0, 'Kaczynski', 0, 'Very Weak', {'first_name': 'Kaczynski'}),
|
||||
(0, 'Faas-Hardegger', 4, 'Strong', {'first_name': 'Kaczynski'}),
|
||||
(12, '?JR!p4A2i:#', 0, 'Very Weak', {}),
|
||||
],
|
||||
)
|
||||
def test_password_strength(app, settings, min_length, password, strength, label):
|
||||
def test_password_strength(app, settings, min_length, password, strength, label, inputs):
|
||||
LoginPasswordAuthenticator.objects.update(password_min_length=min_length)
|
||||
response = app.post_json('/api/password-strength/', params={'password': password})
|
||||
response = app.post_json('/api/password-strength/', params={'password': password, 'inputs': inputs})
|
||||
assert response.json['result'] == 1
|
||||
assert response.json['strength'] == strength
|
||||
assert response.json['strength_label'] == label
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'min_length, password, hint',
|
||||
'min_length, password, hint,inputs',
|
||||
[
|
||||
(0, '', 'add more words or characters.'),
|
||||
(0, 'sdfgh', 'avoid straight rows of keys like "sdfgh".'),
|
||||
(0, 'ertgfd', 'avoid short keyboard patterns like "ertgfd".'),
|
||||
(0, 'abab', 'avoid repeated words and characters like "abab".'),
|
||||
(0, 'abcd', 'avoid sequences like "abcd".'),
|
||||
(0, '2019', 'avoid recent years.'),
|
||||
(0, '02/08/14', 'avoid dates and years that are associated with you.'),
|
||||
(0, '02/08/14', 'avoid dates and years that are associated with you.'),
|
||||
(0, 'p@ssword', 'avoid "p@ssword" : it\'s similar to a commonly used password'),
|
||||
(0, 'password', 'avoid "password" : it\'s a commonly used password.'),
|
||||
(42, 'password', 'use at least 42 characters.'),
|
||||
(0, '', 'add more words or characters.', {}),
|
||||
(0, 'sdfgh', 'avoid straight rows of keys like "sdfgh".', {}),
|
||||
(0, 'ertgfd', 'avoid short keyboard patterns like "ertgfd".', {}),
|
||||
(0, 'abab', 'avoid repeated words and characters like "abab".', {}),
|
||||
(0, 'abcd', 'avoid sequences like "abcd".', {}),
|
||||
(0, '2019', 'avoid recent years.', {}),
|
||||
(0, '02/08/14', 'avoid dates and years that are associated with you.', {}),
|
||||
(0, '02/08/14', 'avoid dates and years that are associated with you.', {}),
|
||||
(0, 'p@ssword', 'avoid "p@ssword" : it\'s similar to a commonly used password', {}),
|
||||
(0, 'password', 'avoid "password" : it\'s a commonly used password.', {}),
|
||||
(
|
||||
0,
|
||||
'Kaczynski',
|
||||
'avoid "Kaczynski" : it\'s similar to one of your personal informations.',
|
||||
{'first_name': 'Kaczynski'},
|
||||
),
|
||||
(42, 'password', 'use at least 42 characters.', {}),
|
||||
],
|
||||
)
|
||||
def test_password_strength_hints(app, settings, min_length, password, hint):
|
||||
def test_password_strength_hints(app, settings, min_length, password, hint, inputs):
|
||||
LoginPasswordAuthenticator.objects.update(password_min_length=min_length)
|
||||
settings.A2_PASSWORD_POLICY_MIN_STRENGTH = 3
|
||||
response = app.post_json('/api/password-strength/', params={'password': password})
|
||||
response = app.post_json('/api/password-strength/', params={'password': password, 'inputs': inputs})
|
||||
assert response.json['result'] == 1
|
||||
assert response.json['hint'] == hint
|
||||
|
||||
|
|
|
@ -18,9 +18,7 @@
|
|||
import pytest
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
|
||||
from authentic2.forms.fields import PhoneField
|
||||
from authentic2.forms.passwords import NewPasswordField
|
||||
|
||||
|
||||
def test_phonenumber_field(settings):
|
||||
|
@ -49,54 +47,3 @@ def test_phonenumber_field(settings):
|
|||
]:
|
||||
with pytest.raises(ValidationError):
|
||||
field.clean(value)
|
||||
|
||||
|
||||
def test_validate_password(db, settings):
|
||||
field = NewPasswordField()
|
||||
with pytest.raises(ValidationError):
|
||||
field.validate('aaaaaZZZZZZ')
|
||||
with pytest.raises(ValidationError):
|
||||
field.validate('00000aaaaaa')
|
||||
with pytest.raises(ValidationError):
|
||||
field.validate('00000ZZZZZZ')
|
||||
field.validate('000aaaaZZZZ')
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'password,min_strength',
|
||||
[
|
||||
('?', 0),
|
||||
('?JR!', 1),
|
||||
('?JR!p4A', 2),
|
||||
('?JR!p4A2i', 3),
|
||||
('?JR!p4A2i:#', 4),
|
||||
],
|
||||
)
|
||||
def test_validate_password_strength(db, settings, password, min_strength):
|
||||
LoginPasswordAuthenticator.objects.update(password_min_length=len(password))
|
||||
field = NewPasswordField()
|
||||
|
||||
field.min_strength = min_strength
|
||||
field.validate(password)
|
||||
|
||||
LoginPasswordAuthenticator.objects.update(password_min_length=len(password) + 1)
|
||||
with pytest.raises(ValidationError):
|
||||
field.validate(password)
|
||||
|
||||
if min_strength < 4:
|
||||
LoginPasswordAuthenticator.objects.update(password_min_length=len(password))
|
||||
field.min_strength = min_strength + 1
|
||||
with pytest.raises(ValidationError):
|
||||
field.validate(password)
|
||||
|
||||
|
||||
def test_digits_password_policy(db, settings):
|
||||
LoginPasswordAuthenticator.objects.update(
|
||||
password_regex='^[0-9]{8}$', password_regex_error_msg='pasbon', password_min_length=0
|
||||
)
|
||||
settings.A2_PASSWORD_POLICY_MIN_CLASSES = 0
|
||||
field = NewPasswordField()
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
field.validate('aaa')
|
||||
field.validate('12345678')
|
||||
|
|
|
@ -17,8 +17,13 @@
|
|||
|
||||
import string
|
||||
|
||||
import pytest
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from authentic2 import app_settings
|
||||
from authentic2.passwords import generate_password
|
||||
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
|
||||
from authentic2.models import Attribute
|
||||
from authentic2.passwords import generate_password, validate_password
|
||||
from authentic2.utils.misc import get_password_authenticator
|
||||
|
||||
|
||||
|
@ -32,3 +37,94 @@ def test_generate_password(db):
|
|||
assert sum(any(char in char_class for char in password) for char_class in char_classes) == max(
|
||||
app_settings.A2_PASSWORD_POLICY_MIN_CLASSES, 3
|
||||
)
|
||||
|
||||
|
||||
def test_validate_password_default_policy(db, settings):
|
||||
with pytest.raises(ValidationError):
|
||||
validate_password('aaaaaZZZZZZ')
|
||||
with pytest.raises(ValidationError):
|
||||
validate_password('00000aaaaaa')
|
||||
with pytest.raises(ValidationError):
|
||||
validate_password('00000ZZZZZZ')
|
||||
validate_password('000aaaaZZZZ')
|
||||
|
||||
|
||||
def test_digits_password_policy(db, settings):
|
||||
LoginPasswordAuthenticator.objects.update(
|
||||
password_regex='^[0-9]{8}$', password_regex_error_msg='pasbon', password_min_length=0
|
||||
)
|
||||
settings.A2_PASSWORD_POLICY_MIN_CLASSES = 0
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
validate_password('aaa')
|
||||
validate_password('12345678')
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'password,min_strength',
|
||||
[
|
||||
('?', 0),
|
||||
('?JR!', 1),
|
||||
('?JR!p4A', 2),
|
||||
('?JR!p4A2i', 3),
|
||||
('?JR!p4A2i:#', 4),
|
||||
],
|
||||
)
|
||||
def test_validate_password_strength(db, settings, password, min_strength):
|
||||
LoginPasswordAuthenticator.objects.update(
|
||||
password_min_length=len(password), min_password_strength=min_strength
|
||||
)
|
||||
validate_password(password)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
LoginPasswordAuthenticator.objects.update(password_min_length=len(password) + 1)
|
||||
validate_password(password)
|
||||
|
||||
if min_strength < 4:
|
||||
LoginPasswordAuthenticator.objects.update(
|
||||
password_min_length=len(password), min_password_strength=min_strength + 1
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
validate_password(password)
|
||||
|
||||
|
||||
def test_validate_password_strength_user_attributes(db, simple_user):
|
||||
LoginPasswordAuthenticator.objects.update(min_password_strength=3)
|
||||
simple_user.attributes.last_name = 'Kaczynski'
|
||||
|
||||
validate_password('Kaczynski')
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
validate_password('Kaczynski', inputs={'last_name': 'Kaczynski'})
|
||||
|
||||
# each word of input should be matched
|
||||
with pytest.raises(ValidationError):
|
||||
validate_password('Kaczynski', inputs={'last_name': 'Kaczynski Faas-Hardegger'})
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
validate_password('Kaczynski Faas-Hardegger', inputs={'last_name': 'Kaczynski Faas-Hardegger'})
|
||||
|
||||
simple_user.attributes.last_name = 'Kaczynski Faas-Hardegger'
|
||||
with pytest.raises(ValidationError):
|
||||
validate_password('Kaczynski', user=simple_user)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
validate_password('Kaczynski Faas-Hardegger', user=simple_user)
|
||||
|
||||
simple_user.attributes.last_name = 'Kaczynski'
|
||||
# inputs dict should override user attributes
|
||||
validate_password('Kaczynski', user=simple_user, inputs={'last_name': 'Faas-Hardegger'})
|
||||
|
||||
|
||||
def test_validate_password_strength_custom_attribute(db, simple_user):
|
||||
LoginPasswordAuthenticator.objects.update(min_password_strength=3)
|
||||
Attribute.objects.create(
|
||||
kind='string',
|
||||
name='favourite_song',
|
||||
)
|
||||
|
||||
validate_password('0opS 1 D1t iT @GAiN', user=simple_user)
|
||||
|
||||
simple_user.attributes.favourite_song = "0opS 1 D1t iT @GAiN"
|
||||
with pytest.raises(ValidationError):
|
||||
validate_password('0opS 1 D1t iT @GAiN', user=simple_user)
|
||||
|
|
|
@ -29,7 +29,7 @@ from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
|
|||
from authentic2.apps.journal.models import Event
|
||||
from authentic2.forms.profile import modelform_factory
|
||||
from authentic2.forms.registration import RegistrationCompletionForm
|
||||
from authentic2.models import SMSCode, Token
|
||||
from authentic2.models import Attribute, SMSCode, Token
|
||||
from authentic2.utils import misc as utils_misc
|
||||
from authentic2.validators import EmailValidator
|
||||
|
||||
|
@ -932,6 +932,20 @@ def test_registration_completion_form(db, simple_user):
|
|||
|
||||
|
||||
def test_registration_completion(db, app, mailoutbox):
|
||||
Attribute.objects.create(
|
||||
kind='string',
|
||||
label='Favourite Song',
|
||||
name='favourite_song',
|
||||
asked_on_registration=True,
|
||||
)
|
||||
|
||||
Attribute.objects.create(
|
||||
kind='boolean',
|
||||
label='Favourite Boolean',
|
||||
name='favourite_boolean',
|
||||
asked_on_registration=True,
|
||||
)
|
||||
|
||||
LoginPasswordAuthenticator.objects.update(min_password_strength=3)
|
||||
|
||||
resp = app.get(reverse('registration_register'))
|
||||
|
@ -944,10 +958,35 @@ def test_registration_completion(db, app, mailoutbox):
|
|||
resp.form.set('password2', 'Password0')
|
||||
resp.form.set('first_name', 'John')
|
||||
resp.form.set('last_name', 'Doe')
|
||||
resp.form.set('favourite_song', '0opS 1 D1t iT @GAiN')
|
||||
resp.form.set('favourite_boolean', True)
|
||||
|
||||
assert resp.pyquery('input[name=first_name][data-password-strength-input]')
|
||||
assert resp.pyquery('input[name=last_name][data-password-strength-input]')
|
||||
assert resp.pyquery('input[name=favourite_song][data-password-strength-input]')
|
||||
assert not resp.pyquery('input[name=password1][data-password-strength-input]')
|
||||
assert not resp.pyquery('input[name=password2][data-password-strength-input]')
|
||||
|
||||
resp = resp.form.submit()
|
||||
assert 'This password is not strong enough' in resp.text
|
||||
|
||||
resp.form.set('password1', 'testbot@entrouvert.com')
|
||||
resp.form.set('password2', 'testbot@entrouvert.com')
|
||||
resp = resp.form.submit()
|
||||
|
||||
assert 'This password is not strong enough' in resp.text
|
||||
|
||||
resp.form.set('password1', '0opS 1 D1t iT @GAiN')
|
||||
resp.form.set('password2', '0opS 1 D1t iT @GAiN')
|
||||
resp = resp.form.submit()
|
||||
|
||||
assert 'This password is not strong enough' in resp.text
|
||||
|
||||
resp.form.set('favourite_song', 'Baby one more time')
|
||||
resp = resp.form.submit()
|
||||
|
||||
assert 'This password is not strong enough' not in resp.text
|
||||
|
||||
|
||||
def test_registration_no_identifier(app, db, settings, phone_activated_authn):
|
||||
resp = app.get(reverse('registration_register'))
|
||||
|
|
|
@ -134,6 +134,13 @@ def test_password_change_error(
|
|||
|
||||
|
||||
def test_password_change_form(simple_user):
|
||||
Attribute.objects.create(
|
||||
kind='string',
|
||||
name='favourite_song',
|
||||
)
|
||||
|
||||
simple_user.attributes.favourite_song = "0opS 1 D1t iT @GAiN"
|
||||
|
||||
data = {
|
||||
'new_password1': 'Password0',
|
||||
'new_password2': 'Password0',
|
||||
|
@ -148,8 +155,26 @@ def test_password_change_form(simple_user):
|
|||
assert form.fields['new_password1'].widget.min_strength == 3
|
||||
assert form.errors['new_password1'] == ['This password is not strong enough.']
|
||||
|
||||
data = {
|
||||
'new_password1': '0opS 1 D1t iT @GAiN',
|
||||
'new_password2': '0opS 1 D1t iT @GAiN',
|
||||
}
|
||||
form = PasswordChangeForm(user=simple_user, data=data)
|
||||
assert form.errors['new_password1'] == ['This password is not strong enough.']
|
||||
|
||||
simple_user.attributes.favourite_song = "Baby one more time"
|
||||
form = PasswordChangeForm(user=simple_user, data=data)
|
||||
assert 'new_password1' not in form.errors
|
||||
|
||||
|
||||
def test_set_password_form(simple_user):
|
||||
Attribute.objects.create(
|
||||
kind='string',
|
||||
name='favourite_song',
|
||||
)
|
||||
|
||||
simple_user.attributes.favourite_song = "0opS 1 D1t iT @GAiN"
|
||||
|
||||
data = {
|
||||
'new_password1': 'Password0',
|
||||
'new_password2': 'Password0',
|
||||
|
@ -164,6 +189,17 @@ def test_set_password_form(simple_user):
|
|||
assert form.fields['new_password1'].widget.min_strength == 3
|
||||
assert form.errors['new_password1'] == ['This password is not strong enough.']
|
||||
|
||||
data = {
|
||||
'new_password1': '0opS 1 D1t iT @GAiN',
|
||||
'new_password2': '0opS 1 D1t iT @GAiN',
|
||||
}
|
||||
form = SetPasswordForm(user=simple_user, data=data)
|
||||
assert form.errors['new_password1'] == ['This password is not strong enough.']
|
||||
|
||||
simple_user.attributes.favourite_song = "Baby one more time"
|
||||
form = SetPasswordForm(user=simple_user, data=data)
|
||||
assert 'new_password1' not in form.errors
|
||||
|
||||
|
||||
def test_well_known_password_change(app):
|
||||
resp = app.get('/.well-known/change-password')
|
||||
|
|
Loading…
Reference in New Issue