summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDylan William Hardison <dylan@hardison.net>2017-09-19 17:48:11 +0200
committerGitHub <noreply@github.com>2017-09-19 17:48:11 +0200
commit64edd1e8659406d638ec44bf6bc2e69ff7e31ef0 (patch)
treee11680ca012ae863235c9ea3c74a19684b7be7a6
parent5abefb689bc17dc26e747164f9251926faade358 (diff)
downloadbugzilla-64edd1e8659406d638ec44bf6bc2e69ff7e31ef0.tar.gz
bugzilla-64edd1e8659406d638ec44bf6bc2e69ff7e31ef0.tar.xz
Bug 1400949 - Password requirements not stated
-rw-r--r--Bugzilla/Config/Auth.pm6
-rw-r--r--js/account.js129
-rw-r--r--node_modules/passwdqc/src/passwdqc_check.js492
-rw-r--r--skins/standard/admin.css65
-rw-r--r--template/en/default/account/email/confirm-new.html.tmpl5
-rw-r--r--template/en/default/account/password/set-forgotten-password.html.tmpl5
-rw-r--r--template/en/default/account/prefs/account.html.tmpl6
-rw-r--r--template/en/default/account/reset-password.html.tmpl4
-rw-r--r--template/en/default/admin/params/auth.html.tmpl7
-rw-r--r--template/en/default/global/password-features.html.tmpl22
10 files changed, 536 insertions, 205 deletions
diff --git a/Bugzilla/Config/Auth.pm b/Bugzilla/Config/Auth.pm
index 612fd1f3f..965d922d7 100644
--- a/Bugzilla/Config/Auth.pm
+++ b/Bugzilla/Config/Auth.pm
@@ -158,6 +158,12 @@ sub get_param_list {
},
{
+ name => 'passwdqc_desc',
+ type => 'l',
+ default => 'The password must be complex.',
+ },
+
+ {
name => 'auth_delegation',
type => 'b',
default => 0,
diff --git a/js/account.js b/js/account.js
index 8642cadbd..87e1e01f2 100644
--- a/js/account.js
+++ b/js/account.js
@@ -7,100 +7,6 @@
$(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()) {
@@ -112,9 +18,8 @@ $(function() {
};
}
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;
+ var check_password_confirm;
if (page == "account") {
$("#new_password1, #new_password2, #new_login_name").change(function() {
@@ -122,31 +27,21 @@ $(function() {
});
}
- 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);
+ if (page == "confirm") {
+ password1_sel = "#passwd1";
+ password2_sel = "#passwd2";
}
else {
- $("#password-features").hide();
+ password1_sel = "#new_password1";
+ password2_sel = "#new_password2";
}
+ check_password_confirm = make_password_confirm($(password1_sel), $(password2_sel));
+
+ $(password1_sel).on("blur", check_password_confirm);
+ $(password1_sel).on("change", check_password_confirm);
+ $(password2_sel).on("input", check_password_confirm);
+
// account disabling
$('#account-disable-toggle')
diff --git a/node_modules/passwdqc/src/passwdqc_check.js b/node_modules/passwdqc/src/passwdqc_check.js
new file mode 100644
index 000000000..913bcc008
--- /dev/null
+++ b/node_modules/passwdqc/src/passwdqc_check.js
@@ -0,0 +1,492 @@
+/*
+ * Copyright (c) 2000-2002,2010,2013 by Solar Designer. See LICENSE.
+ * Copyright (c) 2014 Parallels, Inc.
+ */
+({define:typeof define!="undefined"?define:function(deps, factory){module.exports = factory(exports, require("./dictionary"));}}).
+define(["exports", "./dictionary"], function(exports, dict){
+ var dictionary = dict.dictionary;
+
+ var FIXED_BITS = 15;
+
+ /*
+ * Calculates the expected number of different characters for a random
+ * password of a given length. The result is rounded down. We use this
+ * with the _requested_ minimum length (so longer passwords don't have
+ * to meet this strict requirement for their length).
+ */
+ function expected_different(charset, length){
+ var x, y, z;
+
+ x = ((charset - 1) << FIXED_BITS) / charset;
+ y = x;
+ while (--length > 0)
+ y = (y * x) >> FIXED_BITS;
+ z = charset * ((1 << FIXED_BITS) - y);
+
+ return (z >> FIXED_BITS)|0;
+ }
+
+ /*
+ * A password is too simple if it is too short for its class, or doesn't
+ * contain enough different characters for its class, or doesn't contain
+ * enough words for a passphrase.
+ *
+ * The biases are added to the length, and they may be positive or negative.
+ * The passphrase length check uses passphrase_bias instead of bias so that
+ * zero may be passed for this parameter when the (other) bias is non-zero
+ * because of a dictionary word, which is perfectly normal for a passphrase.
+ * The biases do not affect the number of different characters, character
+ * classes, and word count.
+ */
+ function is_simple(params, newpass, bias, passphrase_bias){
+ var length, classes, words, chars,
+ digits, lowers, uppers, others, unknowns,
+ c, p;
+
+ length = classes = words = chars = 0;
+ digits = lowers = uppers = others = unknowns = 0;
+ p = ' ';
+ while (c = newpass[length]) {
+ length++;
+
+ if (!isascii(c))
+ unknowns++;
+ else if (isdigit(c))
+ digits++;
+ else if (islower(c))
+ lowers++;
+ else if (isupper(c))
+ uppers++;
+ else
+ others++;
+ /* A word starts when a letter follows a non-letter or when a non-ASCII
+ * character follows a space character. We treat all non-ASCII characters
+ * as non-spaces, which is not entirely correct (there's the non-breaking
+ * space character at 0xa0, 0x9a, or 0xff), but it should not hurt. */
+ if (isascii(p)) {
+ if (isascii(c)) {
+ if (isalpha(c) && !isalpha(p))
+ words++;
+ } else if (isspace(p))
+ words++;
+ }
+ p = c;
+
+ /* Count this character just once: when we're not going to see it anymore */
+ if(newpass.slice(length).indexOf(c) === -1)
+ chars++;
+ }
+
+ length = strlen(newpass);
+
+ if (!length)
+ return 1;
+
+ /* Upper case characters and digits used in common ways don't increase the
+ * strength of a password */
+ c = newpass[0];
+ if (uppers && isascii(c) && isupper(c))
+ uppers--;
+ c = newpass[length - 1];
+ if (digits && isascii(c) && isdigit(c))
+ digits--;
+
+ /* Count the number of different character classes we've seen. We assume
+ * that there are no non-ASCII characters for digits. */
+ classes = 0;
+ if (digits)
+ classes++;
+ if (lowers)
+ classes++;
+ if (uppers)
+ classes++;
+ if (others)
+ classes++;
+ if (unknowns && classes <= 1 && (!classes || digits || words >= 2))
+ classes++;
+
+ for (var min = params.min; classes > 0; classes--)
+ switch (classes) {
+ case 1:
+ if (length + bias >= min[0] &&
+ chars >= expected_different(10, min[0]) - 1)
+ return 0;
+ return 1;
+
+ case 2:
+ if (length + bias >= min[1] &&
+ chars >= expected_different(36, min[1]) - 1)
+ return 0;
+ if (!params.passphrase_words ||
+ words < params.passphrase_words)
+ continue;
+ if (length + passphrase_bias >= min[2] &&
+ chars >= expected_different(27, min[2]) - 1)
+ return 0;
+ continue;
+
+ case 3:
+ if (length + bias >= min[3] &&
+ chars >= expected_different(62, min[3]) - 1)
+ return 0;
+ continue;
+
+ case 4:
+ if (length + bias >= min[4] &&
+ chars >= expected_different(95, min[4]) - 1)
+ return 0;
+ continue;
+ }
+
+ return 1;
+ }
+
+ function unify(dst, src){
+ for (var i = 0; i < src.length; i++){
+ var c = src.charAt(i);
+ if (isascii(c) && isupper(c))
+ c = c.toLowerCase();
+ switch (c) {
+ case 'a': case '@':
+ c = '4'; break;
+ case 'e':
+ c = '3'; break;
+ /* Unfortunately, if we translate both 'i' and 'l' to '1', this would
+ * associate these two letters with each other - e.g., "mile" would
+ * match "MLLE", which is undesired. To solve this, we'd need to test
+ * different translations separately, which is not implemented yet. */
+ case 'i': case '|':
+ c = '!'; break;
+ case 'l':
+ c = '1'; break;
+ case 'o':
+ c = '0'; break;
+ case 's': case '$':
+ c = '5'; break;
+ case 't': case '+':
+ c = '7'; break;
+ }
+ dst += c;
+ }
+
+ return dst;
+ }
+
+ function reverse(src){
+ return src.split("").reverse().join("");
+ }
+
+ /*
+ * Needle is based on haystack if both contain a long enough common
+ * substring and needle would be too simple for a password with the
+ * substring either removed with partial length credit for it added
+ * or partially discounted for the purpose of the length check.
+ */
+ function is_based(params, haystack, needle, original, mode){
+ var scratch, length, i, j, p, worst_bias;
+
+ if (!params.match_length) // disabled
+ return 0;
+
+ if (params.match_length < 0) // misconfigured
+ return 1;
+
+ scratch = null;
+ worst_bias = 0;
+
+ length = needle.length;
+ for (i = 0; i <= length - params.match_length; i++)
+ for (j = params.match_length; i + j <= length; j++) {
+ var bias = 0, j1 = j - 1;
+ var q0 = needle[i], q1 = needle.slice(i+1);
+
+ for (var k=0; k<haystack.length; k++)
+ if (haystack[k] == q0 && haystack.substring(k+1, k+1+j1) == q1.substring(0,j1)) { // or memcmp()
+ if ((mode & 0xff) == 0) { // remove & credit
+ // remove j chars
+ var pos = length - (i + j);
+ if (!(mode & 0x100)) // not reversed
+ pos = i;
+
+ scratch = original.substring(0, pos) + original.substring(pos+j);
+
+ // add credit for match_length - 1 chars
+ bias = params.match_length - 1;
+ if (is_simple(params, scratch, bias, bias))
+ return 1;
+ } else { // discount
+ // Require a 1 character longer match for substrings containing leetspeak
+ // when matching against dictionary words
+ bias = -1;
+ if ((mode & 0xff) == 1) { // words
+ var pos = i, end = i + j;
+ if (mode & 0x100) { // reversed
+ pos = length - end;
+ end = length - i;
+ }
+ for (; pos < end; pos++)
+ if (!isalpha(original[pos])) {
+ if (j == params.match_length){
+ var cnt = true;
+ break;
+ }
+ bias = 0;
+ break;
+ }
+ if(cnt){
+ cnt = false;
+ continue;
+ }
+ }
+
+ // discount j - (match_length + bias) chars
+ bias += params.match_length - j;
+ // bias <= -1
+ if (bias < worst_bias) {
+ if (is_simple(params, original, bias,
+ (mode & 0xff) == 1 ? 0 : bias))
+ return 1;
+ worst_bias = bias;
+ }
+ }
+ }
+
+ // Zero bias implies that there were no matches for this length. If so,
+ // * there's no reason to try the next substring length (it would result in
+ // * no matches as well). We break out of the substring length loop and
+ // * proceed with all substring lengths for the next position in needle.
+ if (!bias)
+ break;
+ }
+
+ return 0;
+ }
+
+ /*
+ * Common sequences of characters.
+ * We don't need to list any of the entire strings in reverse order because the
+ * code checks the new password in both "unified" and "unified and reversed"
+ * form against these strings (unifying them first indeed). We also don't have
+ * to include common repeats of characters (e.g., "777", "!!!", "1000") because
+ * these are often taken care of by the requirement on the number of different
+ * characters.
+ */
+ var seq = [
+ "0123456789",
+ "`1234567890-=",
+ "~!@#$%^&*()_+",
+ "abcdefghijklmnopqrstuvwxyz",
+ "a1b2c3d4e5f6g7h8i9j0",
+ "1a2b3c4d5e6f7g8h9i0j",
+ "abc123",
+ "qwertyuiop[]\\asdfghjkl;'zxcvbnm,./",
+ "qwertyuiop{}|asdfghjkl:\"zxcvbnm<>?",
+ "qwertyuiopasdfghjklzxcvbnm",
+ "1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik,9ol.0p;/-['=]\\",
+ "!qaz@wsx#edc$rfv%tgb^yhn&ujm*ik<(ol>)p:?_{\"+}|",
+ "qazwsxedcrfvtgbyhnujmikolp",
+ "1q2w3e4r5t6y7u8i9o0p-[=]",
+ "q1w2e3r4t5y6u7i8o9p0[-]=\\",
+ "1qaz1qaz",
+ "1qaz!qaz", /* can't unify '1' and '!' - see comment in unify() */
+ "1qazzaq1",
+ "zaq!1qaz",
+ "zaq!2wsx"
+ ];
+
+ /*
+ * This wordlist check is now the least important given the checks above
+ * and the support for passphrases (which are based on dictionary words,
+ * and checked by other means). It is still useful to trap simple short
+ * passwords (if short passwords are allowed) that are word-based, but
+ * passed the other checks due to uncommon capitalization, digits, and
+ * special characters. We (mis)use the same set of words that are used
+ * to generate random passwords. This list is much smaller than those
+ * used for password crackers, and it doesn't contain common passwords
+ * that aren't short English words. Perhaps support for large wordlists
+ * should still be added, even though this is now of little importance.
+ */
+ function is_word_based(params, needle, original, is_reversed){
+ var word, unified, i, length, mode;
+
+ if (!params.match_length) /* disabled */
+ return null;
+
+ mode = is_reversed | 1;
+ word = "";
+ for (i = 0; i < 0x1000; i++) {
+ word = dictionary[i];
+ length = strlen(word);
+ if (length < params.match_length)
+ continue;
+
+ word = unify("", word);
+ if (is_based(params, word, needle, original, mode))
+ return REASON_WORD;
+ }
+
+ mode = is_reversed | 2;
+ for (i = 0; i < seq.length; i++) {
+ unified = unify("", seq[i]);
+ if (!unified)
+ return REASON_ERROR;
+ if (is_based(params, unified, needle, original, mode))
+ return REASON_SEQ;
+ }
+
+ if (params.match_length <= 4)
+ for (i = 1900; i <= 2039; i++) {
+ if (is_based(params, i.toString(), needle, original, mode))
+ return REASON_SEQ;
+ }
+
+ return null;
+ }
+
+ function passwdqc_check(params, newpass, oldpass, pw){
+ var truncated, u_newpass, u_reversed, u_oldpass,
+ u_name, u_gecos, u_dir, reason, length;
+
+ u_newpass = u_reversed = null;
+ u_oldpass = null;
+ u_name = u_gecos = u_dir = null;
+
+ reason = REASON_ERROR;
+
+ if (oldpass && oldpass == newpass)
+ return REASON_SAME;
+
+ length = strlen(newpass);
+
+ if (length < params.min[4])
+ return REASON_SHORT;
+
+ if (length > params.max) {
+ if (params.max == 8) {
+ truncated = newpass.substr(0, 8);
+ newpass = truncated;
+ if (oldpass && !oldpass.substr(0, 8) !== newpass.substr(0, 8))
+ return REASON_SAME;
+ } else {
+ return REASON_LONG;
+ }
+ }
+
+ if (is_simple(params, newpass, 0, 0)) {
+ reason = REASON_SIMPLE;
+ if (length < params.min[1] && params.min[1] <= params.max)
+ reason = REASON_SIMPLESHORT;
+ return reason;
+ }
+
+ if (!(u_newpass = unify("", newpass)))
+ return reason; /* REASON_ERROR */
+ if (!(u_reversed = reverse(u_newpass)))
+ return reason;
+ if (oldpass && !(u_oldpass = unify("", oldpass)))
+ return reason;
+ if (pw) {
+ if (!(u_name = unify("", pw.pw_name)) ||
+ !(u_gecos = unify("", pw.pw_gecos)) ||
+ !(u_dir = unify("", pw.pw_dir)))
+ return reason;
+ }
+
+ if (oldpass && params.similar_deny &&
+ (is_based(params, u_oldpass, u_newpass, newpass, 0) ||
+ is_based(params, u_oldpass, u_reversed, newpass, 0x100)))
+ return REASON_SIMILAR;
+
+ if (pw &&
+ (is_based(params, u_name, u_newpass, newpass, 0) ||
+ is_based(params, u_name, u_reversed, newpass, 0x100) ||
+ is_based(params, u_gecos, u_newpass, newpass, 0) ||
+ is_based(params, u_gecos, u_reversed, newpass, 0x100) ||
+ is_based(params, u_dir, u_newpass, newpass, 0) ||
+ is_based(params, u_dir, u_reversed, newpass, 0x100)))
+ return REASON_PERSONAL;
+
+ reason = is_word_based(params, u_newpass, newpass, 0);
+ if (!reason)
+ reason = is_word_based(params, u_reversed, newpass, 0x100);
+
+ return reason;
+ }
+
+ function isascii(c){
+ return /^[\x00-\x7F]?$/.test(c);
+ }
+
+ function isdigit(c){
+ return /^\d?$/.test(c);
+ }
+
+ function islower(c){
+ return isalpha(c) && c.toLowerCase() === c;
+ }
+
+ function isupper(c){
+ return isalpha(c) && c.toUpperCase() === c;
+ }
+
+ function isalpha(c){
+ return /^\w?$/.test(c) && c != '_' && /^\D?$/.test(c);
+ }
+
+ function isspace(c){
+ return /^\s?$/.test(c);
+ }
+
+ function strlen(str){
+ var length = str.length, count = 0, ch = 0;
+ for(var i=0; i < length; i++){
+ ch = str.charCodeAt(i);
+ if(ch <= 127){
+ count++;
+ }else if(ch <= 2047){
+ count += 2;
+ }else if(ch <= 65535){
+ count += 3;
+ }else if(ch <= 2097151){
+ count += 4;
+ }else if(ch <= 67108863){
+ count += 5;
+ }else{
+ count += 6;
+ }
+ }
+
+ return count;
+ }
+
+ var REASON_ERROR = "check failed",
+ REASON_SAME = "is the same as the old one",
+ REASON_SIMILAR = "is based on the old one",
+ REASON_SHORT = "too short",
+ REASON_LONG = "too long",
+ REASON_SIMPLESHORT = "not enough different characters or classes for this length",
+ REASON_SIMPLE = "not enough different characters or classes",
+ REASON_PERSONAL = "based on personal login information",
+ REASON_WORD = "based on a dictionary word and not a passphrase",
+ REASON_SEQ = "based on a common sequence of characters and not a passphrase",
+ INT_MAX = 2147483647;
+
+ var params = {
+ min: [INT_MAX, 24, 11, 8, 7],
+ max: 40,
+ passphrase_words: 3,
+ match_length: 4,
+ similar_deny: 1,
+ random_bits: 47,
+ flags: 3,
+ retry: 3
+ }
+
+ function check(newpass, oldpass, login, gecos, pms){
+ return passwdqc_check(pms || params, newpass, oldpass, login ? { pw_name: login, pw_gecos: gecos } : login);
+ }
+
+ exports.check = check;
+
+ return exports;
+}); \ No newline at end of file
diff --git a/skins/standard/admin.css b/skins/standard/admin.css
index 96bc4347c..fd0ff2808 100644
--- a/skins/standard/admin.css
+++ b/skins/standard/admin.css
@@ -337,68 +337,3 @@ input[disabled] {
.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 f505268f5..41c2b09a9 100644
--- a/template/en/default/account/email/confirm-new.html.tmpl
+++ b/template/en/default/account/email/confirm-new.html.tmpl
@@ -26,13 +26,14 @@
javascript_urls = ['js/account.js']
onload = "document.forms['confirm_account_form'].realname.focus();" %]
-[% password_complexity = Param('password_complexity') %]
-
<p>
To create your account, you must enter a password in the form below.
Your email address and Real Name (if provided) will be shown with
changes you make.
</p>
+<p>
+ [% Param('passwdqc_desc') FILTER html_light %]
+</p>
<form id="confirm_account_form" method="post" action="token.cgi">
<input type="hidden" name="t" value="[% token FILTER html %]">
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 68119252e..b17bd510d 100644
--- a/template/en/default/account/password/set-forgotten-password.html.tmpl
+++ b/template/en/default/account/password/set-forgotten-password.html.tmpl
@@ -25,7 +25,10 @@
%]
<p>
- To change your password, enter a new password twice:
+ To change your password, enter a new password twice.
+</p>
+<p>
+ [% Param('passwdqc_desc') FILTER html_light %]
</p>
<div class="flex">
diff --git a/template/en/default/account/prefs/account.html.tmpl b/template/en/default/account/prefs/account.html.tmpl
index c41ea116f..1e7bc25db 100644
--- a/template/en/default/account/prefs/account.html.tmpl
+++ b/template/en/default/account/prefs/account.html.tmpl
@@ -71,6 +71,12 @@
</tr>
[% IF user.authorizer.can_change_password %]
<tr>
+ <td>&nbsp;</td>
+ <td>
+ [% Param('passwdqc_desc') FILTER html_light %]
+ </td>
+ </tr>
+ <tr>
<th align="right">New password:</th>
<td>
<input type="password" name="new_password1" id="new_password1">
diff --git a/template/en/default/account/reset-password.html.tmpl b/template/en/default/account/reset-password.html.tmpl
index ec57f19dd..ca60c5772 100644
--- a/template/en/default/account/reset-password.html.tmpl
+++ b/template/en/default/account/reset-password.html.tmpl
@@ -75,6 +75,10 @@ $(function() {
[% user.password_change_reason || "You are required to update your password." FILTER html %]
</p>
+<p>
+ [% Param('passwdqc_desc') FILTER html_light %]
+</p>
+
<form method="POST" action="reset_password.cgi">
<input type="hidden" name="token" value="[% token FILTER html %]">
<input type="hidden" name="do_save" value="1">
diff --git a/template/en/default/admin/params/auth.html.tmpl b/template/en/default/admin/params/auth.html.tmpl
index e19712351..72fcc7e55 100644
--- a/template/en/default/admin/params/auth.html.tmpl
+++ b/template/en/default/admin/params/auth.html.tmpl
@@ -87,6 +87,10 @@
The size of randomly-generated passphrases in bits (24 to 85).
[% END %]
+[% desc_passwdqc_desc = BLOCK %]
+ This should be a short paragraph describing the password requirements in plain English.
+ Limited HTML allowed.
+[% END %]
[% param_descs = {
auth_env_id => "Environment variable used by external authentication system " _
@@ -197,11 +201,12 @@
"will be permitted to create their own accounts and all accounts " _
"will have to be created by an administrator.",
- passwdqc_min => desc_passwdqc_min,
+ passwdqc_min => desc_passwdqc_min,
passwdqc_max => desc_passwdqc_max
passwdqc_passphrase_words => desc_passwdqc_passphrase_words,
passwdqc_match_length => desc_passwdqc_match_length,
passwdqc_random_bits => desc_random_bits,
+ passwdqc_desc => desc_passwdqc_desc,
password_complexity =>
"Set the complexity required for passwords. In all cases must the passwords " _
diff --git a/template/en/default/global/password-features.html.tmpl b/template/en/default/global/password-features.html.tmpl
index ab7ae1d81..60196568f 100644
--- a/template/en/default/global/password-features.html.tmpl
+++ b/template/en/default/global/password-features.html.tmpl
@@ -6,22 +6,6 @@
# defined by the Mozilla Public License, v. 2.0.
#%]
-<div id="password-features"
- style="display: none"
- class="[% class FILTER html %]"
- data-password-page="[% password_page FILTER html %]"
- data-password-complexity="no_constraints">
- Password must be 8 characters or longer,
- and match at least 3 of the following requirements:
-
- <ul>
- <li id="password-feature-upper">uppercase letters</li>
- <li id="password-feature-lower">lowercase letters</li>
- <li id="password-feature-numbers">numbers</li>
- <li id="password-feature-symbols">symbols</li>
- <li id="password-feature-length12">longer than 12 characters</li>
- </ul>
- <div id="password-msg"></div>
-
- <div id="password-meter-label" style="display: none">Strength: <span id="password-meter" class="meter"></span></div>
-</div>
+<span id="password-features"
+ data-password-page="[% password_page FILTER html %]">
+</span>