0001-misc-add-password-strength-meter-63831.patch
src/authentic2/api_views.py | ||
---|---|---|
1401 | 1401 | |
1402 | 1402 |
def rpc(self, request, serializer): |
1403 | 1403 |
password_checker = get_password_checker() |
1404 |
check_report = password_checker(serializer.validated_data['password']) |
|
1405 |
if check_report.strength is not None: |
|
1406 |
return { |
|
1407 |
'result': 1, |
|
1408 |
'ok': check_report.ok, |
|
1409 |
'strength': check_report.strength, |
|
1410 |
'hints': check_report.hints, |
|
1411 |
'strength_label': check_report.strength_label, |
|
1412 |
}, status.HTTP_200_OK |
|
1413 | ||
1404 | 1414 |
checks = [] |
1405 |
result = {'result': 1, 'checks': checks} |
|
1406 |
ok = True |
|
1407 |
for check in password_checker(serializer.validated_data['password']): |
|
1408 |
ok = ok and check.result |
|
1415 |
result = {'result': 1, 'checks': checks, 'ok': check_report.ok} |
|
1416 | ||
1417 |
for check in check_report.checks: |
|
1409 | 1418 |
checks.append( |
1410 | 1419 |
{ |
1411 | 1420 |
'result': check.result, |
1412 | 1421 |
'label': check.label, |
1413 | 1422 |
} |
1414 | 1423 |
) |
1415 |
result['ok'] = ok |
|
1424 | ||
1416 | 1425 |
return result, status.HTTP_200_OK |
1417 | 1426 | |
1418 | 1427 |
src/authentic2/passwords.py | ||
---|---|---|
56 | 56 |
self.label = label |
57 | 57 |
self.result = result |
58 | 58 | |
59 |
class CheckReport: |
|
60 |
def __init__(self, ok, checks=None, strength=None, hints=None): |
|
61 |
self.ok = ok |
|
62 |
self.checks = checks |
|
63 |
self.strength = strength |
|
64 |
self.hints = hints if hints is not None else [] |
|
65 |
if strength is not None: |
|
66 |
self.strength_label = [_('Weak'), _('Fair'), _('Good'), _('Strong')][strength] |
|
67 | ||
59 | 68 |
def __init__(self, *args, **kwargs): |
60 | 69 |
pass |
61 | 70 | |
... | ... | |
96 | 105 |
return app_settings.A2_PASSWORD_POLICY_MIN_STRENGTH |
97 | 106 | |
98 | 107 |
def __call__(self, password, **kwargs): |
99 | ||
108 |
if self.min_strength: |
|
109 |
if password: |
|
110 |
result = zxcvbn(password) |
|
111 |
strength = result['score'] |
|
112 |
return self.CheckReport( |
|
113 |
ok=strength >= self.min_strength, |
|
114 |
strength=strength, |
|
115 |
hints=result['feedback']['suggestions'], |
|
116 |
) |
|
117 | ||
118 |
return self.CheckReport(ok=False, strength=0) |
|
119 |
else: |
|
120 |
checks = list(self.checks(password, **kwargs)) |
|
121 |
ok = all([it.result for it in checks]) |
|
122 |
return self.CheckReport(ok=ok, checks=checks) |
|
123 | ||
124 |
def checks(self, password, **kwargs): |
|
100 | 125 |
if self.min_length: |
101 | 126 |
yield self.Check( |
102 | 127 |
result=len(password) >= self.min_length, label=_('%s characters') % self.min_length |
... | ... | |
114 | 139 |
if self.regexp and self.regexp_label: |
115 | 140 |
yield self.Check(result=bool(re.match(self.regexp, password)), label=self.regexp_label) |
116 | 141 | |
117 |
if self.min_strength: |
|
118 |
score = 0 |
|
119 |
if password: |
|
120 |
score = zxcvbn(password)['score'] |
|
121 | ||
122 |
yield self.Check(result=score > self.min_strength, label=_('Secure password')) |
|
123 | ||
124 | 142 | |
125 | 143 |
def get_password_checker(*args, **kwargs): |
126 | 144 |
return import_string(app_settings.A2_PASSWORD_POLICY_CLASS)(*args, **kwargs) |
... | ... | |
134 | 152 | |
135 | 153 |
def password_help_text(password='', only_errors=False): |
136 | 154 |
password_checker = get_password_checker() |
137 |
criteria = [check.label for check in password_checker(password) if not (only_errors and check.result)] |
|
155 |
check_report = password_checker(password) |
|
156 |
if check_report.strength is not None: |
|
157 |
hints = ['<div class="a2-password-hint">%s</div>' % hint for hint in check_report.hints] |
|
158 |
return ( |
|
159 |
'<div class="a2-password-strength-label">' |
|
160 |
' <div>%s</div>' |
|
161 |
' <div class="a2-password-strength-label-value">%s</div>' |
|
162 |
'</div>' |
|
163 |
'<div class="a2-password-strength-meter-container">' |
|
164 |
' <div class="a2-password-strength-meter a2-password-strength-0"></div>' |
|
165 |
'</div>' |
|
166 |
'<div class="a2-password-hint-container">%s</div>' |
|
167 |
) % (_('Password strength:'), check_report.strength_label, ''.join(hints)) |
|
168 | ||
169 |
criteria = [check.label for check in check_report.checks if not (only_errors and check.result)] |
|
138 | 170 |
if criteria: |
139 | 171 |
html_criteria = ['<span class="a2-password-policy-rule">%s</span>' % criter for criter in criteria] |
140 | 172 |
return _( |
src/authentic2/static/authentic2/css/password.scss | ||
---|---|---|
9 | 9 |
padding-left: 1.25rem; |
10 | 10 |
} |
11 | 11 | |
12 |
.a2-password-hint-container { |
|
13 |
display: block; |
|
14 |
margin-top: 0.6em; |
|
15 |
margin-left: 1.8em; |
|
16 |
} |
|
17 | ||
18 |
.a2-password-strength-label { |
|
19 |
&::after { |
|
20 |
font-family: FontAwesome; |
|
21 |
content: "\f00c"; /* ok icon */ |
|
22 |
color: green; |
|
23 |
visibility: hidden; |
|
24 |
margin: 1rem; |
|
25 |
} |
|
26 |
margin: 0.4rem; |
|
27 | ||
28 |
div { |
|
29 |
display: inline-block; |
|
30 |
} |
|
31 |
} |
|
32 | ||
33 |
.a2-password-ok.a2-password-strength-label { |
|
34 |
color: green; |
|
35 |
&::after { |
|
36 |
visibility: visible; |
|
37 |
} |
|
38 |
} |
|
39 | ||
40 | ||
41 |
.a2-password-strength-meter-container { |
|
42 |
margin: 0.2rem 1rem; |
|
43 |
border: 1px solid; |
|
44 |
} |
|
45 | ||
46 |
.a2-password-strength-meter { |
|
47 |
height: 0.5rem; |
|
48 | ||
49 |
&.a2-password-strength-0 { |
|
50 |
background: darkred; |
|
51 |
width: 25%; |
|
52 |
} |
|
53 | ||
54 |
&.a2-password-strength-1 { |
|
55 |
background: yellow; |
|
56 |
width: 50%; |
|
57 |
} |
|
58 | ||
59 |
&.a2-password-strength-2 { |
|
60 |
background: green; |
|
61 |
width: 75%; |
|
62 |
} |
|
63 | ||
64 |
&.a2-password-strength-3 { |
|
65 |
background: blue; |
|
66 |
width: 100%; |
|
67 |
} |
|
68 |
} |
|
69 | ||
70 |
.a2-password-hint-container { |
|
71 |
margin: 0.2rem 1rem; |
|
72 |
} |
|
73 | ||
12 | 74 |
.a2-password-policy-container { |
13 | 75 |
display: block; |
14 | 76 |
margin-top: 0.6em; |
src/authentic2/static/authentic2/js/password.js | ||
---|---|---|
37 | 37 |
function get_validation($input) { |
38 | 38 |
var password = $input.val(); |
39 | 39 |
var $help_text = $input.parent().find('.hint'); |
40 |
var $policyContainer = $help_text.find('.a2-password-policy-container'); |
|
41 | 40 |
$.ajax({ |
42 | 41 |
method: 'POST', |
43 | 42 |
url: '/api/validate-password/', |
... | ... | |
49 | 48 |
return; |
50 | 49 |
} |
51 | 50 | |
52 |
$policyContainer.empty(); |
|
53 |
$policyContainer.removeClass('a2-password-ok a2-password-nok'); |
|
54 |
for (var i = 0; i < data.checks.length; i++) { |
|
55 |
var error = data.checks[i]; |
|
51 |
if ( data.strength !== undefined) { |
|
52 |
var $hintContainer = $help_text.find('.a2-password-hint-container'); |
|
53 |
var $strengthMeter = $help_text.find('.a2-password-strength-meter'); |
|
54 |
var $strengthLabel = $help_text.find('.a2-password-strength-label'); |
|
55 |
var $strengthLabelValue = $help_text.find('.a2-password-strength-label-value'); |
|
56 |
$strengthLabel.toggleClass('a2-password-ok', data.ok); |
|
57 |
$strengthLabel.toggleClass('a2-password-nok', ! data.ok); |
|
58 |
$strengthLabelValue.text(data.strength_label) |
|
56 | 59 | |
57 |
var $rule = $('<span class="a2-password-policy-rule"/>'); |
|
58 |
$rule.text(error.label) |
|
59 |
$rule.appendTo($policyContainer); |
|
60 |
$rule.toggleClass('a2-password-ok', error.result); |
|
61 |
$rule.toggleClass('a2-password-nok', ! error.result); |
|
60 |
$hintContainer.empty() |
|
61 |
for (var i = 0; i < 4; ++i) { |
|
62 |
$strengthMeter.removeClass('a2-password-strength-' + i); |
|
63 |
} |
|
64 | ||
65 |
$strengthMeter.addClass('a2-password-strength-' + data.strength); |
|
66 | ||
67 |
for (var i = 0; i < data.hints.length; i++) { |
|
68 |
var $hint = $('<div class="a2-password-policy-hint"/>'); |
|
69 |
$hint.text(data.hints[i]) |
|
70 |
$hint.appendTo($hintContainer); |
|
71 |
} |
|
72 |
} |
|
73 |
else { |
|
74 |
var $policyContainer = $help_text.find('.a2-password-policy-container'); |
|
75 |
$policyContainer.empty(); |
|
76 |
$policyContainer.removeClass('a2-password-ok a2-password-nok'); |
|
77 |
for (var i = 0; i < data.checks.length; i++) { |
|
78 |
var error = data.checks[i]; |
|
79 | ||
80 |
var $rule = $('<span class="a2-password-policy-rule"/>'); |
|
81 |
$rule.text(error.label) |
|
82 |
$rule.appendTo($policyContainer); |
|
83 |
$rule.toggleClass('a2-password-ok', error.result); |
|
84 |
$rule.toggleClass('a2-password-nok', ! error.result); |
|
85 |
} |
|
62 | 86 |
} |
63 | 87 |
} |
64 | 88 |
}); |
tests/api/test_all.py | ||
---|---|---|
1672 | 1672 | |
1673 | 1673 | |
1674 | 1674 |
def test_validate_password_strength(app, settings): |
1675 |
settings.A2_PASSWORD_POLICY_MIN_STRENGTH = 2
|
|
1676 |
response = app.post_json('/api/validate-password/', params={'password': 'w34k P455w0rd'})
|
|
1675 |
settings.A2_PASSWORD_POLICY_MIN_STRENGTH = 3
|
|
1676 |
response = app.post_json('/api/validate-password/', params={'password': 'P@ssword'})
|
|
1677 | 1677 |
assert response.json['result'] == 1 |
1678 | 1678 |
assert response.json['ok'] is False |
1679 |
assert len(response.json['checks']) == 5 |
|
1680 |
assert response.json['checks'][4]['label'] == 'Secure password' |
|
1681 |
assert response.json['checks'][4]['result'] is False |
|
1679 |
assert response.json['strength'] == 0 |
|
1680 |
assert response.json['strength_label'] == 'Weak' |
|
1681 |
assert len(response.json['hints']) == 3 |
|
1682 |
assert response.json['hints'][0] == 'Add another word or two. Uncommon words are better.' |
|
1683 |
assert response.json['hints'][1] == 'Capitalization doesn\'t help very much.' |
|
1684 |
assert ( |
|
1685 |
response.json['hints'][2] |
|
1686 |
== 'Predictable substitutions like \'@\' instead of \'a\' don\'t help very much.' |
|
1687 |
) |
|
1682 | 1688 | |
1683 | 1689 |
response = app.post_json('/api/validate-password/', params={'password': 'xbA2E4]#o'}) |
1684 | 1690 |
assert response.json['result'] == 1 |
1685 | 1691 |
assert response.json['ok'] is True |
1686 |
assert len(response.json['checks']) == 5
|
|
1687 |
assert response.json['checks'][4]['label'] == 'Secure password'
|
|
1688 |
assert response.json['checks'][4]['result'] is True
|
|
1692 |
assert response.json['strength'] == 3
|
|
1693 |
assert response.json['strength_label'] == 'Strong'
|
|
1694 |
assert len(response.json['hints']) == 0
|
|
1689 | 1695 | |
1690 | 1696 | |
1691 | 1697 |
def test_api_users_get_or_create(settings, app, admin): |
1692 |
- |