From eb32e1771f2731ce638a85b05a4bbc42b33e0167 Mon Sep 17 00:00:00 2001 From: Aysel Afsar Date: Sun, 14 Feb 2016 17:03:00 -0500 Subject: [PATCH] LT-97: Add requireRegexValidation and requireStrongPasswords properties under passwordOptions which is under passwordOptions. - requireRegexValidation toggles whether or not password validation is controlled by regular expression - requireStrongPasswords toggles whether password validation is controlled by zxcvbn package LT-104: User account shall be locked after 5 failed attempts - "failedAttemptsLimit" property which is under passwordOptions in ActiveEntry configuration object sets number of failed attempts count to lock user account, it is set 5 as default LT-99: Passwords shall use password history of 6 - "passwordHistoryCount" property which is under passwordOptions in ActiveEntry configuration object sets count of last passwords that is not used to reset password - Show error messages in changePassword template - Make signIn button disabled if inputs are not validated --- LesionTracker/activeEntry.js | 7 +- .../lesionTrackerLayout.html | 4 +- .../lesionTrackerLayout.js | 3 + .../lesionTrackerLayout.styl | 17 +++++ .../changePassword/changePassword.html | 8 +++ .../changePassword/changePassword.js | 50 +++++++++++++-- .../components/entrySignIn/entrySignIn.html | 2 +- .../components/entrySignIn/entrySignIn.js | 29 ++++++--- .../components/entrySignIn/entrySignIn.less | 16 ++++- .../components/entrySignUp/entrySignUp.html | 7 -- Packages/active-entry/lib/ActiveEntry.js | 59 ++++++++++++++--- .../active-entry/lib/hashCodeGenerator.js | 10 +++ Packages/active-entry/package.js | 3 +- Packages/active-entry/server/methods.js | 64 +++++++++++++++++++ Packages/meteor-stale-session/client.js | 20 ++---- 15 files changed, 250 insertions(+), 49 deletions(-) create mode 100644 Packages/active-entry/lib/hashCodeGenerator.js diff --git a/LesionTracker/activeEntry.js b/LesionTracker/activeEntry.js index 48cb0cafc..cdde9557d 100644 --- a/LesionTracker/activeEntry.js +++ b/LesionTracker/activeEntry.js @@ -15,10 +15,13 @@ if (Meteor.isClient){ primary: "" }, passwordOptions: { - showPasswordStrengthIndicator: true, - requireRegexValidation: true + showPasswordStrengthIndicator: false, + requireRegexValidation: true, //requireStrongPasswords: false + passwordHistoryCount: 6, + failedAttemptsLimit: 5 } + }); } diff --git a/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.html b/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.html index f15126f79..a3b7cc309 100644 --- a/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.html +++ b/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.html @@ -10,7 +10,9 @@ diff --git a/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.js b/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.js index a750d7f44..ce9797a2b 100644 --- a/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.js +++ b/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.js @@ -3,6 +3,9 @@ Template.layoutLesionTracker.events({ Meteor.logout(function(){ Router.go('/entrySignIn'); }); + }, + 'click #changePassword': function() { + Router.go('/changePassword'); } }); diff --git a/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.styl b/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.styl index c21a9d6d6..1ec80b232 100644 --- a/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.styl +++ b/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.styl @@ -11,9 +11,26 @@ position: relative float: right + .nav-pills > li > a:hover, .nav .open > a, .nav .open > a:hover, .nav .open > a:focus + background-color: #eee + color: black + + .dropdown-menu > li > a:hover + background-color: #eee + + .dropdown-menu > li > a >i + margin-right: 5px + .dropdown-toggle color: #C1C1C1 + .divider + height: 2px + margin: 9px 2px + overflow: hidden + background-color: #e5e5e5 + border-bottom: 1px solid #fff + .navbar-brand display: inline-block padding: 0 diff --git a/Packages/active-entry/components/changePassword/changePassword.html b/Packages/active-entry/components/changePassword/changePassword.html index add5897aa..f6c4c805f 100755 --- a/Packages/active-entry/components/changePassword/changePassword.html +++ b/Packages/active-entry/components/changePassword/changePassword.html @@ -8,6 +8,14 @@
{{getChangePasswordMessage}}
+ {{#if entryErrorMessages}} +
+ {{#each entryErrorMessages}} +
{{this}}
+ {{/each}} +
+ {{/if}} +
diff --git a/Packages/active-entry/components/changePassword/changePassword.js b/Packages/active-entry/components/changePassword/changePassword.js index cf0c56a4d..c903840ce 100755 --- a/Packages/active-entry/components/changePassword/changePassword.js +++ b/Packages/active-entry/components/changePassword/changePassword.js @@ -44,9 +44,20 @@ Template.changePassword.helpers({ } else { return "border: 1px solid gray"; } + }, + + entryErrorMessages: function() { + var errorMessages = []; + Object.keys(ActiveEntry.errorMessages.all()).forEach(function(key) { + if ((key === "password" || key === "confirm") && ActiveEntry.errorMessages.get(key)) { + errorMessages.push(ActiveEntry.errorMessages.get(key)); + } + }); + return errorMessages; } }); + Template.changePassword.events({ 'change, keyup #changePasswordPagePasswordInput': function (event, template) { var password = $('[name="password"]').val(); @@ -73,12 +84,37 @@ Template.changePassword.events({ ActiveEntry.verifyConfirmPassword(password, confirmPassword); ActiveEntry.errorMessages.set('changePasswordError', null); - Accounts.changePassword(oldPassword, confirmPassword, function(error) { - if (error) { - console.warn(error); - return; - } - console.log('Password changed!'); - }); + + if (ActiveEntry.successMessages.get('password') && ActiveEntry.successMessages.get('confirm') && oldPassword) { + Meteor.call("checkPasswordExistence", new String(password).hashCode(), function(error, result) { + if (error) { + console.warn(error.message); + ActiveEntry.errorMessages.set('changePasswordError', error.message); + + } else { + if (result) { + ActiveEntry.errorMessages.set('changePasswordError', 'Password is used before. Please change your new password.'); + } else { + ActiveEntry.errorMessages.set('changePasswordError', null); + + // If password is not found in password history, change the password + Accounts.changePassword(oldPassword, confirmPassword, function(error) { + if (error) { + console.warn(error); + ActiveEntry.errorMessages.set('changePasswordError', error.message); + } else { + // Save the new password + ActiveEntry.insertHashedPassword(confirmPassword); + // Logout + ActiveEntry.signOut(); + // Go to signIn page for new entry + Router.go('/entrySignIn'); + } + }); + } + } + }); + } + } }); diff --git a/Packages/active-entry/components/entrySignIn/entrySignIn.html b/Packages/active-entry/components/entrySignIn/entrySignIn.html index b3202cae5..712801ea0 100755 --- a/Packages/active-entry/components/entrySignIn/entrySignIn.html +++ b/Packages/active-entry/components/entrySignIn/entrySignIn.html @@ -30,7 +30,7 @@

- +

diff --git a/Packages/active-entry/components/entrySignIn/entrySignIn.js b/Packages/active-entry/components/entrySignIn/entrySignIn.js index c6420a56f..f6af2401b 100755 --- a/Packages/active-entry/components/entrySignIn/entrySignIn.js +++ b/Packages/active-entry/components/entrySignIn/entrySignIn.js @@ -65,8 +65,8 @@ Template.entrySignIn.helpers({ return "border: 1px solid gray"; } } -}); +}); //================================================================================================== // COMPONENT OUTPUTS @@ -90,24 +90,28 @@ Template.entrySignIn.events({ ActiveEntry.verifyEmail(email); ActiveEntry.errorMessages.set('signInError', null); + setSignInButtonStyling(); }, 'change input[name="email"]': function (event, template) { var email = $('input[name="email"]').val(); ActiveEntry.verifyEmail(email); ActiveEntry.errorMessages.set('signInError', null); + setSignInButtonStyling(); }, 'keyup #signInPagePasswordInput': function (event, template) { var password = $('input[name="password"]').val(); ActiveEntry.verifyPassword(password); ActiveEntry.errorMessages.set('signInError', null); + setSignInButtonStyling(); }, 'change #signInPagePasswordInput': function (event, template) { var password = $('input[name="password"]').val(); ActiveEntry.verifyPassword(password); ActiveEntry.errorMessages.set('signInError', null); + setSignInButtonStyling(); }, // 'submit': function (event, template) { // event.preventDefault(); @@ -128,13 +132,7 @@ Template.entrySignIn.events({ }, 'keyup #entrySignIn': function(event, template) { if(event.keyCode == 13) { - ActiveEntry.verifyEmail($("#signInPageEmailInput").val()); - - if (!ActiveEntry.errorMessages.get('signInError') && - ActiveEntry.successMessages.get('email') && - $("#signInPagePasswordInput").val()) { - $("#signInToAppButton").click(); - } + $("#signInToAppButton").click(); } } }); @@ -142,3 +140,18 @@ Template.entrySignIn.events({ //================================================================================================== + +// Sets SignInButton Styling according to email and password fields +function setSignInButtonStyling() { + var signInToAppButton = $("#signInToAppButton"); + if ($("#signInPagePasswordInput").val() && ActiveEntry.successMessages.get('email')) { + // Set button as enable + signInToAppButton.removeClass("disabledButton"); + signInToAppButton.attr("disabled", false); + } else { + signInToAppButton.addClass("disabledButton"); + signInToAppButton.attr("disabled", true); + + + } +} \ No newline at end of file diff --git a/Packages/active-entry/components/entrySignIn/entrySignIn.less b/Packages/active-entry/components/entrySignIn/entrySignIn.less index 5b3b93e34..4cc2f5fc3 100755 --- a/Packages/active-entry/components/entrySignIn/entrySignIn.less +++ b/Packages/active-entry/components/entrySignIn/entrySignIn.less @@ -14,4 +14,18 @@ color: red; font-weight: bold; } -} + + .disabledButton { + border: none; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + filter: alpha(opacity=40); + -khtml-opacity: 0.40; + -moz-opacity: 0.40; + opacity: 0.40; + cursor: not-allowed; + box-shadow: none; + } + + +} \ No newline at end of file diff --git a/Packages/active-entry/components/entrySignUp/entrySignUp.html b/Packages/active-entry/components/entrySignUp/entrySignUp.html index b84c44103..05a15a177 100755 --- a/Packages/active-entry/components/entrySignUp/entrySignUp.html +++ b/Packages/active-entry/components/entrySignUp/entrySignUp.html @@ -11,13 +11,6 @@
{{getSignUpMessage}}
- {{#if entryErrorMessages}}
diff --git a/Packages/active-entry/lib/ActiveEntry.js b/Packages/active-entry/lib/ActiveEntry.js index 83710f7d9..064061c70 100755 --- a/Packages/active-entry/lib/ActiveEntry.js +++ b/Packages/active-entry/lib/ActiveEntry.js @@ -28,6 +28,7 @@ if (Meteor.isClient) { requireRegexValidation: true //requireStrongPasswords: false } + }); } @@ -62,7 +63,6 @@ ActiveEntry.verifyPassword = function (password) { ActiveEntry.errorMessages.set('password', null); ActiveEntry.successMessages.set('password', 'Password present'); } - }; ActiveEntry.verifyConfirmPassword = function (password, confirmPassword) { @@ -88,7 +88,6 @@ ActiveEntry.verifyEmail = function (email) { ActiveEntry.errorMessages.set('email', 'Email is poorly formatted'); ActiveEntry.successMessages.set('email', null); } else if (email.indexOf("@") >= 0){ - //ActiveEntry.errorMessages.set('email', 'Email present'); ActiveEntry.errorMessages.set('email', null); ActiveEntry.successMessages.set('email', 'Email present'); } @@ -111,17 +110,43 @@ ActiveEntry.verifyFullName = function (fullName) { ActiveEntry.signIn = function (emailValue, passwordValue){ ActiveEntry.verifyPassword(passwordValue); ActiveEntry.verifyEmail(emailValue); + var ActiveEntryConfig = Session.get('Photonic.ActiveEntry'); + var failedAttemptsLimit = ActiveEntryConfig && ActiveEntryConfig.passwordOptions && ActiveEntryConfig.passwordOptions.failedAttemptsLimit || 5; - Meteor.loginWithPassword({email: emailValue}, passwordValue, function (error, result) { + Meteor.call("getFailedAttemptsCount", emailValue, function(error, failedAttemptsCount) { if (error) { - ActiveEntry.errorMessages.set('signInError', error.message); + console.warn(error.message); } else { - console.log('result', result); - var ActiveEntryConfig = Session.get('Photonic.ActiveEntry'); - console.log('ActiveEntryConfig', JSON.stringify(ActiveEntryConfig)); - Router.go(ActiveEntryConfig.signIn.destination); + if (failedAttemptsCount != failedAttemptsLimit) { + Meteor.loginWithPassword({email: emailValue}, passwordValue, function (error, result) { + if (error) { + // Login failed + Meteor.call("updateFailedAttempts", [emailValue, failedAttemptsLimit], function(error, failedAttemptCount) { + if (error) { + console.warn(error); + } else { + if (failedAttemptCount == failedAttemptsLimit) { + ActiveEntry.errorMessages.set('signInError', "Too many failed login attempts. Your account has been locked."); + + } else { + ActiveEntry.errorMessages.set('signInError', (failedAttemptsLimit - failedAttemptCount) + " attempts remaining."); + + } + } + }); + } else { + console.log('result', result); + Meteor.call("resetFailedAttempts", emailValue); + Router.go(ActiveEntryConfig.signIn.destination); + } + }); + } else { + ActiveEntry.errorMessages.set('signInError', "Your account has been locked."); + } } + }); + }; ActiveEntry.signUp = function (emailValue, passwordValue, confirmPassword, fullName){ @@ -147,12 +172,18 @@ ActiveEntry.signUp = function (emailValue, passwordValue, confirmPassword, fullN password: passwordValue, profile: { fullName: fullName + }, + testCase: { + createdAt: new Date() } }, function (error, result) { if (error) { console.log(error); ActiveEntry.errorMessages.set('signInError', error.message); } else { + // Add password in previous password field + ActiveEntry.insertHashedPassword(passwordValue); + ActiveEntry.updatePasswordCreatedDate(); var ActiveEntryConfig = Session.get('Photonic.ActiveEntry'); Router.go(ActiveEntryConfig.signUp.destination); } @@ -172,6 +203,18 @@ ActiveEntry.signUp = function (emailValue, passwordValue, confirmPassword, fullN // Router.go(ActiveEntryConfig.signIn.destination); // }); }; + +// Insert hashed password in previousPasswords fields +ActiveEntry.insertHashedPassword = function(passwordValue) { + var ActiveEntryConfig = Session.get('Photonic.ActiveEntry'); + var passwordHistoryCount = ActiveEntryConfig && ActiveEntryConfig.passwordOptions && ActiveEntryConfig.passwordOptions.passwordHistoryCount || 6; + Meteor.call("insertHashedPassword", [new String(passwordValue).hashCode(),passwordHistoryCount]); +}; + +ActiveEntry.updatePasswordCreatedDate = function() { + Meteor.call("updatePasswordCreatedDate"); +}; + ActiveEntry.signOut = function (){ Meteor.logout(); }; diff --git a/Packages/active-entry/lib/hashCodeGenerator.js b/Packages/active-entry/lib/hashCodeGenerator.js new file mode 100644 index 000000000..764f46e58 --- /dev/null +++ b/Packages/active-entry/lib/hashCodeGenerator.js @@ -0,0 +1,10 @@ +String.prototype.hashCode = function() { + var hash = 0, i, chr, len; + if (this.length === 0) return hash; + for (i = 0, len = this.length; i < len; i++) { + chr = this.charCodeAt(i); + hash = ((hash << 5) - hash) + chr; + hash |= 0; // Convert to 32bit integer + } + return hash; +}; \ No newline at end of file diff --git a/Packages/active-entry/package.js b/Packages/active-entry/package.js index a4e5244fb..d8382c70a 100755 --- a/Packages/active-entry/package.js +++ b/Packages/active-entry/package.js @@ -35,7 +35,8 @@ Package.onUse(function (api) { api.addFiles([ 'lib/jquery.pwstrength.bootstrap.js', - 'lib/checkPasswordStrength.js' + 'lib/checkPasswordStrength.js', + 'lib/hashCodeGenerator.js' ], ['client']); api.imply('accounts-base'); diff --git a/Packages/active-entry/server/methods.js b/Packages/active-entry/server/methods.js index 834dfde21..5b6f4c268 100644 --- a/Packages/active-entry/server/methods.js +++ b/Packages/active-entry/server/methods.js @@ -3,10 +3,74 @@ Meteor.methods({ console.log('Initializing Users', Meteor.users.find().fetch()); }, + dropEntryUsers: function (){ console.log('Drop Users', Meteor.users.find().fetch()); Meteor.users.find().forEach(function(user){ Meteor.users.remove({_id: user._id}); }); + }, + + insertHashedPassword: function(passwordParameters) { + var hashedPassword = passwordParameters[0]; + var passwordHistoryCount = passwordParameters[1]; + + var userId = Meteor.userId(); + var previousPasswords = Meteor.users.findOne({_id: userId}).previousPasswords; + if (previousPasswords) { + if (previousPasswords.length == passwordHistoryCount) { + // Remove oldest password + var ascSortedPasswords = _.sortBy(previousPasswords, function(previousPassword){ return previousPassword.createdAt; }); + ascSortedPasswords.splice(0, 1); + previousPasswords = ascSortedPasswords; + } + + previousPasswords.push({hashedPassword: hashedPassword, createdAt: new Date()}); + Meteor.users.update({_id: userId}, {$set: {previousPasswords: previousPasswords}}); + } else { + Meteor.users.update({_id: userId}, {$set: {previousPasswords: [{hashedPassword: hashedPassword, createdAt: new Date(), select: false}]}}); + } + }, + + checkPasswordExistence: function(hashedPassword) { + var previousPasswords = Meteor.users.find({_id: Meteor.userId()}).fetch()[0].previousPasswords; + for(var i=0; i< previousPasswords.length; i++) { + var recordedHashedPassword = previousPasswords[i].hashedPassword; + if (recordedHashedPassword == hashedPassword) { + return true; + } + } + return false; + + }, + + getFailedAttemptsCount: function(emailAddress) { + return Meteor.users.findOne({"emails.address": emailAddress}).failedPasswordAttempts || 0; + }, + + updateFailedAttempts: function(failedAttemptsParameters) { + var emailAddress = failedAttemptsParameters[0]; + var failedAttemptsLimit = failedAttemptsParameters[1]; + var failedAttemptCount = Meteor.users.findOne({"emails.address": emailAddress}).failedPasswordAttempts || 0; + if (failedAttemptCount == failedAttemptsLimit) { + return failedAttemptCount; + } else { + if (failedAttemptCount == 4) { + // Locked user account + Meteor.users.update({"emails.address": emailAddress}, {$set: {"profile.isLocked": true, failedPasswordAttempts: failedAttemptCount + 1}}); + } else if (failedAttemptCount < 4) { + Meteor.users.update({"emails.address": emailAddress}, {$set: {failedPasswordAttempts: failedAttemptCount + 1}}); + } + } + + return failedAttemptCount + 1; + }, + + resetFailedAttempts: function(emailAddress) { + Meteor.users.update({"emails.address": emailAddress}, {$set: {failedPasswordAttempts: 0}}); + }, + + updatePasswordCreatedDate: function() { + Meteor.users.update({_id: Meteor.userId()}, {$set: {"services.password.createdAt": new Date()}}); } }); diff --git a/Packages/meteor-stale-session/client.js b/Packages/meteor-stale-session/client.js index 34594fd7b..bb16b7f00 100644 --- a/Packages/meteor-stale-session/client.js +++ b/Packages/meteor-stale-session/client.js @@ -41,19 +41,13 @@ Meteor.startup(function() { overdueTimestamp = overdueTimestamp - (overdueTimestamp % 1000); console.log(overdueTimestamp); var startTime = inactivityTimeout - dialogTimeout; - if (overdueTimestamp <= inactivityTimeout) { - var nextIntervalTime = overdueTimestamp + countdownHeartbeatInterval; - if (nextIntervalTime <= inactivityTimeout && nextIntervalTime >= startTime) { - if (Math.abs(startTime - overdueTimestamp) <= Math.abs(nextIntervalTime - startTime) && !dialogIsOpen) { - // Open dialog - var leftTime = Math.round((inactivityTimeout - overdueTimestamp) / 1000); - $.event.trigger('TriggerOpenTimeoutCountdownDialog', leftTime); - dialogIsOpen = true; - } - } else { - // Event to close dialog - $.event.trigger('TriggerCloseTimeoutCountdownDialog'); - dialogIsOpen = false; + var nextIntervalTime = overdueTimestamp + countdownHeartbeatInterval; + if (overdueTimestamp <= inactivityTimeout && nextIntervalTime <= inactivityTimeout && nextIntervalTime >= startTime) { + if (Math.abs(startTime - overdueTimestamp) <= Math.abs(nextIntervalTime - startTime) && !dialogIsOpen) { + // Open dialog + var leftTime = Math.round((inactivityTimeout - overdueTimestamp) / 1000); + $.event.trigger('TriggerOpenTimeoutCountdownDialog', leftTime); + dialogIsOpen = true; } } else {