add new widget and fields for passwords (#24439)

This commit is contained in:
Benjamin Dauvergne 2018-07-19 16:12:33 +02:00
parent 6a44c5f558
commit f36b480419
7 changed files with 309 additions and 2 deletions

View File

@ -146,6 +146,7 @@ default_settings = dict(
A2_PASSWORD_POLICY_CLASS=Setting(
default='authentic2.passwords.DefaultPasswordChecker',
definition='path of a class to validate passwords'),
A2_PASSWORD_POLICY_SHOW_LAST_CHAR=Setting(default=False, definition='Show last character in password fields'),
A2_AUTH_PASSWORD_ENABLE=Setting(default=True, definition='Activate login/password authentication', names=('AUTH_PASSWORD',)),
A2_LOGIN_FAILURE_COUNT_BEFORE_WARNING=Setting(default=0,
definition='Failure count before logging a warning to '

View File

@ -0,0 +1,35 @@
from django.forms import CharField
from django.utils.translation import ugettext_lazy as _
from authentic2.passwords import password_help_text, validate_password
from authentic2.forms.widgets import PasswordInput, NewPasswordInput, CheckPasswordInput
class PasswordField(CharField):
widget = PasswordInput
class NewPasswordField(CharField):
widget = NewPasswordInput
default_validators = [validate_password]
def __init__(self, *args, **kwargs):
kwargs['help_text'] = password_help_text()
super(NewPasswordField, self).__init__(*args, **kwargs)
class CheckPasswordField(CharField):
widget = CheckPasswordInput
def __init__(self, *args, **kwargs):
kwargs['help_text'] = u'''
<span class="a2-password-check-equality-default">%(default)s</span>
<span class="a2-password-check-equality-matched">%(match)s</span>
<span class="a2-password-check-equality-unmatched">%(nomatch)s</span>
''' % {
'default': _('Both passwords must match.'),
'match': _('Passwords match.'),
'nomatch': _('Passwords do not match.'),
}
super(CheckPasswordField, self).__init__(*args, **kwargs)

View File

@ -12,12 +12,15 @@ import re
import uuid
from django.forms.widgets import DateTimeInput, DateInput, TimeInput
from django.forms.widgets import PasswordInput as BasePasswordInput
from django.utils.formats import get_language, get_format
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from gadjo.templatetags.gadjo import xstatic
from authentic2 import app_settings
DATE_FORMAT_JS_PY_MAPPING = {
'P': '%p',
'ss': '%S',
@ -197,3 +200,45 @@ class TimeWidget(PickerWidgetMixin, TimeInput):
options['format'] = options.get('format', self.get_format())
super(TimeWidget, self).__init__(attrs, options, usel10n)
class PasswordInput(BasePasswordInput):
class Media:
js = ('authentic2/js/password.js',)
css = {
'all': ('authentic2/css/password.css',)
}
def render(self, name, value, attrs=None):
output = super(PasswordInput, self).render(name, value, attrs=attrs)
if attrs and app_settings.A2_PASSWORD_POLICY_SHOW_LAST_CHAR:
_id = attrs.get('id')
if _id:
output += u'''\n<script>a2_password_show_last_char(%s);</script>''' % json.dumps(_id)
return output
class NewPasswordInput(PasswordInput):
def render(self, name, value, attrs=None):
output = super(NewPasswordInput, self).render(name, value, attrs=attrs)
if attrs:
_id = attrs.get('id')
if _id:
output += u'''\n<script>a2_password_validate(%s);</script>''' % json.dumps(_id)
return output
class CheckPasswordInput(PasswordInput):
# this widget must be named xxx2 and the other widget xxx1, it's a
# convention, js code expect it.
def render(self, name, value, attrs=None):
output = super(CheckPasswordInput, self).render(name, value, attrs=attrs)
if attrs:
_id = attrs.get('id')
if _id and _id.endswith('2'):
other_id = _id[:-1] + '1'
output += u'''\n<script>a2_password_check_equality(%s, %s)</script>''' % (
json.dumps(other_id),
json.dumps(_id),
)
return output

View File

@ -7,6 +7,7 @@ import six
from django.utils.translation import ugettext as _
from django.utils.module_loading import import_string
from django.utils.functional import lazy
from django.utils.safestring import mark_safe
from django.core.exceptions import ValidationError
from . import app_settings
@ -110,14 +111,16 @@ def get_password_checker(*args, **kwargs):
def validate_password(password):
error = password_help_text(password, only_errors=True)
if error:
raise ValidationError(error)
raise ValidationError(mark_safe(error))
def password_help_text(password='', only_errors=False):
password_checker = get_password_checker()
criteria = [check.label for check in password_checker(password) if not (only_errors and check.result)]
if criteria:
return _('In order to create a secure password, please use at least: %s') % (', '.join(criteria))
html_criteria = [u'<span class="a2-password-policy-rule">%s</span>' % criter for criter in criteria]
return _('In order to create a secure password, please use at least: '
'<span class="a2-password-policy-container">%s</span>') % (''.join(html_criteria))
else:
return ''

View File

@ -0,0 +1,80 @@
/* position span to show last char */
.a2-password-show-last-char {
text-align: center;
width: 20px;
font-weight: bold;
}
.a2-password-show-last-char + input[type=password] {
padding-left: 1.25rem;
}
.a2-password-nok {
color: red;
}
.a2-password-ok {
color: green;
}
/* default circle icon */
.a2-password-policy-rule:after {
font-family: FontAwesome;
display: inline-block;
width: 1.5ex;
text-align: center;
content: "\f00d"; /* cross icon */
opacity: 0;
}
.a2-password-nok.a2-password-policy-rule:after {
content: "\f00d"; /* cross icon */
color: red;
opacity: 1;
}
.a2-password-ok.a2-password-policy-rule:after {
content: "\f00c"; /* ok icon */
color: green;
opacity: 1;
}
/* Equality check */
.a2-password-nok .a2-password-check-equality-default,
.a2-password-ok .a2-password-check-equality-default {
display: none;
}
.a2-password-check-equality-matched,
.a2-password-check-equality-unmatched {
display: none;
opacity: 0;
transition: all 0.3s ease;
}
.a2-password-nok .a2-password-check-equality-unmatched,
.a2-password-ok .a2-password-check-equality-matched {
display: inline;
opacity: 1;
}
.a2-password-check-equality-default:after,
.a2-password-check-equality-unmatched:after,
.a2-password-check-equality-matched:after {
font-family: FontAwesome;
width: 1rem;
display: inline-block;
}
.a2-password-check-equality-default:after {
content: "\f00d"; /* cross icon */
opacity: 0;
}
.a2-password-check-equality-unmatched:after {
content: "\f00d"; /* cross icon */
}
.a2-password-check-equality-matched:after {
content: "\f00c"; /* ok icon */
}

View File

@ -76,3 +76,7 @@
.a2-log-message {
white-space: pre-wrap;
}
.a2-password-policy-rule {
padding: 0.3ex;
}

View File

@ -0,0 +1,139 @@
a2_password_check_equality = (function () {
return function(id1, id2) {
$(function () {
function check_equality() {
setTimeout(function () {
var $help_text = $input2.parent().find('.helptext');
var password1 = $input1.val();
var password2 = $input2.val();
if (! password2) {
$help_text.removeClass('a2-password-nok');
$help_text.removeClass('a2-password-ok');
} else {
var equal = (password1 == password2);
$help_text.toggleClass('a2-password-ok', equal);
$help_text.toggleClass('a2-password-nok', ! equal);
}
}, 0);
}
var $input1 = $('#' + id1);
var $input2 = $('#' + id2);
$input1.on('change keydown keyup keypress paste', check_equality);
$input2.on('change keydown keyup keypress paste', check_equality);
});
}
})();
a2_password_validate = (function () {
function toggle_error($elt) {
$elt.removeClass('a2-password-check-equality-ok');
$elt.addClass('a2-password-check-equality-error');
}
function toggle_ok($elt) {
$elt.removeClass('a2-password-check-equality-error');
$elt.addClass('a2-password-check-equality-ok');
}
function get_validation($input) {
var password = $input.val();
var $help_text = $input.parent().find('.helptext');
var $policyContainer = $help_text.find('.a2-password-policy-container');
$.ajax({
method: 'POST',
url: '/api/validate-password/',
data: JSON.stringify({'password': password}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
success: function(data) {
if (! data.result) {
return;
}
$policyContainer.empty();
$policyContainer.removeClass('a2-password-ok a2-password-nok');
for (var i = 0; i < data.checks.length; i++) {
var error = data.checks[i];
var $rule = $('<span class="a2-password-policy-rule"/>');
$rule.text(error.label)
$rule.appendTo($policyContainer);
$rule.toggleClass('a2-password-ok', error.result);
$rule.toggleClass('a2-password-nok', ! error.result);
}
}
});
}
function validate_password(event) {
var $input = $(event.target);
setTimeout(function () {
get_validation($input);
}, 0);
}
return function (id) {
var $input = $('#' + id);
$input.on('keyup.a2-password-validate paste.a2-password-validate', validate_password);
}
})();
a2_password_show_last_char = (function () {
function debounce(func, milliseconds) {
var timer;
return function() {
window.clearTimeout(timer);
timer = window.setTimeout(function() {
func();
}, milliseconds);
};
}
return function(id) {
var $input = $('#' + id);
var last_char_id = id + '-last-char';
var $span = $('<span class="a2-password-show-last-char" id="' + last_char_id + '"/>');
function show_last_char(event) {
if (event.keyCode == 32 || event.key === undefined || event.key == ""
|| event.key == "Unidentified" || event.key.length > 1 || event.ctrlKey) {
return;
}
// import input's layout to the span
$span.css({
'position': 'absolute',
'font-size': $input.css('font-size'),
'font-family': $input.css('font-family'),
'line-height': $input.css('line-height'),
'padding-top': $input.css('padding-top'),
'padding-bottom': $input.css('padding-bottom'),
'margin-top': $input.css('margin-top'),
'margin-bottom': $input.css('margin-bottom'),
'border-top-width': $input.css('border-top-width'),
'border-bottom-width': $input.css('border-bottom-width'),
'border-style': 'hidden',
'top': $input.position().top,
'left': $input.position().left,
});
var duration = 1000;
var id = $input.attr('id');
var last_char_id = id + '-last-char';
$('#' + last_char_id)
.text(event.key)
.animate({'opacity': 1}, {
duration: 50,
queue: false,
complete: function () {
var $this = $(this);
window.setTimeout(
debounce(function () {
$this.animate({'opacity': 0}, {
duration: 50
});
}, duration), duration);
}
});
}
// place span absolutery in padding-left of the input
$input.before($span);
$input.on('keypress.a2-password-show-last-char', show_last_char);
}
})();