From 3238e2d9fcd532807847556514c0519fa0869b14 Mon Sep 17 00:00:00 2001 From: Dylan Hardison Date: Wed, 4 Nov 2015 17:51:25 -0500 Subject: Bug 1177911 - Determine and implement better password requirements for BMO --- Bugzilla/Config/Auth.pm | 5 +- Bugzilla/User.pm | 19 +- js/account.js | 140 +++++++++++++ skins/standard/admin.css | 79 ++++++++ .../en/default/account/email/confirm-new.html.tmpl | 69 ++++--- .../password/set-forgotten-password.html.tmpl | 62 +++--- .../en/default/account/prefs/account.html.tmpl | 218 +++++++++++---------- .../en/default/account/reset-password.html.tmpl | 156 ++++----------- .../en/default/global/password-features.html.tmpl | 27 +++ template/en/default/global/user-error.html.tmpl | 52 +---- 10 files changed, 482 insertions(+), 345 deletions(-) create mode 100644 template/en/default/global/password-features.html.tmpl diff --git a/Bugzilla/Config/Auth.pm b/Bugzilla/Config/Auth.pm index 36287b107..ac5394f04 100644 --- a/Bugzilla/Config/Auth.pm +++ b/Bugzilla/Config/Auth.pm @@ -132,9 +132,8 @@ sub get_param_list { { name => 'password_complexity', type => 's', - choices => [ 'no_constraints', 'mixed_letters', 'letters_numbers', - 'letters_numbers_specialchars' ], - default => 'no_constraints', + choices => [ 'no_constraints', 'bmo' ], + default => 'bmo', checker => \&check_multi }, diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm index ebd82002f..1a0deed6b 100644 --- a/Bugzilla/User.pm +++ b/Bugzilla/User.pm @@ -2482,15 +2482,16 @@ sub validate_password_check { } my $complexity_level = Bugzilla->params->{password_complexity}; - if ($complexity_level eq 'letters_numbers_specialchars') { - return 'password_not_complex' - if ($password !~ /[[:alpha:]]/ || $password !~ /\d/ || $password !~ /[[:punct:]]/); - } elsif ($complexity_level eq 'letters_numbers') { - return 'password_not_complex' - if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/ || $password !~ /\d/); - } elsif ($complexity_level eq 'mixed_letters') { - return 'password_not_complex' - if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/); + if ($complexity_level eq 'bmo') { + my $features = 0; + + $features++ if $password =~ /[a-z]/; + $features++ if $password =~ /[A-Z]/; + $features++ if $password =~ /[0-9]/; + $features++ if $password =~ /[^A-Za-z0-9]/; + $features++ if length($password) > 12; + + return 'password_not_complex' if $features < 3; } # Having done these checks makes us consider the password untainted. diff --git a/js/account.js b/js/account.js index 31c1a50e6..c268bb8e5 100644 --- a/js/account.js +++ b/js/account.js @@ -7,6 +7,146 @@ $(function() { + function make_password_strength($password) { + return function(event) { + var password = $password.val(); + var missing_features = {"upper": true, "lower": true, "numbers": true, "symbols": true, "length12": true}; + var features = [], + charset = 0, + score = 0, + min_features = 3; + + $("#password-meter").show(); + $("#password-meter-label").show(); + + if (password.match(/[A-Z]/)) { + delete missing_features.upper; + features.push("upper"); + charset += 26; + } + if (password.match(/[a-z]/)) { + delete missing_features.lower; + features.push("lower"); + charset += 26; + } + if (password.match(/[0-9]/)) { + delete missing_features.numbers; + features.push("numbers"); + charset += 10; + } + if (password.match(/[^A-Za-z0-9]/)) { + delete missing_features.symbols; + features.push("symbols"); + charset += 30; // there are 30-32 typable characters on a keyboard. + } + if (password.length > 12) { + delete missing_features.length12; + features.push("length12"); + } + + $("#password-features li").removeClass("feature-ok"); + features.forEach(function(name) { + $("#password-feature-" + name).addClass("feature-ok"); + }); + + var entropy = Math.floor(Math.log(charset) * (password.length / Math.log(2))); + if (entropy) { + score = entropy/128; + } + + $password.get(0).setCustomValidity(""); + if (features.length < min_features) { + $("#password-msg") + .text("Password does not meet requirements") + .attr("class", "password-bad"); + $password.get(0).setCustomValidity($("#password-msg").text()); + } + else if (password.length < 8) { + $("#password-msg") + .text("Password is too short") + .attr("class", "password-bad"); + $password.get(0).setCustomValidity($("#password-msg").text()); + } + else { + $("#password-msg") + .text("Password meets requirements") + .attr("class", "password-good"); + $password.get(0).setCustomValidity(""); + } + + if (entropy < 60) { + $("#password-meter") + .removeClass("meter-good meter-ok") + .addClass("meter-bad"); + } + else if (entropy >= 120) { + $("#password-meter") + .removeClass("meter-bad meter-ok") + .addClass("meter-good"); + } + else if (entropy > 60) { + $("#password-meter") + .removeClass("meter-bad meter-good") + .addClass("meter-ok"); + } + + if (score === 0) { + score = 0.01; + $("#password-meter") + .removeClass("meter-good meter-ok") + .addClass("meter-bad"); + } + + $("#password-meter").width(Math.max(0, Math.min($password.width()+10, Math.ceil(($password.width()+10) * score)))); + }; + } + + function make_password_confirm($password1, $password2) { + return function (event) { + if ($password1.val() != $password2.val()) { + $password2.get(0).setCustomValidity("Does not match previous password"); + } + else { + $password2.get(0).setCustomValidity(""); + } + }; + } + var password1_sel, password2_sel; + var complexity = $("#password-features").data("password-complexity"); + var page = $("#password-features").data("password-page"); + var check_password_strength, check_password_confirm; + + if (page == "account") { + $("#new_password1, #new_password2, #new_login_name").change(function() { + $("#old_password").attr("required", true); + }); + } + + if (complexity == "bmo") { + if (page == "confirm") { + password1_sel = "#passwd1"; + password2_sel = "#passwd2"; + } + else { + password1_sel = "#new_password1"; + password2_sel = "#new_password2"; + } + $("#password-features").show(); + + check_password_strength = make_password_strength($(password1_sel)); + check_password_confirm = make_password_confirm($(password1_sel), $(password2_sel)); + + $(password1_sel).on("input", check_password_strength); + $(password1_sel).on("focus", check_password_strength); + + $(password1_sel).on("blur", check_password_confirm); + $(password1_sel).on("change", check_password_confirm); + $(password2_sel).on("input", check_password_confirm); + } + else { + $("#password-features").hide(); + } + // account disabling $('#account-disable-toggle') diff --git a/skins/standard/admin.css b/skins/standard/admin.css index 2b56c1831..b6ac508c2 100644 --- a/skins/standard/admin.css +++ b/skins/standard/admin.css @@ -314,3 +314,82 @@ input[disabled] { float: left; margin-right: 1em; } + +.flex { + display: flex; + flex-flow: row; +} + +.flex-left { + flex: 1; + min-width: 500px; +} + +.flex-right { + flex: 2; +} + +.meter { + width: 25px; + height: 1em; + display: inline-block; +} + +.meter-bad { + background-color: #DD514C; + background-image: linear-gradient(to bottom, #EE5F5B, #C43C35); + background-repeat: repeat-x; +} + +.meter-ok { + background-color: #FAA732; + background-image: linear-gradient(to bottom, #FBB450, #F89406); + background-repeat: repeat-x; +} + +.meter-good { + background-color: #5EB95E; + background-image: linear-gradient(to bottom, #62C462, #57A957); + background-repeat: repeat-x; +} + +.password-bad { + color: #DD514C; +} + +.password-ok { + color: #FAA732; +} + +.password-good { + color: #5EB95E; +} + +#password-features { + display: none; +} + +#password-features li.feature-ok:before { + font-size: normal; + content: "\2611"; + margin-left: -13px; + margin-right: 2px; +} + +#password-features li.feature-ok { + color: #5EB95E; +} + +#password-features li:before { + font-size: normal; + content: "\2610"; + margin-left: -13px; + margin-right: 2px; +} + +#password-features ul { + padding-left: 20px; + text-indent: 2px; + list-style: none; + list-style-position: outside; +} diff --git a/template/en/default/account/email/confirm-new.html.tmpl b/template/en/default/account/email/confirm-new.html.tmpl index 3241030e8..f505268f5 100644 --- a/template/en/default/account/email/confirm-new.html.tmpl +++ b/template/en/default/account/email/confirm-new.html.tmpl @@ -21,8 +21,10 @@ [% title = BLOCK %]Create a new user account for '[% email FILTER html %]'[% END %] [% PROCESS "global/header.html.tmpl" - title = title - onload = "document.forms['confirm_account_form'].realname.focus();" %] + title = title + style_urls = ['skins/standard/admin.css'] + javascript_urls = ['js/account.js'] + onload = "document.forms['confirm_account_form'].realname.focus();" %] [% password_complexity = Param('password_complexity') %] @@ -35,39 +37,36 @@
- - - - - - - - - - - - - - - - - - - - - -
Email Address:[% email FILTER html %]
(OPTIONAL) :
: - - (Password should be a minimum of [% constants.USER_PASSWORD_MIN_LENGTH FILTER none %] characters long - [% IF password_complexity == "mixed_letters" %] - and must contain at least one UPPER and one lowercase letter - [% ELSIF password_complexity == "letters_numbers" %] - and must contain at least one UPPER and one lowercase letter and a number - [% ELSIF password_complexity == "letters_numbers_specialchars" %] - and must contain at least one letter, a number and a special character - [% END ~%] - .) -
:
 
+
+
+ + + + + + + + + + + + + + + + + + + + + +
Email Address:[% email FILTER html %]
(OPTIONAL) :
: + +
:
 
+
+ + [% INCLUDE "global/password-features.html.tmpl" class="flex-right" password_page="confirm" %] +

diff --git a/template/en/default/account/password/set-forgotten-password.html.tmpl b/template/en/default/account/password/set-forgotten-password.html.tmpl index cfeacbb93..68119252e 100644 --- a/template/en/default/account/password/set-forgotten-password.html.tmpl +++ b/template/en/default/account/password/set-forgotten-password.html.tmpl @@ -19,39 +19,47 @@ #%] [% title = "Change Password" %] -[% PROCESS global/header.html.tmpl %] + [% PROCESS global/header.html.tmpl + style_urls = ['skins/standard/admin.css'] + javascript_urls = ['js/account.js'] + %]

To change your password, enter a new password twice:

-
- - - - - - - +
+
+ + + +
New Password: - - (minimum [% constants.USER_PASSWORD_MIN_LENGTH FILTER none %] characters) -
+ + + + - - - - + + + + - - - - -
New Password: + +
New Password Again: - -
New Password Again: + +
  - - [% INCLUDE mfa/protected.html.tmpl user=token_user %] -
-
+ +   + + + [% INCLUDE mfa/protected.html.tmpl user=token_user %] + + + + + + + [% INCLUDE "global/password-features.html.tmpl" class="flex-right" password_page="forgot" %] + [% PROCESS global/footer.html.tmpl %] diff --git a/template/en/default/account/prefs/account.html.tmpl b/template/en/default/account/prefs/account.html.tmpl index 3f838691b..c41ea116f 100644 --- a/template/en/default/account/prefs/account.html.tmpl +++ b/template/en/default/account/prefs/account.html.tmpl @@ -27,129 +27,133 @@ [%# BMO - add hook for displaying user-profile link %] [% Hook.process('start') %] - - - - - - - [%# BMO - moved field hook from end of file to here to group with other account fields %] - [% Hook.process('field') %] - - [% SET can_change = [] %] - [% IF user.authorizer.can_change_password %] - [% can_change.push('password') %] - [% END %] - [% IF user.authorizer.can_change_email && Param('allowemailchange') %] - [% can_change.push('email address') %] - [% END %] - - [% IF can_change.size %] - - - - - - - - - - - - [% IF user.authorizer.can_change_password %] +
+
+
Your real name: - - optional, but encouraged -

- Your current password is required to - confirm [% can_change.join(' or ') FILTER html %] - changes. -
Current password: - - - I forgot my password -
- + - - - - - [% END %] + [%# BMO - moved field hook from end of file to here to group with other account fields %] + [% Hook.process('field') %] - [% IF user.authorizer.can_change_email && Param('allowemailchange') %] - [% IF login_change_date %] - [% IF new_login_name %] - - - - - - - - - [% ELSE %] + [% SET can_change = [] %] + [% IF user.authorizer.can_change_password %] + [% can_change.push('password') %] + [% END %] + [% IF user.authorizer.can_change_email && Param('allowemailchange') %] + [% can_change.push('email address') %] + [% END %] + + [% IF can_change.size %] + + + + + + + + + + + + [% IF user.authorizer.can_change_password %] - - + + - - + + [% END %] - [% ELSE %] + + [% IF user.authorizer.can_change_email && Param('allowemailchange') %] + [% IF login_change_date %] + [% IF new_login_name %] + + + + + + + + + [% ELSE %] + + + + + + + + + [% END %] + [% ELSE %] + + + + + [% END %] + [% END %] - - + + [% END %] - [% END %] - - - - - [% END %] - - - - - - - - - - - - + + + + + + + + + + + + +
New password:Your real name: - - [% INCLUDE "mfa/protected.html.tmpl" %] +
Confirm new password: - -
Pending email address:[% new_login_name FILTER html %]
Change request expires:[% login_change_date FILTER time %]

+ Your current password is required to + confirm [% can_change.join(' or ') FILTER html %] + changes. +
Current password: + + + I forgot my password +
Confirmed email address:[% user.login FILTER html %]New password: + + [% INCLUDE "mfa/protected.html.tmpl" %] +
Completion date:[% login_change_date FILTER time %]Confirm new password: + +
Pending email address:[% new_login_name FILTER html %]
Change request expires:[% login_change_date FILTER time %]
Confirmed email address:[% user.login FILTER html %]
Completion date:[% login_change_date FILTER time %]
New email address: + + [% INCLUDE "mfa/protected.html.tmpl" %] +
New email address: - - [% INCLUDE "mfa/protected.html.tmpl" %] -

- Disable My Account -

+ Disable My Account +

+ - + [% INCLUDE "global/password-features.html.tmpl" class="flex-right" password_page="account" %] + [% tab_footer = BLOCK %]
diff --git a/template/en/default/account/reset-password.html.tmpl b/template/en/default/account/reset-password.html.tmpl index 2b1d297dc..ec57f19dd 100644 --- a/template/en/default/account/reset-password.html.tmpl +++ b/template/en/default/account/reset-password.html.tmpl @@ -52,78 +52,6 @@ [% inline_js = BLOCK %] $(function() { - - $('#old_password, #new_password1, #new_password2') - .keyup(function() { - var errors = []; - var old = $('#old_password').val(); - var new1 = $('#new_password1').val(); - var new2 = $('#new_password2').val(); - - if (old === '') { - errors.push('Missing current password'); - } - if (new1 === '' || new2 === '') { - errors.push('Missing new password'); - } - else if (new1 !== new2) { - errors.push('New passwords do not match'); - } - else if (new1 === old) { - errors.push('Your new password must be different from your old password'); - } - else if (new1.length < [% constants.USER_PASSWORD_MIN_LENGTH FILTER none %]) { - errors.push('Your password must be at least [% constants.USER_PASSWORD_MIN_LENGTH FILTER none %] long'); - } - else { - var complexity_fn; - [% SWITCH Param('password_complexity') %] - [% CASE 'no_constraints' %] - complexity_fn = function() {}; - [% CASE 'mixed_letters' %] - complexity_fn = function(pass, errors) { - if ( - pass.search(/[a-z]/) == -1 || - pass.search(/[A-Z]/) == -1 - ) { - errors.push('New password is not complex enough'); - } - }; - [% CASE 'letters_numbers' %] - complexity_fn = function(pass, errors) { - if ( - pass.search(/[a-z]/) == -1 || - pass.search(/[A-Z]/) == -1 || - pass.search(/[0-9]/) == -1 - ) { - errors.push('New password is not complex enough'); - } - }; - [% CASE 'letters_numbers_specialchars' %] - complexity_fn = function(pass, errors) { - if ( - pass.search(/[a-z]/) == -1 || - pass.search(/[A-Z]/) == -1 || - pass.search(/[0-9]/) == -1 || - pass.search(/\W/) == -1 - ) { - errors.push('New password is not complex enough'); - } - }; - [% END %] - complexity_fn(new1, errors); - } - - $('#submit').attr('disabled', errors.length > 0); - if ((old !== '' || new1 !== '' || new2 !== '') && errors.length) { - $('#errors').html(''); - } - else { - $('#errors').html(''); - } - }) - .keyup(); - $('#forgot_password') .click(function(event) { event.preventDefault(); @@ -134,9 +62,11 @@ $(function() { [% END %] [% PROCESS global/header.html.tmpl - title = "Password change required" - style = inline_style - javascript = inline_js + title = "Password change required" + style = inline_style + style_urls = ['skins/standard/admin.css'] + javascript = inline_js + javascript_urls = ['js/account.js'] %]

Password Reset

@@ -149,58 +79,48 @@ $(function() { -
-
 
-
-
Email
-
- [% user.login FILTER html %] +
+
+
 
+
+
Email
+
+ [% user.login FILTER html %] +
-
-
-
Current Password
-
- +
+
Current Password
+
+ +
-
-
 
-
-
-
New Password
-
- +
 
+
+
+
New Password
+
+ +
-
-
-
New Password
-
- - (again) +
+
New Password
+
+ + (again) +
-
-
 
-
-
- - Forgot Password +
 
+
+
+ [% INCLUDE "global/password-features.html.tmpl" class="flex-right" password_page="reset_password" %]
- -

- Your password must be a minimum of [% constants.USER_PASSWORD_MIN_LENGTH FILTER none %] characters long - [% SWITCH Param('password_complexity') %] - [% CASE 'mixed_letters' %] - and must contain at least one UPPER and one lowercase letter - [% CASE 'letters_numbers' %] - and must contain at least one UPPER and one lowercase letter and a number - [% CASE 'letters_numbers_specialchars' %] - and must contain at least one letter, a number and a special character - [% END ~%]. -

-
diff --git a/template/en/default/global/password-features.html.tmpl b/template/en/default/global/password-features.html.tmpl new file mode 100644 index 000000000..5d6c0f8c1 --- /dev/null +++ b/template/en/default/global/password-features.html.tmpl @@ -0,0 +1,27 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + + \ No newline at end of file diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl index 6996832aa..14399f010 100644 --- a/template/en/default/global/user-error.html.tmpl +++ b/template/en/default/global/user-error.html.tmpl @@ -1503,59 +1503,19 @@ [% ELSIF error == "password_not_complex" %] [% title = "Password Fails Requirements" %] - [% passregex = Param('password_complexity') %] - The password must contain at least one: + The Password must meet three of the following requirements
    - [% IF passregex == 'letters_numbers_specialchars' %] -
  • letter
  • -
  • special character
  • - [% ELSIF passregex.search('letters') %] -
  • UPPERCASE letter
  • -
  • lowercase letter
  • - [% END %] - [% IF passregex.search('numbers') %] -
  • digit
  • - [% END %] +
  • uppercase letters
  • +
  • lowercase letters
  • +
  • numbers
  • +
  • symbols
  • +
  • longer than 12 characters
[% IF locked_user %] You must request a new password in order to log in again. [% END %] - [% ELSIF error == "password_not_complex" %] - [% title = "Password Fails Requirements" %] - [% passregex = Param('password_complexity') %] - Password must contain at least one: -
    - [% IF passregex.search('letters') %] -
  • UPPERCASE letter
  • -
  • lowercase letter
  • - [% END %] - [% IF passregex.search('numbers') %] -
  • digit
  • - [% END %] - [% IF passregex.search('specialchars') %] -
  • special character
  • - [% END %] -
- - [% ELSIF error == "password_not_complex" %] - [% title = "Password Fails Requirements" %] - [% passregex = Param('password_complexity') %] - Password must contain at least one: -
    - [% IF passregex.search('letters') %] -
  • UPPERCASE letter
  • -
  • lowercase letter
  • - [% END %] - [% IF passregex.search('numbers') %] -
  • digit
  • - [% END %] - [% IF passregex.search('specialchars') %] -
  • special character
  • - [% END %] -
- [% ELSIF error == "product_access_denied" %] [% title = "Product Access Denied" %] Either the product -- cgit v1.2.3-24-g4f1b