Skip to content

Adds ability to set an account lockout policy #2601

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 3, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
* `loggerAdapter` - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js)).
* `sessionLength` - The length of time in seconds that a session should be valid for. Defaults to 31536000 seconds (1 year).
* `revokeSessionOnPasswordReset` - When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.
* `accountLockout` - Lock account when a malicious user is attempting to determine an account password by trial and error.

##### Logging

Expand Down Expand Up @@ -259,7 +260,14 @@ var server = ParseServer({
// Your API key from mailgun.com
apiKey: 'key-mykey',
}
}
},

// account lockout policy setting (OPTIONAL) - defaults to undefined
// if the account lockout policy is set and there are more than `threshold` number of failed login attempts then the `login` api call returns error code `Parse.Error.OBJECT_NOT_FOUND` with error message `Your account is locked due to multiple failed login attempts. Please try again after <duration> minute(s)`. After `duration` minutes of no login attempts, the application will allow the user to try login again.
accountLockout: {
duration: 5, // duration policy setting determines the number of minutes that a locked-out account remains locked out before automatically becoming unlocked. Set it to a value greater than 0 and less than 100000.
threshold: 3, // threshold policy setting determines the number of failed sign-in attempts that will cause a user account to be locked. Set it to an integer value greater than 0 and less than 1000.
},
});
```

Expand Down
318 changes: 318 additions & 0 deletions spec/AccountLockoutPolicy.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
"use strict";

const Config = require("../src/Config");

var loginWithWrongCredentialsShouldFail = function(username, password) {
return new Promise((resolve, reject) => {
Parse.User.logIn(username, password)
.then(user => reject('login should have failed'))
.catch(err => {
if (err.message === 'Invalid username/password.') {
resolve();
} else {
reject(err);
}
});
});
};

var isAccountLockoutError = function(username, password, duration, waitTime) {
return new Promise((resolve, reject) => {
setTimeout(() => {
Parse.User.logIn(username, password)
.then(user => reject('login should have failed'))
.catch(err => {
if (err.message === 'Your account is locked due to multiple failed login attempts. Please try again after ' + duration + ' minute(s)') {
resolve();
} else {
reject(err);
}
});
}, waitTime);
});
};

describe("Account Lockout Policy: ", () => {

it('account should not be locked even after failed login attempts if account lockout policy is not set', done => {
reconfigureServer({
appName: 'unlimited',
publicServerURL: 'http://localhost:1337/1',
})
.then(() => {
var user = new Parse.User();
user.setUsername('username1');
user.setPassword('password');
return user.signUp(null);
})
.then(user => {
return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 1');
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 2');
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 3');
})
.then(() => done())
.catch(err => {
fail('allow unlimited failed login attempts failed: ' + JSON.stringify(err));
done();
});
});

it('throw error if duration is set to an invalid number', done => {
reconfigureServer({
appName: 'duration',
accountLockout: {
duration: 'invalid value',
threshold: 5
},
publicServerURL: "https://my.public.server.com/1"
})
.then(() => {
var config = new Config('test');
fail('set duration to an invalid number test failed');
done();
})
.catch(err => {
if (err && err === 'Account lockout duration should be greater than 0 and less than 100000') {
done();
} else {
fail('set duration to an invalid number test failed: ' + JSON.stringify(err));
done();
}
});
});

it('throw error if threshold is set to an invalid number', done => {
reconfigureServer({
appName: 'threshold',
accountLockout: {
duration: 5,
threshold: 'invalid number'
},
publicServerURL: "https://my.public.server.com/1"
})
.then(() => {
var config = new Config('test');
fail('set threshold to an invalid number test failed');
done();
})
.catch(err => {
if (err && err === 'Account lockout threshold should be an integer greater than 0 and less than 1000') {
done();
} else {
fail('set threshold to an invalid number test failed: ' + JSON.stringify(err));
done();
}
});
});

it('throw error if threshold is < 1', done => {
reconfigureServer({
appName: 'threshold',
accountLockout: {
duration: 5,
threshold: 0
},
publicServerURL: "https://my.public.server.com/1"
})
.then(() => {
var config = new Config('test');
fail('threshold value < 1 is invalid test failed');
done();
})
.catch(err => {
if (err && err === 'Account lockout threshold should be an integer greater than 0 and less than 1000') {
done();
} else {
fail('threshold value < 1 is invalid test failed: ' + JSON.stringify(err));
done();
}
});
});

it('throw error if threshold is > 999', done => {
reconfigureServer({
appName: 'threshold',
accountLockout: {
duration: 5,
threshold: 1000
},
publicServerURL: "https://my.public.server.com/1"
})
.then(() => {
var config = new Config('test');
fail('threshold value > 999 is invalid test failed');
done();
})
.catch(err => {
if (err && err === 'Account lockout threshold should be an integer greater than 0 and less than 1000') {
done();
} else {
fail('threshold value > 999 is invalid test failed: ' + JSON.stringify(err));
done();
}
});
});

it('throw error if duration is <= 0', done => {
reconfigureServer({
appName: 'duration',
accountLockout: {
duration: 0,
threshold: 5
},
publicServerURL: "https://my.public.server.com/1"
})
.then(() => {
var config = new Config('test');
fail('duration value < 1 is invalid test failed');
done();
})
.catch(err => {
if (err && err === 'Account lockout duration should be greater than 0 and less than 100000') {
done();
} else {
fail('duration value < 1 is invalid test failed: ' + JSON.stringify(err));
done();
}
});
});

it('throw error if duration is > 99999', done => {
reconfigureServer({
appName: 'duration',
accountLockout: {
duration: 100000,
threshold: 5
},
publicServerURL: "https://my.public.server.com/1"
})
.then(() => {
var config = new Config('test');
fail('duration value > 99999 is invalid test failed');
done();
})
.catch(err => {
if (err && err === 'Account lockout duration should be greater than 0 and less than 100000') {
done();
} else {
fail('duration value > 99999 is invalid test failed: ' + JSON.stringify(err));
done();
}
});
});

it('lock account if failed login attempts are above threshold', done => {
reconfigureServer({
appName: 'lockout threshold',
accountLockout: {
duration: 1,
threshold: 2
},
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
var user = new Parse.User();
user.setUsername("username2");
user.setPassword("failedLoginAttemptsThreshold");
return user.signUp();
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username2', 'wrong password');
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username2', 'wrong password');
})
.then(() => {
return isAccountLockoutError('username2', 'wrong password', 1, 1);
})
.then(() => {
done();
})
.catch(err => {
fail('lock account after failed login attempts test failed: ' + JSON.stringify(err));
done();
});
});

it('lock account for accountPolicy.duration minutes if failed login attempts are above threshold', done => {
reconfigureServer({
appName: 'lockout threshold',
accountLockout: {
duration: 0.05, // 0.05*60 = 3 secs
threshold: 2
},
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
var user = new Parse.User();
user.setUsername("username3");
user.setPassword("failedLoginAttemptsThreshold");
return user.signUp();
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username3', 'wrong password');
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username3', 'wrong password');
})
.then(() => {
return isAccountLockoutError('username3', 'wrong password', 0.05, 1);
})
.then(() => {
// account should still be locked even after 2 seconds.
return isAccountLockoutError('username3', 'wrong password', 0.05, 2000);
})
.then(() => {
done();
})
.catch(err => {
fail('account should be locked for duration mins test failed: ' + JSON.stringify(err));
done();
});
});

it('allow login for locked account after accountPolicy.duration minutes', done => {
reconfigureServer({
appName: 'lockout threshold',
accountLockout: {
duration: 0.05, // 0.05*60 = 3 secs
threshold: 2
},
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
var user = new Parse.User();
user.setUsername("username4");
user.setPassword("correct password");
return user.signUp();
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username4', 'wrong password');
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username4', 'wrong password');
})
.then(() => {
// allow locked user to login after 3 seconds with a valid userid and password
return new Promise((resolve, reject) => {
setTimeout(() => {
Parse.User.logIn('username4', 'correct password')
.then(user => resolve())
.catch(err => reject(err));
}, 3001);
});
})
.then(() => {
done();
})
.catch(err => {
fail('allow login for locked account after accountPolicy.duration minutes test failed: ' + JSON.stringify(err));
done();
});
});

})
Loading