* added account unlock on password reset * added account policy option * added changelog entry * Added docs entry * moved changelog entry to correct position * improved tests to ensure requesting password reset email does not unlock account * run prettier
467 lines
14 KiB
JavaScript
467 lines
14 KiB
JavaScript
'use strict';
|
|
|
|
const Config = require('../lib/Config');
|
|
const Definitions = require('../lib/Options/Definitions');
|
|
const request = require('../lib/request');
|
|
|
|
const loginWithWrongCredentialsShouldFail = function (username, password) {
|
|
return new Promise((resolve, reject) => {
|
|
Parse.User.logIn(username, password)
|
|
.then(() => reject('login should have failed'))
|
|
.catch(err => {
|
|
if (err.message === 'Invalid username/password.') {
|
|
resolve();
|
|
} else {
|
|
reject(err);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
const isAccountLockoutError = function (username, password, duration, waitTime) {
|
|
return new Promise((resolve, reject) => {
|
|
setTimeout(() => {
|
|
Parse.User.logIn(username, password)
|
|
.then(() => 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(() => {
|
|
const user = new Parse.User();
|
|
user.setUsername('username1');
|
|
user.setPassword('password');
|
|
return user.signUp(null);
|
|
})
|
|
.then(() => {
|
|
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(() => {
|
|
Config.get('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(() => {
|
|
Config.get('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(() => {
|
|
Config.get('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(() => {
|
|
Config.get('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(() => {
|
|
Config.get('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(() => {
|
|
Config.get('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(() => {
|
|
const 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(() => {
|
|
const 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(() => {
|
|
const 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(() => 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();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('lockout with password reset option', () => {
|
|
let sendPasswordResetEmail;
|
|
|
|
async function setup(options = {}) {
|
|
const accountLockout = Object.assign(
|
|
{
|
|
duration: 10000,
|
|
threshold: 1,
|
|
},
|
|
options
|
|
);
|
|
const config = {
|
|
appName: 'exampleApp',
|
|
accountLockout: accountLockout,
|
|
publicServerURL: 'http://localhost:8378/1',
|
|
emailAdapter: {
|
|
sendVerificationEmail: () => Promise.resolve(),
|
|
sendPasswordResetEmail: () => Promise.resolve(),
|
|
sendMail: () => {},
|
|
},
|
|
};
|
|
await reconfigureServer(config);
|
|
|
|
sendPasswordResetEmail = spyOn(config.emailAdapter, 'sendPasswordResetEmail').and.callThrough();
|
|
}
|
|
|
|
it('accepts valid unlockOnPasswordReset option', async () => {
|
|
const values = [true, false];
|
|
|
|
for (const value of values) {
|
|
await expectAsync(setup({ unlockOnPasswordReset: value })).toBeResolved();
|
|
}
|
|
});
|
|
|
|
it('rejects invalid unlockOnPasswordReset option', async () => {
|
|
const values = ['a', 0, {}, [], null];
|
|
|
|
for (const value of values) {
|
|
await expectAsync(setup({ unlockOnPasswordReset: value })).toBeRejected();
|
|
}
|
|
});
|
|
|
|
it('uses default value if unlockOnPasswordReset is not set', async () => {
|
|
await expectAsync(setup({ unlockOnPasswordReset: undefined })).toBeResolved();
|
|
|
|
const parseConfig = Config.get(Parse.applicationId);
|
|
expect(parseConfig.accountLockout.unlockOnPasswordReset).toBe(
|
|
Definitions.AccountLockoutOptions.unlockOnPasswordReset.default
|
|
);
|
|
});
|
|
|
|
it('allow login for locked account after password reset', async () => {
|
|
await setup({ unlockOnPasswordReset: true });
|
|
const config = Config.get(Parse.applicationId);
|
|
|
|
const user = new Parse.User();
|
|
const username = 'exampleUsername';
|
|
const password = 'examplePassword';
|
|
user.setUsername(username);
|
|
user.setPassword(password);
|
|
user.setEmail('mail@example.com');
|
|
await user.signUp();
|
|
|
|
await expectAsync(Parse.User.logIn(username, 'incorrectPassword')).toBeRejected();
|
|
await expectAsync(Parse.User.logIn(username, password)).toBeRejected();
|
|
|
|
await Parse.User.requestPasswordReset(user.getEmail());
|
|
await expectAsync(Parse.User.logIn(username, password)).toBeRejected();
|
|
|
|
const link = sendPasswordResetEmail.calls.all()[0].args[0].link;
|
|
const linkUrl = new URL(link);
|
|
const token = linkUrl.searchParams.get('token');
|
|
const newPassword = 'newPassword';
|
|
await request({
|
|
method: 'POST',
|
|
url: `${config.publicServerURL}/apps/test/request_password_reset`,
|
|
body: `new_password=${newPassword}&token=${token}&username=${username}`,
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
followRedirects: false,
|
|
});
|
|
|
|
await expectAsync(Parse.User.logIn(username, newPassword)).toBeResolved();
|
|
});
|
|
|
|
it('reject login for locked account after password reset (default)', async () => {
|
|
await setup();
|
|
const config = Config.get(Parse.applicationId);
|
|
|
|
const user = new Parse.User();
|
|
const username = 'exampleUsername';
|
|
const password = 'examplePassword';
|
|
user.setUsername(username);
|
|
user.setPassword(password);
|
|
user.setEmail('mail@example.com');
|
|
await user.signUp();
|
|
|
|
await expectAsync(Parse.User.logIn(username, 'incorrectPassword')).toBeRejected();
|
|
await expectAsync(Parse.User.logIn(username, password)).toBeRejected();
|
|
|
|
await Parse.User.requestPasswordReset(user.getEmail());
|
|
await expectAsync(Parse.User.logIn(username, password)).toBeRejected();
|
|
|
|
const link = sendPasswordResetEmail.calls.all()[0].args[0].link;
|
|
const linkUrl = new URL(link);
|
|
const token = linkUrl.searchParams.get('token');
|
|
const newPassword = 'newPassword';
|
|
await request({
|
|
method: 'POST',
|
|
url: `${config.publicServerURL}/apps/test/request_password_reset`,
|
|
body: `new_password=${newPassword}&token=${token}&username=${username}`,
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
followRedirects: false,
|
|
});
|
|
|
|
await expectAsync(Parse.User.logIn(username, newPassword)).toBeRejected();
|
|
});
|
|
});
|