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
This commit is contained in:
Aysel Afsar 2016-02-14 17:03:00 -05:00
parent 24e615da1e
commit eb32e1771f
15 changed files with 250 additions and 49 deletions

View File

@ -15,10 +15,13 @@ if (Meteor.isClient){
primary: ""
},
passwordOptions: {
showPasswordStrengthIndicator: true,
requireRegexValidation: true
showPasswordStrengthIndicator: false,
requireRegexValidation: true,
//requireStrongPasswords: false
passwordHistoryCount: 6,
failedAttemptsLimit: 5
}
});
}

View File

@ -10,7 +10,9 @@
<ul class="nav nav-pills pull-right">
<li class="dropdown"><a class="dropdown-toggle" data-toggle="dropdown" href="#">{{fullName}}<b class="caret"></b></a>
<ul class="dropdown-menu pull-right">
<li><a href="#" id="logoutButton">Logout</a></li>
<li><a href="changePassword" id="changePassword"><i class="fa fa-lock"></i>Change Password</a></li>
<li class="divider"></li>
<li><a href="#" id="logoutButton"><i class="fa fa-power-off"></i>Logout</a></li>
</ul>
</li>
</ul>

View File

@ -3,6 +3,9 @@ Template.layoutLesionTracker.events({
Meteor.logout(function(){
Router.go('/entrySignIn');
});
},
'click #changePassword': function() {
Router.go('/changePassword');
}
});

View File

@ -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

View File

@ -8,6 +8,14 @@
<div id="changePasswordPageMessage" class="subtitle-auth" style="{{getChangePasswordMessageColor}}">{{getChangePasswordMessage}}</div>
<form>
{{#if entryErrorMessages}}
<div id="errorMessages" class="list-errors">
{{#each entryErrorMessages}}
<div class="alert alert-danger list-item">{{this}}</div>
{{/each}}
</div>
{{/if}}
<div class="input-symbol">
<input id="changePasswordPageOldPasswordInput" type="password" name="oldPassword" placeholder="Old Password" style="{{getPasswordStyling}}" />
<span class="fa fa-lock" title="Password"></span>

View File

@ -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');
}
});
}
}
});
}
}
});

View File

@ -30,7 +30,7 @@
</form>
<br><br>
<button id="signInToAppButton" class="btn-primary btn-main btn-large" style="{{getButtonColor}}">{{getButtonText}}</button>
<button id="signInToAppButton" class="btn-primary btn-main btn-large disabledButton" disabled style="{{getButtonColor}}">{{getButtonText}}</button>
<br><br>
<button id="needAnAccountButton" class="btn-gray btn-main btn-large">Need an account?</button>

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -11,13 +11,6 @@
<div id="signUpPageMessage" class="subtitle-auth" style="{{getSignUpMessageColor}}">{{getSignUpMessage}}</div>
<form id="entrySignUpForm">
<!--{{#if errorMessages}}
<div id="errorMessages" class="list-errors">
{{#each errorMessages}}
<div class="alert alert-danger list-item">{{this}}</div>
{{/each}}
</div>
{{/if}}-->
{{#if entryErrorMessages}}
<div id="errorMessages" class="list-errors">

View File

@ -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();
};

View File

@ -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;
};

View File

@ -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');

View File

@ -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()}});
}
});

View File

@ -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 {