0002-misc-generate-hints-from-zxcvbn-report-63831.patch
src/authentic2/passwords.py | ||
---|---|---|
122 | 122 |
elif password: |
123 | 123 |
report = zxcvbn(password) |
124 | 124 |
strength = report['score'] |
125 |
suggestions = report['feedback']['suggestions'] |
|
126 |
if len(suggestions): |
|
127 |
hint = report['feedback']['suggestions'][0] |
|
128 |
else: |
|
129 |
hint = _('add more words or characters') |
|
125 |
hint = get_hint(report['sequence']) |
|
130 | 126 | |
131 | 127 |
return self.StrengthReport(strength >= self.min_strength, strength, hint) |
132 | 128 | |
... | ... | |
149 | 145 |
yield self.Check(result=bool(re.match(self.regexp, password)), label=self.regexp_label) |
150 | 146 | |
151 | 147 | |
148 |
def get_hint(matches): |
|
149 |
matches = sorted(matches, key=lambda m: len(m['token']), reverse=True) |
|
150 |
for match in matches: |
|
151 |
hint = get_hint_for_match(match) |
|
152 |
if hint: |
|
153 |
return hint |
|
154 |
return [_('use a longer password.')] |
|
155 | ||
156 | ||
157 |
def get_hint_for_match(match): |
|
158 |
pattern = match['pattern'] |
|
159 |
hint = None |
|
160 |
if pattern == 'spatial': |
|
161 |
if match['turns'] == 1: |
|
162 |
hint = _('avoid straight rows of keys like "{token}".') |
|
163 |
else: |
|
164 |
hint = _('avoid short keyboard patterns like "{token}".') |
|
165 | ||
166 |
if pattern == 'repeat': |
|
167 |
hint = _('avoid repeated words and characters like "{token}".') |
|
168 | ||
169 |
if pattern == 'sequence': |
|
170 |
hint = _('avoid sequences like "{token}".') |
|
171 | ||
172 |
if pattern == 'regex': |
|
173 |
if match['regex_name'] == 'recent_year': |
|
174 |
hint = _('avoid recent years.') |
|
175 | ||
176 |
if pattern == 'date': |
|
177 |
hint = _('avoid dates and years that are associated with you.') |
|
178 | ||
179 |
if pattern == 'dictionary': |
|
180 |
if match['l33t'] or match['reversed']: |
|
181 |
hint = _('avoid "{token}" : it\'s similar to a commonly used password') |
|
182 |
else: |
|
183 |
hint = _('avoid "{token}" : it\'s a commonly used password.') |
|
184 | ||
185 |
if hint is not None: |
|
186 |
return hint.format(token=match['token']) |
|
187 | ||
188 |
return None |
|
189 | ||
190 | ||
152 | 191 |
def get_password_checker(*args, **kwargs): |
153 | 192 |
return import_string(app_settings.A2_PASSWORD_POLICY_CLASS)(*args, **kwargs) |
154 | 193 |
tests/api/test_all.py | ||
---|---|---|
1756 | 1756 |
assert response.json['checks'][4]['result'] is True |
1757 | 1757 | |
1758 | 1758 | |
1759 |
def test_validate_password_strength(app, settings): |
|
1759 |
@pytest.mark.parametrize( |
|
1760 |
'password, strength', |
|
1761 |
[ |
|
1762 |
('?', 0), |
|
1763 |
('?JR!', 1), |
|
1764 |
('?JR!p4"', 2), |
|
1765 |
('?JR!p4A2"', 3), |
|
1766 |
('?JR!p4A2i:"', 4), |
|
1767 |
], |
|
1768 |
) |
|
1769 |
def test_validate_password_strength(app, settings, password, strength): |
|
1760 | 1770 |
settings.A2_PASSWORD_POLICY_MIN_STRENGTH = 3 |
1761 | 1771 |
response = app.post_json('/api/validate-password/', params={'password': 'short'}) |
1762 | 1772 |
assert response.json['result'] == 1 |
... | ... | |
1764 | 1774 |
assert response.json['ok'] is False |
1765 | 1775 |
assert response.json['hint'] == 'use at least 8 characters' |
1766 | 1776 | |
1767 |
response = app.post_json('/api/validate-password/', params={'password': 'w34k P455w0rd'}) |
|
1777 |
settings.A2_PASSWORD_POLICY_MIN_LENGTH = 0 |
|
1778 |
settings.A2_PASSWORD_POLICY_MIN_CLASSES = 0 |
|
1779 |
response = app.post_json('/api/validate-password/', params={'password': password}) |
|
1768 | 1780 |
assert response.json['result'] == 1 |
1769 |
assert response.json['ok'] is False |
|
1770 |
assert response.json['hint'] == 'Add another word or two. Uncommon words are better.' |
|
1771 |
response = app.post_json('/api/validate-password/', params={'password': 'xbA2E4]#o'}) |
|
1781 |
assert response.json['ok'] == (strength >= 3) |
|
1782 |
assert response.json['strength_label'] == ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong'][strength] |
|
1783 |
assert response.json['strength'] == strength |
|
1784 | ||
1785 | ||
1786 |
@pytest.mark.parametrize( |
|
1787 |
'password, hint', |
|
1788 |
[ |
|
1789 |
('sdfgh', 'avoid straight rows of keys like "sdfgh".'), |
|
1790 |
('ertgfd', 'avoid short keyboard patterns like "ertgfd".'), |
|
1791 |
('abab', 'avoid repeated words and characters like "abab".'), |
|
1792 |
('abcd', 'avoid sequences like "abcd".'), |
|
1793 |
('2019', 'avoid recent years.'), |
|
1794 |
('02/08/14', 'avoid dates and years that are associated with you.'), |
|
1795 |
('02/08/14', 'avoid dates and years that are associated with you.'), |
|
1796 |
('p@ssword', 'avoid "p@ssword" : it\'s similar to a commonly used password'), |
|
1797 |
('password', 'avoid "password" : it\'s a commonly used password.'), |
|
1798 |
], |
|
1799 |
) |
|
1800 |
def test_validate_password_hints(app, settings, password, hint): |
|
1801 |
settings.A2_PASSWORD_POLICY_MIN_STRENGTH = 3 |
|
1802 |
settings.A2_PASSWORD_POLICY_MIN_LENGTH = 0 |
|
1803 |
settings.A2_PASSWORD_POLICY_MIN_CLASSES = 0 |
|
1804 |
response = app.post_json('/api/validate-password/', params={'password': password}) |
|
1772 | 1805 |
assert response.json['result'] == 1 |
1773 |
assert response.json['ok'] is True |
|
1774 |
assert response.json['hint'] == 'add more words or characters' |
|
1806 |
assert response.json['hint'] == hint |
|
1775 | 1807 | |
1776 | 1808 | |
1777 | 1809 |
def test_api_users_get_or_create(settings, app, admin): |
1778 |
- |