Repository: zulip/zulip

Issue:
Set good zxcvbn defaults for production environments
We should add a new server setting controlling how strong passwords are required in Zulip, using zxcvbn's logic.  We can make it a server-level configuration parameter (see http://zulip.readthedocs.io/en/latest/settings.html).  `git grep zxcvbn` should make it easy to find the code needing modification to implement this.

It's important that we do the error messages for too-weak-passwords carefully.


Discussion:
I guess I'm going to work on this one :smiley: 

Resolving patch:
diff --git a/zerver/context_processors.py b/zerver/context_processors.py
--- a/zerver/context_processors.py
+++ b/zerver/context_processors.py
@@ -61,6 +61,8 @@ def add_settings(request):
         'development_environment':   settings.DEVELOPMENT,
         'support_email':             settings.ZULIP_ADMINISTRATOR,
         'find_team_link_disabled':   settings.FIND_TEAM_LINK_DISABLED,
+        'password_min_length':       settings.PASSWORD_MIN_LENGTH,
+        'password_min_quality':      settings.PASSWORD_MIN_ZXCVBN_QUALITY,
     }
 
 
diff --git a/zproject/prod_settings_template.py b/zproject/prod_settings_template.py
--- a/zproject/prod_settings_template.py
+++ b/zproject/prod_settings_template.py
@@ -121,6 +121,10 @@
 # Session cookie expiry in seconds after the last page load
 SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2 # 2 weeks
 
+# Controls password strength requirements
+PASSWORD_MIN_LENGTH = 6
+PASSWORD_MIN_ZXCVBN_QUALITY = 0.4 # 0 to disable
+
 # Controls whether or not there is a feedback button in the UI.
 ENABLE_FEEDBACK = False
 
diff --git a/zproject/settings.py b/zproject/settings.py
--- a/zproject/settings.py
+++ b/zproject/settings.py
@@ -191,6 +191,8 @@ def get_secret(key):
                     'POST_MIGRATION_CACHE_FLUSHING': False,
                     'ENABLE_FILE_LINKS': False,
                     'USE_WEBSOCKETS': True,
+                    'PASSWORD_MIN_LENGTH': 6,
+                    'PASSWORD_MIN_ZXCVBN_QUALITY': 0.4,
                     }
 
 for setting_name, setting_val in six.iteritems(DEFAULT_SETTINGS):


Test patch:
diff --git a/frontend_tests/casper_tests/00-realm-creation.js b/frontend_tests/casper_tests/00-realm-creation.js
--- a/frontend_tests/casper_tests/00-realm-creation.js
+++ b/frontend_tests/casper_tests/00-realm-creation.js
@@ -63,7 +63,7 @@ casper.then(function () {
             full_name: 'Alice',
             realm_name: organization_name,
             realm_subdomain: subdomain,
-            password: 'password',
+            password: 'passwordwhichisreallyreallyreallycomplexandnotguessable',
             terms: true,
         }, true);
     });
diff --git a/frontend_tests/node_tests/password.js b/frontend_tests/node_tests/password.js
new file mode 100644
--- /dev/null
+++ b/frontend_tests/node_tests/password.js
@@ -0,0 +1,70 @@
+add_dependencies({
+    zxcvbn: 'node_modules/zxcvbn/dist/zxcvbn.js',
+});
+
+var common = require("js/common.js");
+
+(function test_basics() {
+    var accepted;
+    var password;
+
+    var bar = (function () {
+        var self = {};
+
+        self.width = function (width) {
+            self.w = width;
+            return self;
+        };
+
+        self.removeClass = function (arg) {
+            assert.equal(arg, 'bar-success bar-danger');
+            return self;
+        };
+
+        self.addClass = function (arg) {
+            self.added_class = arg;
+            return self;
+        };
+
+        return self;
+    }());
+
+    password = 'plain';
+    accepted = common.password_quality(password, bar);
+    assert(!accepted);
+    assert.equal(bar.w, '10.654507557627486%');
+    assert.equal(bar.added_class, 'bar-danger');
+
+    password = 'z!X4@S_&';
+    accepted = common.password_quality(password, bar);
+    assert(accepted);
+    assert.equal(bar.w, '47.679074269445294%');
+    assert.equal(bar.added_class, 'bar-success');
+
+    function password_field(min_length, min_quality) {
+        var self = {};
+
+        self.data = function (field) {
+            if (field === 'minLength') {
+                return min_length;
+            } else if (field === 'minQuality') {
+                return min_quality;
+            }
+        };
+
+        return self;
+    }
+
+    password = 'z!X4@S_&';
+    accepted = common.password_quality(password, bar, password_field(10, 0.10));
+    assert(!accepted);
+    assert.equal(bar.w, '39.7%');
+    assert.equal(bar.added_class, 'bar-danger');
+
+
+    password = 'foo';
+    accepted = common.password_quality(password, bar, password_field(2, 0.001));
+    assert(accepted);
+    assert.equal(bar.w, '10.390277164940581%');
+    assert.equal(bar.added_class, 'bar-success');
+}());