Projet

Général

Profil

0001-misc-add-password-strength-meter-63831.patch

Corentin Séchet, 29 juin 2022 12:39

Télécharger (11,7 ko)

Voir les différences:

Subject: [PATCH] misc: add password strength meter (#63831)

 src/authentic2/api_views.py                   | 19 ++++--
 src/authentic2/passwords.py                   | 50 ++++++++++++---
 .../static/authentic2/css/password.scss       | 62 +++++++++++++++++++
 .../static/authentic2/js/password.js          | 44 ++++++++++---
 tests/api/test_all.py                         | 22 ++++---
 5 files changed, 165 insertions(+), 32 deletions(-)
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
-