diff --git a/bower.json b/bower.json index e36409780a..71a6b51671 100644 --- a/bower.json +++ b/bower.json @@ -12,7 +12,8 @@ "angular-ui-utils": "bower", "angular-ui-router": "~0.2", "angular-file-upload": "1.1.5", - "angular-messages": "1.3.17" + "angular-messages": "1.3.17", + "owasp-password-strength-test": "~1.3.0" }, "resolutions": { "angular": "~1.3" diff --git a/config/assets/default.js b/config/assets/default.js index 85917d595d..c64fdb7ab8 100644 --- a/config/assets/default.js +++ b/config/assets/default.js @@ -15,7 +15,8 @@ module.exports = { 'public/lib/angular-ui-router/release/angular-ui-router.js', 'public/lib/angular-ui-utils/ui-utils.js', 'public/lib/angular-bootstrap/ui-bootstrap-tpls.js', - 'public/lib/angular-file-upload/angular-file-upload.js' + 'public/lib/angular-file-upload/angular-file-upload.js', + 'public/lib/owasp-password-strength-test/owasp-password-strength-test.js' ], tests: ['public/lib/angular-mocks/angular-mocks.js'] }, diff --git a/modules/articles/tests/server/article.server.model.tests.js b/modules/articles/tests/server/article.server.model.tests.js index f45d154c79..aa45237c13 100644 --- a/modules/articles/tests/server/article.server.model.tests.js +++ b/modules/articles/tests/server/article.server.model.tests.js @@ -24,7 +24,7 @@ describe('Article Model Unit Tests:', function () { displayName: 'Full Name', email: 'test@test.com', username: 'username', - password: 'password' + password: 'M3@n.jsI$Aw3$0m3' }); user.save(function () { diff --git a/modules/articles/tests/server/article.server.routes.tests.js b/modules/articles/tests/server/article.server.routes.tests.js index ef0f1ac781..87854620ad 100644 --- a/modules/articles/tests/server/article.server.routes.tests.js +++ b/modules/articles/tests/server/article.server.routes.tests.js @@ -29,7 +29,7 @@ describe('Article CRUD tests', function () { // Create user credentials credentials = { username: 'username', - password: 'password' + password: 'M3@n.jsI$Aw3$0m3' }; // Create a new user diff --git a/modules/users/client/controllers/authentication.client.controller.js b/modules/users/client/controllers/authentication.client.controller.js index f5f256e6c6..47dd234491 100644 --- a/modules/users/client/controllers/authentication.client.controller.js +++ b/modules/users/client/controllers/authentication.client.controller.js @@ -1,8 +1,9 @@ 'use strict'; -angular.module('users').controller('AuthenticationController', ['$scope', '$state', '$http', '$location', '$window', 'Authentication', - function ($scope, $state, $http, $location, $window, Authentication) { +angular.module('users').controller('AuthenticationController', ['$scope', '$state', '$http', '$location', '$window', 'Authentication', 'PasswordValidator', + function ($scope, $state, $http, $location, $window, Authentication, PasswordValidator) { $scope.authentication = Authentication; + $scope.popoverMsg = PasswordValidator.getPopoverMsg(); // Get an eventual error defined in the URL query string: $scope.error = $location.search().err; diff --git a/modules/users/client/controllers/password.client.controller.js b/modules/users/client/controllers/password.client.controller.js index e24c556296..35b4aae18a 100644 --- a/modules/users/client/controllers/password.client.controller.js +++ b/modules/users/client/controllers/password.client.controller.js @@ -1,8 +1,9 @@ 'use strict'; -angular.module('users').controller('PasswordController', ['$scope', '$stateParams', '$http', '$location', 'Authentication', - function ($scope, $stateParams, $http, $location, Authentication) { +angular.module('users').controller('PasswordController', ['$scope', '$stateParams', '$http', '$location', 'Authentication', 'PasswordValidator', + function ($scope, $stateParams, $http, $location, Authentication, PasswordValidator) { $scope.authentication = Authentication; + $scope.popoverMsg = PasswordValidator.getPopoverMsg(); //If user is signed in then redirect back home if ($scope.authentication.user) { @@ -10,9 +11,15 @@ angular.module('users').controller('PasswordController', ['$scope', '$stateParam } // Submit forgotten password account id - $scope.askForPasswordReset = function () { + $scope.askForPasswordReset = function (isValid) { $scope.success = $scope.error = null; + if (!isValid) { + $scope.$broadcast('show-errors-check-validity', 'forgotPasswordForm'); + + return false; + } + $http.post('/api/auth/forgot', $scope.credentials).success(function (response) { // Show user success message and clear form $scope.credentials = null; @@ -26,9 +33,15 @@ angular.module('users').controller('PasswordController', ['$scope', '$stateParam }; // Change user password - $scope.resetUserPassword = function () { + $scope.resetUserPassword = function (isValid) { $scope.success = $scope.error = null; + if (!isValid) { + $scope.$broadcast('show-errors-check-validity', 'resetPasswordForm'); + + return false; + } + $http.post('/api/auth/reset/' + $stateParams.token, $scope.passwordDetails).success(function (response) { // If successful show success message and clear form $scope.passwordDetails = null; diff --git a/modules/users/client/controllers/settings/change-password.client.controller.js b/modules/users/client/controllers/settings/change-password.client.controller.js index 5e32e11d9b..ff8c4179b1 100644 --- a/modules/users/client/controllers/settings/change-password.client.controller.js +++ b/modules/users/client/controllers/settings/change-password.client.controller.js @@ -1,8 +1,9 @@ 'use strict'; -angular.module('users').controller('ChangePasswordController', ['$scope', '$http', 'Authentication', - function ($scope, $http, Authentication) { +angular.module('users').controller('ChangePasswordController', ['$scope', '$http', 'Authentication', 'PasswordValidator', + function ($scope, $http, Authentication, PasswordValidator) { $scope.user = Authentication.user; + $scope.popoverMsg = PasswordValidator.getPopoverMsg(); // Change user password $scope.changeUserPassword = function (isValid) { diff --git a/modules/users/client/directives/password-validator.client.directive.js b/modules/users/client/directives/password-validator.client.directive.js new file mode 100644 index 0000000000..da9fe08370 --- /dev/null +++ b/modules/users/client/directives/password-validator.client.directive.js @@ -0,0 +1,42 @@ +'use strict'; + +angular.module('users') + .directive('passwordValidator', ['PasswordValidator', function(PasswordValidator) { + return { + require: 'ngModel', + link: function(scope, element, attrs, modelCtrl) { + modelCtrl.$parsers.unshift(function (password) { + var result = PasswordValidator.getResult(password); + var strengthIdx = 0; + + // Strength Meter - visual indicator for users + var strengthMeter = [ + { color: "danger", progress: "20" }, + { color: "warning", progress: "40"}, + { color: "info", progress: "60"}, + { color: "primary", progress: "80"}, + { color: "success", progress: "100"} + ]; + var strengthMax = strengthMeter.length; + + if (result.errors.length < strengthMeter.length) { + strengthIdx = strengthMeter.length - result.errors.length - 1; + } + + scope.strengthColor = strengthMeter[strengthIdx].color; + scope.strengthProgress = strengthMeter[strengthIdx].progress; + + if (result.errors.length) { + scope.popoverMsg = PasswordValidator.getPopoverMsg(); + scope.passwordErrors = result.errors; + modelCtrl.$setValidity('strength', false); + return undefined; + } else { + scope.popoverMsg = ''; + modelCtrl.$setValidity('strength', true); + return password; + } + }); + } + }; +}]); diff --git a/modules/users/client/directives/password-verify.client.directive.js b/modules/users/client/directives/password-verify.client.directive.js new file mode 100644 index 0000000000..63e6c13a84 --- /dev/null +++ b/modules/users/client/directives/password-verify.client.directive.js @@ -0,0 +1,33 @@ +'use strict'; + +angular.module('users') + .directive("passwordVerify", function() { + return { + require: "ngModel", + scope: { + passwordVerify: '=' + }, + link: function(scope, element, attrs, modelCtrl) { + scope.$watch(function() { + var combined; + if (scope.passwordVerify || modelCtrl.$viewValue) { + combined = scope.passwordVerify + '_' + modelCtrl.$viewValue; + } + return combined; + }, function(value) { + if (value) { + modelCtrl.$parsers.unshift(function(viewValue) { + var origin = scope.passwordVerify; + if (origin !== viewValue) { + modelCtrl.$setValidity("passwordVerify", false); + return undefined; + } else { + modelCtrl.$setValidity("passwordVerify", true); + return viewValue; + } + }); + } + }); + } + }; +}); diff --git a/modules/users/client/services/password-validator.client.service.js b/modules/users/client/services/password-validator.client.service.js new file mode 100644 index 0000000000..0c10f554e2 --- /dev/null +++ b/modules/users/client/services/password-validator.client.service.js @@ -0,0 +1,19 @@ +'use strict'; + +// PasswordValidator service used for testing the password strength +angular.module('users').factory('PasswordValidator', ['$window', + function ($window) { + var owaspPasswordStrengthTest = $window.owaspPasswordStrengthTest; + + return { + getResult: function (password) { + var result = owaspPasswordStrengthTest.test(password); + return result; + }, + getPopoverMsg: function () { + var popoverMsg = "Please enter a passphrase or password with greater than 10 characters, numbers, lowercase, upppercase, and special characters."; + return popoverMsg; + } + }; + } +]); diff --git a/modules/users/client/views/authentication/signup.client.view.html b/modules/users/client/views/authentication/signup.client.view.html index 60c42b9ee3..65fbb4bb5c 100644 --- a/modules/users/client/views/authentication/signup.client.view.html +++ b/modules/users/client/views/authentication/signup.client.view.html @@ -34,14 +34,20 @@

Or sign up using your email

- +

Password is required.

-

Password is too short.

+
+

{{passwordError}}

+
+
+ + {{strengthProgress}}% +
- +   or  Sign in
diff --git a/modules/users/client/views/password/forgot-password.client.view.html b/modules/users/client/views/password/forgot-password.client.view.html index 0ca779335c..5e0ac563ad 100644 --- a/modules/users/client/views/password/forgot-password.client.view.html +++ b/modules/users/client/views/password/forgot-password.client.view.html @@ -2,19 +2,22 @@

Restore your password

Enter your account username.

-
+
-
- +
+ +
+

Enter a username.

+
- {{error}} +
- {{success}} +
diff --git a/modules/users/client/views/password/reset-password.client.view.html b/modules/users/client/views/password/reset-password.client.view.html index 07f1a0b5f0..f0189ad681 100644 --- a/modules/users/client/views/password/reset-password.client.view.html +++ b/modules/users/client/views/password/reset-password.client.view.html @@ -1,24 +1,38 @@

Reset your password

-
-
- +

Enter a new password.

+
+

{{passwordError}}

+
- +

Verify your new password.

+

Passwords do not match.

+
+ + {{strengthProgress}}% +
- +
Password Changed Successfully diff --git a/modules/users/client/views/settings/edit-profile.client.view.html b/modules/users/client/views/settings/edit-profile.client.view.html index 8fe25b605d..646fe2cacf 100644 --- a/modules/users/client/views/settings/edit-profile.client.view.html +++ b/modules/users/client/views/settings/edit-profile.client.view.html @@ -32,7 +32,7 @@
- +
Profile Saved Successfully diff --git a/modules/users/server/models/user.server.model.js b/modules/users/server/models/user.server.model.js index 87fd11d9d2..8f102517aa 100644 --- a/modules/users/server/models/user.server.model.js +++ b/modules/users/server/models/user.server.model.js @@ -6,7 +6,8 @@ var mongoose = require('mongoose'), Schema = mongoose.Schema, crypto = require('crypto'), - validator = require('validator'); + validator = require('validator'), + owasp = require('owasp-password-strength-test'); /** * A Validation function for local strategy properties @@ -15,13 +16,6 @@ var validateLocalStrategyProperty = function (property) { return ((this.provider !== 'local' && !this.updated) || property.length); }; -/** - * A Validation function for local strategy password - */ -var validateLocalStrategyPassword = function (password) { - return (this.provider !== 'local' || validator.isLength(password, 6)); -}; - /** * A Validation function for local strategy email */ @@ -66,8 +60,7 @@ var UserSchema = new Schema({ }, password: { type: String, - default: '', - validate: [validateLocalStrategyPassword, 'Password should be longer'] + default: '' }, salt: { type: String @@ -110,7 +103,7 @@ var UserSchema = new Schema({ * Hook a pre save method to hash the password */ UserSchema.pre('save', function (next) { - if (this.password && this.isModified('password') && this.password.length >= 6) { + if (this.password && this.isModified('password')) { this.salt = crypto.randomBytes(16).toString('base64'); this.password = this.hashPassword(this.password); } @@ -118,6 +111,21 @@ UserSchema.pre('save', function (next) { next(); }); +/** + * Hook a pre validate method to test the local password + */ +UserSchema.pre('validate', function (next) { + if (this.provider === 'local' && this.password) { + var result = owasp.test(this.password); + if (result.errors.length) { + var error = result.errors.join(' '); + this.invalidate('password', error); + } + } + + next(); +}); + /** * Create instance method for hashing a password */ diff --git a/modules/users/tests/client/password.client.controller.tests.js b/modules/users/tests/client/password.client.controller.tests.js index 86be25f575..2b1f4a6718 100644 --- a/modules/users/tests/client/password.client.controller.tests.js +++ b/modules/users/tests/client/password.client.controller.tests.js @@ -82,7 +82,7 @@ describe('askForPasswordReset', function() { var credentials = { username: 'test', - password: 'test' + password: 'P@ssw0rd!!' }; beforeEach(function() { scope.credentials = credentials; @@ -91,7 +91,7 @@ it('should clear scope.success and scope.error', function() { scope.success = 'test'; scope.error = 'test'; - scope.askForPasswordReset(); + scope.askForPasswordReset(true); expect(scope.success).toBeNull(); expect(scope.error).toBeNull(); @@ -104,7 +104,7 @@ 'message': errorMessage }); - scope.askForPasswordReset(); + scope.askForPasswordReset(true); $httpBackend.flush(); }); @@ -124,7 +124,7 @@ 'message': successMessage }); - scope.askForPasswordReset(); + scope.askForPasswordReset(true); $httpBackend.flush(); }); @@ -151,7 +151,7 @@ it('should clear scope.success and scope.error', function() { scope.success = 'test'; scope.error = 'test'; - scope.resetUserPassword(); + scope.resetUserPassword(true); expect(scope.success).toBeNull(); expect(scope.error).toBeNull(); @@ -163,7 +163,7 @@ 'message': errorMessage }); - scope.resetUserPassword(); + scope.resetUserPassword(true); $httpBackend.flush(); expect(scope.error).toBe(errorMessage); @@ -176,7 +176,7 @@ beforeEach(function() { $httpBackend.when('POST', '/api/auth/reset/' + token, passwordDetails).respond(user); - scope.resetUserPassword(); + scope.resetUserPassword(true); $httpBackend.flush(); }); diff --git a/modules/users/tests/server/user.server.model.tests.js b/modules/users/tests/server/user.server.model.tests.js index e40e56d0be..ad8e6f401b 100644 --- a/modules/users/tests/server/user.server.model.tests.js +++ b/modules/users/tests/server/user.server.model.tests.js @@ -23,7 +23,7 @@ describe('User Model Unit Tests:', function () { displayName: 'Full Name', email: 'test@test.com', username: 'username', - password: 'password', + password: 'M3@n.jsI$Aw3$0m3', provider: 'local' }; // user2 is a clone of user1 @@ -34,10 +34,9 @@ describe('User Model Unit Tests:', function () { displayName: 'Full Different Name', email: 'test3@test.com', username: 'different_username', - password: 'different_password', + password: 'Different_Password1!', provider: 'local' }; - }); describe('Method Save', function () { @@ -50,7 +49,7 @@ describe('User Model Unit Tests:', function () { it('should be able to save without problems', function (done) { var _user1 = new User(user1); - + _user1.save(function (err) { should.not.exist(err); _user1.remove(function (err) { @@ -63,7 +62,7 @@ describe('User Model Unit Tests:', function () { it('should fail to save an existing user again', function (done) { var _user1 = new User(user1); var _user2 = new User(user2); - + _user1.save(function () { _user2.save(function (err) { should.exist(err); @@ -75,7 +74,7 @@ describe('User Model Unit Tests:', function () { }); }); - it('should be able to show an error when try to save without first name', function (done) { + it('should be able to show an error when trying to save without first name', function (done) { var _user1 = new User(user1); _user1.firstName = ''; @@ -204,9 +203,9 @@ describe('User Model Unit Tests:', function () { }); }); - it('should not save the password in plain text (6 char password)', function (done) { + it('should not save the passphrase in plain text', function (done) { var _user1 = new User(user1); - _user1.password = '123456'; + _user1.password = 'Open-Source Full-Stack Solution for MEAN'; var passwordBeforeSave = _user1.password; _user1.save(function (err) { should.not.exist(err); @@ -217,9 +216,96 @@ describe('User Model Unit Tests:', function () { }); }); }); - }); + describe("User Password Validation Tests", function() { + it('should validate when the password strength passes - "P@$$w0rd!!"', function () { + var _user1 = new User(user1); + _user1.password = 'P@$$w0rd!!'; + + _user1.validate(function (err) { + should.not.exist(err); + }); + }); + + it('should validate when the password is undefined', function () { + var _user1 = new User(user1); + _user1.password = undefined; + + _user1.validate(function (err) { + should.not.exist(err); + }); + }); + + it('should validate when the passphrase strength passes - "Open-Source Full-Stack Solution For MEAN Applications"', function () { + var _user1 = new User(user1); + _user1.password = 'Open-Source Full-Stack Solution For MEAN Applications'; + + _user1.validate(function (err) { + should.not.exist(err); + }); + }); + + it('should not allow a less than 10 characters long - "P@$$w0rd!"', function (done) { + var _user1 = new User(user1); + _user1.password = 'P@$$w0rd!'; + + _user1.validate(function (err) { + err.errors.password.message.should.equal("The password must be at least 10 characters long."); + done(); + }); + }); + + it('should not allow a greater than 128 characters long.', function (done) { + var _user1 = new User(user1); + _user1.password = ')!/uLT="lh&:`6X!]|15o!$!TJf,.13l?vG].-j],lFPe/QhwN#{Z<[*1nX@n1^?WW-%_.*D)m$toB+N7z}kcN#B_d(f41h%w@0F!]igtSQ1gl~6sEV&r~}~1ub>If1c+'; + + _user1.validate(function (err) { + err.errors.password.message.should.equal("The password must be fewer than 128 characters."); + done(); + }); + }); + + it('should not allow more than 3 or more repeating characters - "P@$$w0rd!!!"', function (done) { + var _user1 = new User(user1); + _user1.password = 'P@$$w0rd!!!'; + + _user1.validate(function (err) { + err.errors.password.message.should.equal("The password may not contain sequences of three or more repeated characters."); + done(); + }); + }); + + it('should not allow a password with no uppercase letters - "p@$$w0rd!!"', function (done) { + var _user1 = new User(user1); + _user1.password = 'p@$$w0rd!!'; + + _user1.validate(function (err) { + err.errors.password.message.should.equal("The password must contain at least one uppercase letter."); + done(); + }); + }); + + it('should not allow a password with less than one number - "P@$$word!!"', function (done) { + var _user1 = new User(user1); + _user1.password = 'P@$$word!!'; + + _user1.validate(function (err) { + err.errors.password.message.should.equal("The password must contain at least one number."); + done(); + }); + }); + + it('should not allow a password with less than one special character - "Passw0rdss"', function (done) { + var _user1 = new User(user1); + _user1.password = 'Passw0rdss'; + + _user1.validate(function (err) { + err.errors.password.message.should.equal("The password must contain at least one special character."); + done(); + }); + }); + }); describe("User E-mail Validation Tests", function() { it('should not allow invalid email address - "123"', function (done) { @@ -257,7 +343,7 @@ describe('User Model Unit Tests:', function () { done(); } }); - + }); it('should not allow invalid email address - "123.com"', function (done) { @@ -276,7 +362,7 @@ describe('User Model Unit Tests:', function () { done(); } }); - + }); it('should not allow invalid email address - "@123.com"', function (done) { @@ -295,7 +381,6 @@ describe('User Model Unit Tests:', function () { done(); } }); - }); it('should not allow invalid email address - "abc@abc@abc.com"', function (done) { @@ -314,7 +399,6 @@ describe('User Model Unit Tests:', function () { done(); } }); - }); it('should not allow invalid characters in email address - "abc~@#$%^&*()ef=@abc.com"', function (done) { @@ -333,7 +417,6 @@ describe('User Model Unit Tests:', function () { done(); } }); - }); it('should not allow space characters in email address - "abc def@abc.com"', function (done) { @@ -352,7 +435,6 @@ describe('User Model Unit Tests:', function () { done(); } }); - }); it('should not allow doudble quote characters in email address - "abc\"def@abc.com"', function (done) { @@ -371,7 +453,6 @@ describe('User Model Unit Tests:', function () { done(); } }); - }); it('should not allow double dotted characters in email address - "abcdef@abc..com"', function (done) { @@ -390,7 +471,6 @@ describe('User Model Unit Tests:', function () { done(); } }); - }); it('should allow single quote characters in email address - "abc\'def@abc.com"', function (done) { @@ -427,7 +507,6 @@ describe('User Model Unit Tests:', function () { done(); } }); - }); it('should allow valid email address - "abc+def@abc.com"', function (done) { @@ -446,7 +525,6 @@ describe('User Model Unit Tests:', function () { done(); } }); - }); it('should allow valid email address - "abc.def@abc.com"', function (done) { @@ -465,7 +543,6 @@ describe('User Model Unit Tests:', function () { done(); } }); - }); it('should allow valid email address - "abc.def@abc.def.com"', function (done) { @@ -484,7 +561,6 @@ describe('User Model Unit Tests:', function () { done(); } }); - }); it('should allow valid email address - "abc-def@abc.com"', function (done) { @@ -502,7 +578,6 @@ describe('User Model Unit Tests:', function () { done(); } }); - }); }); diff --git a/modules/users/tests/server/user.server.routes.tests.js b/modules/users/tests/server/user.server.routes.tests.js index 1bce3ee03d..c0686599ff 100644 --- a/modules/users/tests/server/user.server.routes.tests.js +++ b/modules/users/tests/server/user.server.routes.tests.js @@ -28,7 +28,7 @@ describe('User CRUD tests', function () { // Create user credentials credentials = { username: 'username', - password: 'password' + password: 'M3@n.jsI$Aw3$0m3' }; // Create a new user diff --git a/package.json b/package.json index 967ad70f5a..cf9293ae3c 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "multer": "0.1.8", "node-pre-gyp": "0.6.4", "nodemailer": "^1.4.0", + "owasp-password-strength-test": "^1.3.0", "passport": "~0.2.2", "passport-facebook": "^2.0.0", "passport-github": "~0.1.5",