From 4337d5c4b2bc707ee9a93966b6de11dc1ae55ffa Mon Sep 17 00:00:00 2001 From: Elias Showk Date: Thu, 14 Jun 2018 18:12:21 +0200 Subject: [PATCH] create password validation in registration completion (#24439) --- src/authentic2/registration_backend/forms.py | 13 ++- src/authentic2/registration_backend/views.py | 6 ++ .../static/authentic2/css/style.css | 67 ++++++++++++++ .../static/authentic2/js/password.js | 88 +++++++++++++++++++ .../registration_completion_form.html | 3 +- src/authentic2/validators.py | 40 +++++---- 6 files changed, 197 insertions(+), 20 deletions(-) create mode 100644 src/authentic2/static/authentic2/js/password.js diff --git a/src/authentic2/registration_backend/forms.py b/src/authentic2/registration_backend/forms.py index 8ca7d9c5..23440e87 100644 --- a/src/authentic2/registration_backend/forms.py +++ b/src/authentic2/registration_backend/forms.py @@ -115,10 +115,17 @@ class RegistrationCompletionFormNoPassword(forms.BaseUserForm): class RegistrationCompletionForm(RegistrationCompletionFormNoPassword): - password1 = CharField(widget=PasswordInput, label=_("Password"), + class Media: + js = ('authentic2/js/password.js',) + + password1 = CharField(widget=PasswordInput(attrs={'class': 'validatePassword showPassword passwordEquality'}), + label=_("Password"), validators=[validators.validate_password], - help_text=validators.password_help_text()) - password2 = CharField(widget=PasswordInput, label=_("Password (again)")) + help_text='%s %s' % ('', validators.password_help_text())) + password2 = CharField(widget=PasswordInput(attrs={'class': 'passwordEquality'}), + label=_("Password (again)"), + help_text='%s%s' % \ + (ugettext('Passwords match.'), ugettext('Passwords does not match.'))) def clean(self): """ diff --git a/src/authentic2/registration_backend/views.py b/src/authentic2/registration_backend/views.py index 4f4e5715..19e04d0b 100644 --- a/src/authentic2/registration_backend/views.py +++ b/src/authentic2/registration_backend/views.py @@ -2,6 +2,7 @@ import django import collections import logging import random +import json from django.conf import settings from django.shortcuts import get_object_or_404 @@ -266,6 +267,11 @@ class RegistrationCompletionView(CreateView): ctx['email'] = self.email ctx['email_is_unique'] = self.email_is_unique ctx['create'] = 'create' in self.request.GET + ctx['passwordpolicy'] = json.dumps({ + 'A2_PASSWORD_POLICY_MIN_LENGTH': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH, + 'A2_PASSWORD_POLICY_MIN_CLASSES': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES, + 'A2_PASSWORD_POLICY_REGEX': app_settings.A2_PASSWORD_POLICY_REGEX, + }) return ctx def get(self, request, *args, **kwargs): diff --git a/src/authentic2/static/authentic2/css/style.css b/src/authentic2/static/authentic2/css/style.css index c7545cae..110ff31b 100644 --- a/src/authentic2/static/authentic2/css/style.css +++ b/src/authentic2/static/authentic2/css/style.css @@ -76,3 +76,70 @@ .a2-log-message { white-space: pre-wrap; } + +.a2-policy-block { + display: block; +} + +.a2-min-class-policy, .a2-min-length-policy, .a2-regexp-policy { + display: inline; +} + +.password-error { + font-weight: bold; + color: red; +} + +.password-error:before { + content: "\f071"; + margin-right: 0.3em; + font-family: FontAwesome; + font-size: 100%; + color: red; +} + +.password-ok:before { + content: "\f00c"; + font-family: FontAwesome; + font-size: 100%; + color: green; +} + +.showPassword ~ .helptext{ + display: inline; + margin-left: 0.5em; +} + +.show-password-button { + cursor: pointer; +} + +.show-password-button:after { + content: "\f06e"; + font-family: FontAwesome; + font-size: 150%; +} + +.hide-password-button:after { + content: "\f070"; + font-family: FontAwesome; + font-size: 150%; +} + +.a2-passwords-unmatched { + display: none; + color: red; +} + +.a2-passwords-matched { + display: none; + color: green; +} + +.password-error ~ .helptext .a2-passwords-unmatched { + display: block; +} + +.password-ok ~ .helptext .a2-passwords-matched { + display: block; +} \ No newline at end of file diff --git a/src/authentic2/static/authentic2/js/password.js b/src/authentic2/static/authentic2/js/password.js new file mode 100644 index 00000000..c6ce795c --- /dev/null +++ b/src/authentic2/static/authentic2/js/password.js @@ -0,0 +1,88 @@ +"use strict"; +/* globals $, console, window */ + +$(function () { + var toggleError = function($elt) { + $elt.removeClass('password-ok'); + $elt.addClass('password-error'); + } + var toggleOk = function($elt) { + $elt.removeClass('password-error'); + $elt.addClass('password-ok'); + } + var validatePassword = function () { + var settings = $(this).data('passwordPolicy'); + var minClassElt = $(this).find('.a2-min-class-policy'); + var minLengthElt = $(this).find('.a2-min-length-policy'); + var regexpElt = $(this).find('.a2-regexp-policy'); + $(this) + .find('.validatePassword') + .each(function () { + var password = $(this).val(); + var min_len = settings.A2_PASSWORD_POLICY_MIN_LENGTH; + if (min_len && password.length < min_len) { + toggleError(minLengthElt); + } else { + toggleOk(minLengthElt); + } + var digits = /\d/g; + var lowerCase = /[a-z]/g; + var upperCase = /[A-Z]/g; + var punctuation = /'!"#\$%&\\'\(\)\*\+,-\.\/:;<=>\?@\[\]\^_`\{\|\}~'/g; + var minClassCount = settings.A2_PASSWORD_POLICY_MIN_CLASSES; + var classCount = 0; + if (minClassCount) { + [digits, lowerCase, upperCase, punctuation].forEach(function (cls) { + if (cls.test(password)) classCount++; + }) + if (classCount < minClassCount) { + toggleError(minClassElt); + } else { + toggleOk(minClassElt); + } + } + if (settings.A2_PASSWORD_POLICY_REGEX) { + var regExpObject = new RegExp(settings.A2_PASSWORD_POLICY_REGEX, 'g'); + if (!regExpObject.test(password)) { + toggleError(regexpElt); + } else { + toggleOk(regexpElt); + } + } + }); + } + var passwordEquality = function () { + var form = $(this).parents('form'); + var input0 = form.find('.passwordEquality').eq(0); + var input1 = form.find('.passwordEquality').eq(1); + if (!input1.val() || !input0.val()) return; + if (input0.val() !== input1.val()) { + toggleError(input1); + } else { + toggleOk(input1); + } + } + var showPassword = function () { + $(this).addClass('hide-password-button'); + $(this) + .parents('form') + .find('input.showPassword') + .attr('type', 'text'); + } + var hidePassword = function () { + $(this).removeClass('hide-password-button'); + $(this) + .parents('form') + .find('input.showPassword') + .attr('type', 'password'); + } + // TODO check both passwords are equal + $('form[data-password-policy]') + .keyup(validatePassword); + $('form[data-password-policy] input[type=password].passwordEquality') + .keyup(passwordEquality); + $('form.showPassword') + .on('mousedown', '.show-password-button', showPassword); + $('form.showPassword') + .on('mouseup', '.show-password-button', hidePassword); +}); diff --git a/src/authentic2/templates/registration/registration_completion_form.html b/src/authentic2/templates/registration/registration_completion_form.html index cd179aaa..00ff8838 100644 --- a/src/authentic2/templates/registration/registration_completion_form.html +++ b/src/authentic2/templates/registration/registration_completion_form.html @@ -25,7 +25,8 @@ {% block content %}

{% trans "Registration" %}

{% trans "Please fill the form to complete your registration" %}

-
+ {% csrf_token %} {{ form.as_p }} diff --git a/src/authentic2/validators.py b/src/authentic2/validators.py index c9e9b09b..ac24cb3d 100644 --- a/src/authentic2/validators.py +++ b/src/authentic2/validators.py @@ -120,26 +120,34 @@ class UsernameValidator(RegexValidator): def __password_help_text_helper(): - if app_settings.A2_PASSWORD_POLICY_MIN_LENGTH and \ - app_settings.A2_PASSWORD_POLICY_MIN_CLASSES: - yield ugettext('Your password must contain at least %(min_length)d characters from at ' - 'least %(min_classes)d classes among: lowercase letters, uppercase letters, ' - 'digits and punctuations.') % { - 'min_length': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH, - 'min_classes': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES} + ''' + Password fields help_text + ''' + min_length_html = '%s' %\ + ugettext('Your password must contain at least %(min_length)d characters.' % + {'min_length': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH}) + + min_class_html = '%s' %\ + ugettext(('at least %(min_classes)d classes among: lowercase letters, uppercase letters, digits and punctuations.') % + {'min_classes': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES}) + if app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG: + regexp_html = '%s' %\ + ugettext(app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG) + else: + regexp_html = '%s' %\ + ugettext('Your password must match the regular expression: %(regexp)s, please change this message using the A2_PASSWORD_POLICY_REGEX_ERROR_MSG setting.') % \ + {'regexp': app_settings.A2_PASSWORD_POLICY_REGEX} + + if app_settings.A2_PASSWORD_POLICY_MIN_LENGTH and app_settings.A2_PASSWORD_POLICY_MIN_CLASSES: + yield '%s %s %s' % (min_length_html, _('from'), min_class_html) else: if app_settings.A2_PASSWORD_POLICY_MIN_LENGTH: - yield ugettext('Your password must contain at least %(min_length)d characters.') % {'min_length': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH} + yield min_length_html if app_settings.A2_PASSWORD_POLICY_MIN_CLASSES: - yield ugettext('Your password must contain characters from at least %(min_classes)d ' - 'classes among: lowercase letters, uppercase letters, digits ' - 'and punctuations.') % {'min_classes': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES} + yield "%s %s" % (ugettext('Your password must contain characters from'), min_class_html) if app_settings.A2_PASSWORD_POLICY_REGEX: - yield ugettext(app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG) or \ - ugettext('Your password must match the regular expression: ' - '%(regexp)s, please change this message using the ' - 'A2_PASSWORD_POLICY_REGEX_ERROR_MSG setting.') % \ - {'regexp': app_settings.A2_PASSWORD_POLICY_REGEX} + yield regexp_html + def password_help_text(): return ' '.join(__password_help_text_helper()) -- 2.17.1