Skip to content

Commit 28bd378

Browse files
cherukumilliflovilmart
authored andcommitted
Adds ability to set an account lockout policy (#2601)
* Adds ability to set account lockout policy * change fit to it in tests
1 parent f6516a1 commit 28bd378

File tree

10 files changed

+614
-24
lines changed

10 files changed

+614
-24
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
206206
* `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)).
207207
* `sessionLength` - The length of time in seconds that a session should be valid for. Defaults to 31536000 seconds (1 year).
208208
* `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.
209+
* `accountLockout` - Lock account when a malicious user is attempting to determine an account password by trial and error.
209210

210211
##### Logging
211212

@@ -259,7 +260,14 @@ var server = ParseServer({
259260
// Your API key from mailgun.com
260261
apiKey: 'key-mykey',
261262
}
262-
}
263+
},
264+
265+
// account lockout policy setting (OPTIONAL) - defaults to undefined
266+
// 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.
267+
accountLockout: {
268+
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.
269+
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.
270+
},
263271
});
264272
```
265273

spec/AccountLockoutPolicy.spec.js

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
"use strict";
2+
3+
const Config = require("../src/Config");
4+
5+
var loginWithWrongCredentialsShouldFail = function(username, password) {
6+
return new Promise((resolve, reject) => {
7+
Parse.User.logIn(username, password)
8+
.then(user => reject('login should have failed'))
9+
.catch(err => {
10+
if (err.message === 'Invalid username/password.') {
11+
resolve();
12+
} else {
13+
reject(err);
14+
}
15+
});
16+
});
17+
};
18+
19+
var isAccountLockoutError = function(username, password, duration, waitTime) {
20+
return new Promise((resolve, reject) => {
21+
setTimeout(() => {
22+
Parse.User.logIn(username, password)
23+
.then(user => reject('login should have failed'))
24+
.catch(err => {
25+
if (err.message === 'Your account is locked due to multiple failed login attempts. Please try again after ' + duration + ' minute(s)') {
26+
resolve();
27+
} else {
28+
reject(err);
29+
}
30+
});
31+
}, waitTime);
32+
});
33+
};
34+
35+
describe("Account Lockout Policy: ", () => {
36+
37+
it('account should not be locked even after failed login attempts if account lockout policy is not set', done => {
38+
reconfigureServer({
39+
appName: 'unlimited',
40+
publicServerURL: 'http://localhost:1337/1',
41+
})
42+
.then(() => {
43+
var user = new Parse.User();
44+
user.setUsername('username1');
45+
user.setPassword('password');
46+
return user.signUp(null);
47+
})
48+
.then(user => {
49+
return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 1');
50+
})
51+
.then(() => {
52+
return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 2');
53+
})
54+
.then(() => {
55+
return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 3');
56+
})
57+
.then(() => done())
58+
.catch(err => {
59+
fail('allow unlimited failed login attempts failed: ' + JSON.stringify(err));
60+
done();
61+
});
62+
});
63+
64+
it('throw error if duration is set to an invalid number', done => {
65+
reconfigureServer({
66+
appName: 'duration',
67+
accountLockout: {
68+
duration: 'invalid value',
69+
threshold: 5
70+
},
71+
publicServerURL: "https://my.public.server.com/1"
72+
})
73+
.then(() => {
74+
var config = new Config('test');
75+
fail('set duration to an invalid number test failed');
76+
done();
77+
})
78+
.catch(err => {
79+
if (err && err === 'Account lockout duration should be greater than 0 and less than 100000') {
80+
done();
81+
} else {
82+
fail('set duration to an invalid number test failed: ' + JSON.stringify(err));
83+
done();
84+
}
85+
});
86+
});
87+
88+
it('throw error if threshold is set to an invalid number', done => {
89+
reconfigureServer({
90+
appName: 'threshold',
91+
accountLockout: {
92+
duration: 5,
93+
threshold: 'invalid number'
94+
},
95+
publicServerURL: "https://my.public.server.com/1"
96+
})
97+
.then(() => {
98+
var config = new Config('test');
99+
fail('set threshold to an invalid number test failed');
100+
done();
101+
})
102+
.catch(err => {
103+
if (err && err === 'Account lockout threshold should be an integer greater than 0 and less than 1000') {
104+
done();
105+
} else {
106+
fail('set threshold to an invalid number test failed: ' + JSON.stringify(err));
107+
done();
108+
}
109+
});
110+
});
111+
112+
it('throw error if threshold is < 1', done => {
113+
reconfigureServer({
114+
appName: 'threshold',
115+
accountLockout: {
116+
duration: 5,
117+
threshold: 0
118+
},
119+
publicServerURL: "https://my.public.server.com/1"
120+
})
121+
.then(() => {
122+
var config = new Config('test');
123+
fail('threshold value < 1 is invalid test failed');
124+
done();
125+
})
126+
.catch(err => {
127+
if (err && err === 'Account lockout threshold should be an integer greater than 0 and less than 1000') {
128+
done();
129+
} else {
130+
fail('threshold value < 1 is invalid test failed: ' + JSON.stringify(err));
131+
done();
132+
}
133+
});
134+
});
135+
136+
it('throw error if threshold is > 999', done => {
137+
reconfigureServer({
138+
appName: 'threshold',
139+
accountLockout: {
140+
duration: 5,
141+
threshold: 1000
142+
},
143+
publicServerURL: "https://my.public.server.com/1"
144+
})
145+
.then(() => {
146+
var config = new Config('test');
147+
fail('threshold value > 999 is invalid test failed');
148+
done();
149+
})
150+
.catch(err => {
151+
if (err && err === 'Account lockout threshold should be an integer greater than 0 and less than 1000') {
152+
done();
153+
} else {
154+
fail('threshold value > 999 is invalid test failed: ' + JSON.stringify(err));
155+
done();
156+
}
157+
});
158+
});
159+
160+
it('throw error if duration is <= 0', done => {
161+
reconfigureServer({
162+
appName: 'duration',
163+
accountLockout: {
164+
duration: 0,
165+
threshold: 5
166+
},
167+
publicServerURL: "https://my.public.server.com/1"
168+
})
169+
.then(() => {
170+
var config = new Config('test');
171+
fail('duration value < 1 is invalid test failed');
172+
done();
173+
})
174+
.catch(err => {
175+
if (err && err === 'Account lockout duration should be greater than 0 and less than 100000') {
176+
done();
177+
} else {
178+
fail('duration value < 1 is invalid test failed: ' + JSON.stringify(err));
179+
done();
180+
}
181+
});
182+
});
183+
184+
it('throw error if duration is > 99999', done => {
185+
reconfigureServer({
186+
appName: 'duration',
187+
accountLockout: {
188+
duration: 100000,
189+
threshold: 5
190+
},
191+
publicServerURL: "https://my.public.server.com/1"
192+
})
193+
.then(() => {
194+
var config = new Config('test');
195+
fail('duration value > 99999 is invalid test failed');
196+
done();
197+
})
198+
.catch(err => {
199+
if (err && err === 'Account lockout duration should be greater than 0 and less than 100000') {
200+
done();
201+
} else {
202+
fail('duration value > 99999 is invalid test failed: ' + JSON.stringify(err));
203+
done();
204+
}
205+
});
206+
});
207+
208+
it('lock account if failed login attempts are above threshold', done => {
209+
reconfigureServer({
210+
appName: 'lockout threshold',
211+
accountLockout: {
212+
duration: 1,
213+
threshold: 2
214+
},
215+
publicServerURL: "http://localhost:8378/1"
216+
})
217+
.then(() => {
218+
var user = new Parse.User();
219+
user.setUsername("username2");
220+
user.setPassword("failedLoginAttemptsThreshold");
221+
return user.signUp();
222+
})
223+
.then(() => {
224+
return loginWithWrongCredentialsShouldFail('username2', 'wrong password');
225+
})
226+
.then(() => {
227+
return loginWithWrongCredentialsShouldFail('username2', 'wrong password');
228+
})
229+
.then(() => {
230+
return isAccountLockoutError('username2', 'wrong password', 1, 1);
231+
})
232+
.then(() => {
233+
done();
234+
})
235+
.catch(err => {
236+
fail('lock account after failed login attempts test failed: ' + JSON.stringify(err));
237+
done();
238+
});
239+
});
240+
241+
it('lock account for accountPolicy.duration minutes if failed login attempts are above threshold', done => {
242+
reconfigureServer({
243+
appName: 'lockout threshold',
244+
accountLockout: {
245+
duration: 0.05, // 0.05*60 = 3 secs
246+
threshold: 2
247+
},
248+
publicServerURL: "http://localhost:8378/1"
249+
})
250+
.then(() => {
251+
var user = new Parse.User();
252+
user.setUsername("username3");
253+
user.setPassword("failedLoginAttemptsThreshold");
254+
return user.signUp();
255+
})
256+
.then(() => {
257+
return loginWithWrongCredentialsShouldFail('username3', 'wrong password');
258+
})
259+
.then(() => {
260+
return loginWithWrongCredentialsShouldFail('username3', 'wrong password');
261+
})
262+
.then(() => {
263+
return isAccountLockoutError('username3', 'wrong password', 0.05, 1);
264+
})
265+
.then(() => {
266+
// account should still be locked even after 2 seconds.
267+
return isAccountLockoutError('username3', 'wrong password', 0.05, 2000);
268+
})
269+
.then(() => {
270+
done();
271+
})
272+
.catch(err => {
273+
fail('account should be locked for duration mins test failed: ' + JSON.stringify(err));
274+
done();
275+
});
276+
});
277+
278+
it('allow login for locked account after accountPolicy.duration minutes', done => {
279+
reconfigureServer({
280+
appName: 'lockout threshold',
281+
accountLockout: {
282+
duration: 0.05, // 0.05*60 = 3 secs
283+
threshold: 2
284+
},
285+
publicServerURL: "http://localhost:8378/1"
286+
})
287+
.then(() => {
288+
var user = new Parse.User();
289+
user.setUsername("username4");
290+
user.setPassword("correct password");
291+
return user.signUp();
292+
})
293+
.then(() => {
294+
return loginWithWrongCredentialsShouldFail('username4', 'wrong password');
295+
})
296+
.then(() => {
297+
return loginWithWrongCredentialsShouldFail('username4', 'wrong password');
298+
})
299+
.then(() => {
300+
// allow locked user to login after 3 seconds with a valid userid and password
301+
return new Promise((resolve, reject) => {
302+
setTimeout(() => {
303+
Parse.User.logIn('username4', 'correct password')
304+
.then(user => resolve())
305+
.catch(err => reject(err));
306+
}, 3001);
307+
});
308+
})
309+
.then(() => {
310+
done();
311+
})
312+
.catch(err => {
313+
fail('allow login for locked account after accountPolicy.duration minutes test failed: ' + JSON.stringify(err));
314+
done();
315+
});
316+
});
317+
318+
})

0 commit comments

Comments
 (0)