Files
kami-parse-server/spec/PasswordPolicy.spec.js
Manuel 8f877d42c0 feat: Deprecation DEPPS11: Replace PublicAPIRouter with PagesRouter (#9974)
BREAKING CHANGE: This release replaces `PublicAPIRouter` with `PagesRouter` (Deprecation DEPPS11).
2025-12-12 20:55:39 +01:00

1716 lines
52 KiB
JavaScript

'use strict';
const request = require('../lib/request');
describe('Password Policy: ', () => {
it_id('b400a867-9f05-496f-af79-933aa588dde5')(it)('should show the invalid link page if the user clicks on the password reset link after the token expires', done => {
const user = new Parse.User();
let sendEmailOptions;
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: options => {
sendEmailOptions = options;
},
sendMail: () => {},
};
reconfigureServer({
appName: 'passwordPolicy',
emailAdapter: emailAdapter,
passwordPolicy: {
resetTokenValidityDuration: 0.5, // 0.5 second
},
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
user.setUsername('testResetTokenValidity');
user.setPassword('original');
user.set('email', 'user@parse.com');
return user.signUp();
})
.then(() => {
Parse.User.requestPasswordReset('user@parse.com').catch(err => {
jfail(err);
fail('Reset password request should not fail');
done();
});
})
.then(() => {
// wait for a bit more than the validity duration set
setTimeout(() => {
expect(sendEmailOptions).not.toBeUndefined();
request({
url: sendEmailOptions.link,
followRedirects: false,
simple: false,
resolveWithFullResponse: true,
})
.then(response => {
expect(response.status).toEqual(200);
expect(response.text).toContain('Invalid password reset link!');
done();
})
.catch(error => {
fail(error);
});
}, 1000);
})
.catch(err => {
jfail(err);
done();
});
});
it('should show the reset password page if the user clicks on the password reset link before the token expires', done => {
const user = new Parse.User();
let sendEmailOptions;
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: options => {
sendEmailOptions = options;
},
sendMail: () => {},
};
reconfigureServer({
appName: 'passwordPolicy',
emailAdapter: emailAdapter,
passwordPolicy: {
resetTokenValidityDuration: 5, // 5 seconds
},
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
user.setUsername('testResetTokenValidity');
user.setPassword('original');
user.set('email', 'user@parse.com');
return user.signUp();
})
.then(() => {
Parse.User.requestPasswordReset('user@parse.com').catch(err => {
jfail(err);
fail('Reset password request should not fail');
done();
});
})
.then(() => {
// wait for a bit but less than the validity duration
setTimeout(() => {
expect(sendEmailOptions).not.toBeUndefined();
request({
url: sendEmailOptions.link,
simple: false,
resolveWithFullResponse: true,
followRedirects: false,
})
.then(response => {
expect(response.status).toEqual(200);
expect(response.text).toContain('password');
done();
})
.catch(error => {
fail(error);
});
}, 1000);
})
.catch(err => {
jfail(err);
done();
});
});
it('should not keep reset token by default', async done => {
const sendEmailOptions = [];
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: options => {
sendEmailOptions.push(options);
},
sendMail: () => {},
};
await reconfigureServer({
appName: 'passwordPolicy',
emailAdapter: emailAdapter,
passwordPolicy: {
resetTokenValidityDuration: 5 * 60, // 5 minutes
},
publicServerURL: 'http://localhost:8378/1',
});
const user = new Parse.User();
user.setUsername('testResetTokenValidity');
user.setPassword('original');
user.set('email', 'user@example.com');
await user.signUp();
await Parse.User.requestPasswordReset('user@example.com');
await Parse.User.requestPasswordReset('user@example.com');
expect(sendEmailOptions[0].link).not.toBe(sendEmailOptions[1].link);
done();
});
it_id('7d98e1f2-ae89-4038-9ea7-5254854ea42e')(it)('should keep reset token with resetTokenReuseIfValid', async done => {
const sendEmailOptions = [];
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: options => {
sendEmailOptions.push(options);
},
sendMail: () => {},
};
await reconfigureServer({
appName: 'passwordPolicy',
emailAdapter: emailAdapter,
passwordPolicy: {
resetTokenValidityDuration: 5 * 60, // 5 minutes
resetTokenReuseIfValid: true,
},
publicServerURL: 'http://localhost:8378/1',
});
const user = new Parse.User();
user.setUsername('testResetTokenValidity');
user.setPassword('original');
user.set('email', 'user@example.com');
await user.signUp();
await Parse.User.requestPasswordReset('user@example.com');
await Parse.User.requestPasswordReset('user@example.com');
expect(sendEmailOptions[0].link).toBe(sendEmailOptions[1].link);
done();
});
it('should throw with invalid resetTokenReuseIfValid', async done => {
const sendEmailOptions = [];
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: options => {
sendEmailOptions.push(options);
},
sendMail: () => {},
};
try {
await reconfigureServer({
appName: 'passwordPolicy',
emailAdapter: emailAdapter,
passwordPolicy: {
resetTokenValidityDuration: 5 * 60, // 5 minutes
resetTokenReuseIfValid: [],
},
publicServerURL: 'http://localhost:8378/1',
});
fail('should have thrown.');
} catch (e) {
expect(e).toBe('resetTokenReuseIfValid must be a boolean value');
}
try {
await reconfigureServer({
appName: 'passwordPolicy',
emailAdapter: emailAdapter,
passwordPolicy: {
resetTokenReuseIfValid: true,
},
publicServerURL: 'http://localhost:8378/1',
});
fail('should have thrown.');
} catch (e) {
expect(e).toBe('You cannot use resetTokenReuseIfValid without resetTokenValidityDuration');
}
done();
});
it('should fail if passwordPolicy.resetTokenValidityDuration is not a number', done => {
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
resetTokenValidityDuration: 'not a number',
},
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
fail('passwordPolicy.resetTokenValidityDuration "not a number" test failed');
done();
})
.catch(err => {
expect(err).toEqual('passwordPolicy.resetTokenValidityDuration must be a positive number');
done();
});
});
it('should fail if passwordPolicy.resetTokenValidityDuration is zero or a negative number', done => {
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
resetTokenValidityDuration: 0,
},
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
fail('resetTokenValidityDuration negative number test failed');
done();
})
.catch(err => {
expect(err).toEqual('passwordPolicy.resetTokenValidityDuration must be a positive number');
done();
});
});
it('should fail if passwordPolicy.validatorPattern setting is invalid type', done => {
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validatorPattern: 1234, // number is not a valid setting
},
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
fail('passwordPolicy.validatorPattern type test failed');
done();
})
.catch(err => {
expect(err).toEqual(
'passwordPolicy.validatorPattern must be a regex string or RegExp object.'
);
done();
});
});
it('should fail if passwordPolicy.validatorCallback setting is invalid type', done => {
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validatorCallback: 'abc', // string is not a valid setting
},
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
fail('passwordPolicy.validatorCallback type test failed');
done();
})
.catch(err => {
expect(err).toEqual('passwordPolicy.validatorCallback must be a function.');
done();
});
});
it('signup should fail if password does not conform to the policy enforced using validatorPattern', done => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validatorPattern: /[0-9]+/, // password should contain at least one digit
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('nodigit');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
fail('Should have failed as password does not conform to the policy.');
done();
})
.catch(error => {
expect(error.code).toEqual(142);
done();
});
});
});
it('signup should fail if password does not conform to the policy enforced using validatorPattern string', done => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validatorPattern: '^.{8,}', // password should contain at least 8 char
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('less');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
fail('Should have failed as password does not conform to the policy.');
done();
})
.catch(error => {
expect(error.code).toEqual(142);
done();
});
});
});
it('signup should fail if password is empty', done => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validatorPattern: '^.{8,}', // password should contain at least 8 char
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
fail('Should have failed as password does not conform to the policy.');
done();
})
.catch(error => {
expect(error.message).toEqual('Cannot sign up user with an empty password.');
done();
});
});
});
it('signup should succeed if password conforms to the policy enforced using validatorPattern', done => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validatorPattern: /[0-9]+/, // password should contain at least one digit
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('1digit');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
Parse.User.logOut()
.then(() => {
Parse.User.logIn('user1', '1digit')
.then(function () {
done();
})
.catch(err => {
jfail(err);
fail('Should be able to login');
done();
});
})
.catch(error => {
jfail(error);
fail('logout should have succeeded');
done();
});
})
.catch(error => {
jfail(error);
fail('Signup should have succeeded as password conforms to the policy.');
done();
});
});
});
it('signup should succeed if password conforms to the policy enforced using validatorPattern string', done => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validatorPattern: '[!@#$]+', // password should contain at least one special char
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('p@sswrod');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
Parse.User.logOut()
.then(() => {
Parse.User.logIn('user1', 'p@sswrod')
.then(function () {
done();
})
.catch(err => {
jfail(err);
fail('Should be able to login');
done();
});
})
.catch(error => {
jfail(error);
fail('logout should have succeeded');
done();
});
})
.catch(error => {
jfail(error);
fail('Signup should have succeeded as password conforms to the policy.');
done();
});
});
});
it('signup should fail if password does not conform to the policy enforced using validatorCallback', done => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validatorCallback: () => false, // just fail
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('any');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
fail('Should have failed as password does not conform to the policy.');
done();
})
.catch(error => {
expect(error.code).toEqual(142);
done();
});
});
});
it('signup should succeed if password conforms to the policy enforced using validatorCallback', done => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validatorCallback: () => true, // never fail
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('oneUpper');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
Parse.User.logOut()
.then(() => {
Parse.User.logIn('user1', 'oneUpper')
.then(function () {
done();
})
.catch(err => {
jfail(err);
fail('Should be able to login');
done();
});
})
.catch(error => {
jfail(error);
fail('Logout should have succeeded');
done();
});
})
.catch(error => {
jfail(error);
fail('Should have succeeded as password conforms to the policy.');
done();
});
});
});
it('signup should fail if password does not match validatorPattern but succeeds validatorCallback', done => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validatorPattern: /[A-Z]+/, // password should contain at least one UPPER case letter
validatorCallback: () => true,
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('all lower');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
fail('Should have failed as password does not conform to the policy.');
done();
})
.catch(error => {
expect(error.code).toEqual(142);
done();
});
});
});
it('signup should fail if password matches validatorPattern but fails validatorCallback', done => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validatorPattern: /[A-Z]+/, // password should contain at least one UPPER case letter
validatorCallback: () => false,
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('oneUpper');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
fail('Should have failed as password does not conform to the policy.');
done();
})
.catch(error => {
expect(error.code).toEqual(142);
done();
});
});
});
it('signup should succeed if password conforms to both validatorPattern and validatorCallback', done => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validatorPattern: /[A-Z]+/, // password should contain at least one digit
validatorCallback: () => true,
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('oneUpper');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
Parse.User.logOut()
.then(() => {
Parse.User.logIn('user1', 'oneUpper')
.then(function () {
done();
})
.catch(err => {
jfail(err);
fail('Should be able to login');
done();
});
})
.catch(error => {
jfail(error);
fail('logout should have succeeded');
done();
});
})
.catch(error => {
jfail(error);
fail('Should have succeeded as password conforms to the policy.');
done();
});
});
});
it('should reset password if new password conforms to password policy', done => {
const user = new Parse.User();
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: options => {
request({
url: options.link,
followRedirects: false,
simple: false,
resolveWithFullResponse: true,
})
.then(response => {
expect(response.status).toEqual(200);
const re = /name="token"[^>]*value="([^"]+)"/;
const match = response.text.match(re);
if (!match) {
fail('should have a token');
done();
return;
}
const token = match[1];
request({
method: 'POST',
url: 'http://localhost:8378/1/apps/test/request_password_reset',
body: `new_password=has2init&token=${token}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
followRedirects: false,
simple: false,
resolveWithFullResponse: true,
})
.then(response => {
expect(response.status).toEqual(200);
expect(response.text).toContain('Success!');
Parse.User.logIn('user1', 'has2init')
.then(function () {
done();
})
.catch(err => {
jfail(err);
fail('should login with new password');
done();
});
})
.catch(error => {
jfail(error);
fail('Failed to POST request password reset');
done();
});
})
.catch(error => {
jfail(error);
fail('Failed to get the reset link');
done();
});
},
sendMail: () => {},
};
reconfigureServer({
appName: 'passwordPolicy',
verifyUserEmails: false,
emailAdapter: emailAdapter,
passwordPolicy: {
validatorPattern: /[0-9]+/, // password should contain at least one digit
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('has 1 digit');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
Parse.User.requestPasswordReset('user1@parse.com').catch(err => {
jfail(err);
fail('Reset password request should not fail');
done();
});
})
.catch(error => {
jfail(error);
fail('signUp should not fail');
done();
});
});
});
it('should fail to reset password if the new password does not conform to password policy', done => {
const user = new Parse.User();
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: options => {
request({
url: options.link,
followRedirects: false,
simple: false,
resolveWithFullResponse: true,
})
.then(response => {
expect(response.status).toEqual(200);
const re = /name="token"[^>]*value="([^"]+)"/;
const match = response.text.match(re);
if (!match) {
fail('should have a token');
done();
return;
}
const token = match[1];
request({
method: 'POST',
url: 'http://localhost:8378/1/apps/test/request_password_reset',
body: `new_password=hasnodigit&token=${token}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
followRedirects: false,
simple: false,
resolveWithFullResponse: true,
})
.then(response => {
expect(response.status).toEqual(200);
expect(response.text).toContain('Password should contain at least one digit.');
Parse.User.logIn('user1', 'has 1 digit')
.then(function () {
done();
})
.catch(err => {
jfail(err);
fail('should login with old password');
done();
});
})
.catch(error => {
jfail(error);
fail('Failed to POST request password reset');
done();
});
})
.catch(error => {
jfail(error);
fail('Failed to get the reset link');
done();
});
},
sendMail: () => {},
};
reconfigureServer({
appName: 'passwordPolicy',
verifyUserEmails: false,
emailAdapter: emailAdapter,
passwordPolicy: {
validatorPattern: /[0-9]+/, // password should contain at least one digit
validationError: 'Password should contain at least one digit.',
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('has 1 digit');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
Parse.User.requestPasswordReset('user1@parse.com').catch(err => {
jfail(err);
fail('Reset password request should not fail');
done();
});
})
.catch(error => {
jfail(error);
fail('signUp should not fail');
done();
});
});
});
it('should fail if passwordPolicy.doNotAllowUsername is not a boolean value', done => {
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
doNotAllowUsername: 'no',
},
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
fail('passwordPolicy.doNotAllowUsername type test failed');
done();
})
.catch(err => {
expect(err).toEqual('passwordPolicy.doNotAllowUsername must be a boolean value.');
done();
});
});
it('signup should fail if password contains the username and is not allowed by policy', done => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validatorPattern: /[0-9]+/,
doNotAllowUsername: true,
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('@user11');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
fail('Should have failed as password contains username.');
done();
})
.catch(error => {
expect(error.code).toEqual(142);
expect(error.message).toEqual('Password cannot contain your username.');
done();
});
});
});
it('signup should succeed if password does not contain the username and is not allowed by policy', done => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
doNotAllowUsername: true,
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('r@nd0m');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
done();
})
.catch(() => {
fail('Should have succeeded as password does not contain username.');
done();
});
});
});
it('signup should succeed if password contains the username and it is allowed by policy', done => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validatorPattern: /[0-9]+/,
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('user1');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
done();
})
.catch(() => {
fail('Should have succeeded as policy allows username in password.');
done();
});
});
});
it('should fail to reset password if the new password contains username and not allowed by password policy', done => {
const user = new Parse.User();
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: options => {
request({
url: options.link,
followRedirects: false,
simple: false,
resolveWithFullResponse: true,
})
.then(response => {
expect(response.status).toEqual(200);
const re = /name="token"[^>]*value="([^"]+)"/;
const match = response.text.match(re);
if (!match) {
fail('should have a token');
done();
return;
}
const token = match[1];
request({
method: 'POST',
url: 'http://localhost:8378/1/apps/test/request_password_reset',
body: `new_password=xuser12&token=${token}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
followRedirects: false,
simple: false,
resolveWithFullResponse: true,
})
.then(response => {
expect(response.status).toEqual(200);
expect(response.text).toContain('Password cannot contain your username.');
Parse.User.logIn('user1', 'r@nd0m')
.then(function () {
done();
})
.catch(err => {
jfail(err);
fail('should login with old password');
done();
});
})
.catch(error => {
jfail(error);
fail('Failed to POST request password reset');
done();
});
})
.catch(error => {
jfail(error);
fail('Failed to get the reset link');
done();
});
},
sendMail: () => {},
};
reconfigureServer({
appName: 'passwordPolicy',
verifyUserEmails: false,
emailAdapter: emailAdapter,
passwordPolicy: {
doNotAllowUsername: true,
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('r@nd0m');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
Parse.User.requestPasswordReset('user1@parse.com').catch(err => {
jfail(err);
fail('Reset password request should not fail');
done();
});
})
.catch(error => {
jfail(error);
fail('signUp should not fail');
done();
});
});
});
it('Should return error when password violates Password Policy and reset through ajax', async done => {
const user = new Parse.User();
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: async options => {
const response = await request({
url: options.link,
followRedirects: false,
simple: false,
resolveWithFullResponse: true,
});
expect(response.status).toEqual(200);
const re = /name="token"[^>]*value="([^"]+)"/;
const match = response.text.match(re);
if (!match) {
fail('should have a token');
return;
}
const token = match[1];
try {
await request({
method: 'POST',
url: 'http://localhost:8378/1/apps/test/request_password_reset',
body: `new_password=xuser12&token=${token}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest',
},
followRedirects: false,
});
} catch (error) {
expect(error.status).not.toBe(302);
expect(error.text).toEqual(
'{"code":-1,"error":"Password cannot contain your username."}'
);
}
await Parse.User.logIn('user1', 'r@nd0m');
done();
},
sendMail: () => {},
};
await reconfigureServer({
appName: 'passwordPolicy',
verifyUserEmails: false,
emailAdapter: emailAdapter,
passwordPolicy: {
doNotAllowUsername: true,
},
publicServerURL: 'http://localhost:8378/1',
});
user.setUsername('user1');
user.setPassword('r@nd0m');
user.set('email', 'user1@parse.com');
await user.signUp();
await Parse.User.requestPasswordReset('user1@parse.com');
});
it('should reset password even if the new password contains user name while the policy allows', done => {
const user = new Parse.User();
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: options => {
request({
url: options.link,
followRedirects: false,
simple: false,
resolveWithFullResponse: true,
})
.then(response => {
expect(response.status).toEqual(200);
const re = /name="token"[^>]*value="([^"]+)"/;
const match = response.text.match(re);
if (!match) {
fail('should have a token');
done();
return;
}
const token = match[1];
request({
method: 'POST',
url: 'http://localhost:8378/1/apps/test/request_password_reset',
body: `new_password=uuser11&token=${token}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
followRedirects: false,
simple: false,
resolveWithFullResponse: true,
})
.then(response => {
expect(response.status).toEqual(200);
expect(response.text).toContain('Success!');
Parse.User.logIn('user1', 'uuser11')
.then(function () {
done();
})
.catch(err => {
jfail(err);
fail('should login with new password');
done();
});
})
.catch(error => {
jfail(error);
fail('Failed to POST request password reset');
});
})
.catch(error => {
jfail(error);
fail('Failed to get the reset link');
});
},
sendMail: () => {},
};
reconfigureServer({
appName: 'passwordPolicy',
verifyUserEmails: false,
emailAdapter: emailAdapter,
passwordPolicy: {
validatorPattern: /[0-9]+/,
doNotAllowUsername: false,
},
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
user.setUsername('user1');
user.setPassword('has 1 digit');
user.set('email', 'user1@parse.com');
user.signUp().then(() => {
Parse.User.requestPasswordReset('user1@parse.com').catch(err => {
jfail(err);
fail('Reset password request should not fail');
done();
});
});
})
.catch(error => {
jfail(error);
fail('signUp should not fail');
done();
});
});
it('should fail if passwordPolicy.maxPasswordAge is not a number', done => {
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
maxPasswordAge: 'not a number',
},
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
fail('passwordPolicy.maxPasswordAge "not a number" test failed');
done();
})
.catch(err => {
expect(err).toEqual('passwordPolicy.maxPasswordAge must be a positive number');
done();
});
});
it('should fail if passwordPolicy.maxPasswordAge is a negative number', done => {
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
maxPasswordAge: -100,
},
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
fail('passwordPolicy.maxPasswordAge negative number test failed');
done();
})
.catch(err => {
expect(err).toEqual('passwordPolicy.maxPasswordAge must be a positive number');
done();
});
});
it_id('d7d0a93e-efe6-48c0-b622-0f7fb570ccc1')(it)('should succeed if logged in before password expires', done => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
maxPasswordAge: 1, // 1 day
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('user1');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
Parse.User.logIn('user1', 'user1')
.then(() => {
done();
})
.catch(error => {
jfail(error);
fail('Login should have succeeded before password expiry.');
done();
});
})
.catch(error => {
jfail(error);
fail('Signup failed.');
done();
});
});
});
it_id('22428408-8763-445d-9833-2b2d92008f62')(it)('should fail if logged in after password expires', done => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
maxPasswordAge: 0.5 / (24 * 60 * 60), // 0.5 sec
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('user1');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
// wait for a bit more than the validity duration set
setTimeout(() => {
Parse.User.logIn('user1', 'user1')
.then(() => {
fail('logIn should have failed');
done();
})
.catch(error => {
expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
expect(error.message).toEqual(
'Your password has expired. Please reset your password.'
);
done();
});
}, 1000);
})
.catch(error => {
jfail(error);
fail('Signup failed.');
done();
});
});
});
it_id('cc97a109-e35f-4f94-b942-3a6134921cdd')(it)('should apply password expiry policy to existing user upon first login after policy is enabled', done => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('user1');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
Parse.User.logOut()
.then(() => {
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
maxPasswordAge: 0.5 / (24 * 60 * 60), // 0.5 sec
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
Parse.User.logIn('user1', 'user1')
.then(() => {
Parse.User.logOut()
.then(() => {
// wait for a bit more than the validity duration set
setTimeout(() => {
Parse.User.logIn('user1', 'user1')
.then(() => {
fail('logIn should have failed');
done();
})
.catch(error => {
expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
expect(error.message).toEqual(
'Your password has expired. Please reset your password.'
);
done();
});
}, 2000);
})
.catch(error => {
jfail(error);
fail('logout should have succeeded');
done();
});
})
.catch(error => {
jfail(error);
fail('Login failed.');
done();
});
});
})
.catch(error => {
jfail(error);
fail('logout should have succeeded');
done();
});
})
.catch(error => {
jfail(error);
fail('Signup failed.');
done();
});
});
});
it_id('d1e6ab9d-c091-4fea-b952-08b7484bfc89')(it)('should reset password timestamp when password is reset', done => {
const user = new Parse.User();
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: options => {
request({
url: options.link,
followRedirects: false,
simple: false,
resolveWithFullResponse: true,
})
.then(response => {
expect(response.status).toEqual(200);
const re = /name="token"[^>]*value="([^"]+)"/;
const match = response.text.match(re);
if (!match) {
fail('should have a token');
done();
return;
}
const token = match[1];
request({
method: 'POST',
url: 'http://localhost:8378/1/apps/test/request_password_reset',
body: `new_password=uuser11&token=${token}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
followRedirects: false,
simple: false,
resolveWithFullResponse: true,
})
.then(response => {
expect(response.status).toEqual(200);
expect(response.text).toContain('Success!');
Parse.User.logIn('user1', 'uuser11')
.then(function () {
done();
})
.catch(err => {
jfail(err);
fail('should login with new password');
done();
});
})
.catch(error => {
jfail(error);
fail('Failed to POST request password reset');
});
})
.catch(error => {
jfail(error);
fail('Failed to get the reset link');
});
},
sendMail: () => {},
};
reconfigureServer({
appName: 'passwordPolicy',
emailAdapter: emailAdapter,
passwordPolicy: {
maxPasswordAge: 0.5 / (24 * 60 * 60), // 0.5 sec
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('user1');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
// wait for a bit more than the validity duration set
setTimeout(() => {
Parse.User.logIn('user1', 'user1')
.then(() => {
fail('logIn should have failed');
done();
})
.catch(error => {
expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
expect(error.message).toEqual(
'Your password has expired. Please reset your password.'
);
Parse.User.requestPasswordReset('user1@parse.com').catch(err => {
jfail(err);
fail('Reset password request should not fail');
done();
});
});
}, 1000);
})
.catch(error => {
jfail(error);
fail('Signup failed.');
done();
});
});
});
it('should fail if passwordPolicy.maxPasswordHistory is not a number', done => {
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
maxPasswordHistory: 'not a number',
},
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
fail('passwordPolicy.maxPasswordHistory "not a number" test failed');
done();
})
.catch(err => {
expect(err).toEqual('passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20');
done();
});
});
it('should fail if passwordPolicy.maxPasswordHistory is a negative number', done => {
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
maxPasswordHistory: -10,
},
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
fail('passwordPolicy.maxPasswordHistory negative number test failed');
done();
})
.catch(err => {
expect(err).toEqual('passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20');
done();
});
});
it('should fail if passwordPolicy.maxPasswordHistory is greater than 20', done => {
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
maxPasswordHistory: 21,
},
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
fail('passwordPolicy.maxPasswordHistory negative number test failed');
done();
})
.catch(err => {
expect(err).toEqual('passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20');
done();
});
});
it('should fail to reset if the new password is same as the last password', done => {
const user = new Parse.User();
const emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: options => {
request({
url: options.link,
followRedirects: false,
})
.then(response => {
expect(response.status).toEqual(200);
const re = /name="token"[^>]*value="([^"]+)"/;
const match = response.text.match(re);
if (!match) {
fail('should have a token');
return Promise.reject('Invalid password link');
}
return Promise.resolve(match[1]); // token
})
.then(token => {
return request({
method: 'POST',
url: 'http://localhost:8378/1/apps/test/request_password_reset',
body: `new_password=user1&token=${token}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
followRedirects: false,
simple: false,
resolveWithFullResponse: true,
}).then(response => {
return [response, token];
});
})
.then(data => {
const response = data[0];
const token = data[1];
expect(response.status).toEqual(200);
expect(response.text).toContain('New password should not be the same as last 1 passwords.');
done();
return Promise.resolve();
})
.catch(error => {
fail(error);
fail('Repeat password test failed');
done();
});
},
sendMail: () => {},
};
reconfigureServer({
appName: 'passwordPolicy',
verifyUserEmails: false,
emailAdapter: emailAdapter,
passwordPolicy: {
maxPasswordHistory: 1,
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('user1');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
return Parse.User.logOut();
})
.then(() => {
return Parse.User.requestPasswordReset('user1@parse.com');
})
.catch(error => {
jfail(error);
fail('SignUp or reset request failed');
done();
});
});
});
it('should fail if the new password is same as the previous one', done => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
verifyUserEmails: false,
passwordPolicy: {
maxPasswordHistory: 5,
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('user1');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
// try to set the same password as the previous one
user.setPassword('user1');
return user.save();
})
.then(() => {
fail('should have failed because the new password is same as the old');
done();
})
.catch(error => {
expect(error.message).toEqual('New password should not be the same as last 5 passwords.');
expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR);
done();
});
});
});
it('should fail if the new password is same as the 5th oldest one and policy does not allow the previous 5', done => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
verifyUserEmails: false,
passwordPolicy: {
maxPasswordHistory: 5,
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('user1');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
// build history
user.setPassword('user2');
return user.save();
})
.then(() => {
user.setPassword('user3');
return user.save();
})
.then(() => {
user.setPassword('user4');
return user.save();
})
.then(() => {
user.setPassword('user5');
return user.save();
})
.then(() => {
// set the same password as the initial one
user.setPassword('user1');
return user.save();
})
.then(() => {
fail('should have failed because the new password is same as the old');
done();
})
.catch(error => {
expect(error.message).toEqual('New password should not be the same as last 5 passwords.');
expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR);
done();
});
});
});
it('should succeed if the new password is same as the 6th oldest one and policy does not allow only previous 5', done => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
verifyUserEmails: false,
passwordPolicy: {
maxPasswordHistory: 5,
},
publicServerURL: 'http://localhost:8378/1',
}).then(() => {
user.setUsername('user1');
user.setPassword('user1');
user.set('email', 'user1@parse.com');
user
.signUp()
.then(() => {
// build history
user.setPassword('user2');
return user.save();
})
.then(() => {
user.setPassword('user3');
return user.save();
})
.then(() => {
user.setPassword('user4');
return user.save();
})
.then(() => {
user.setPassword('user5');
return user.save();
})
.then(() => {
user.setPassword('user6'); // this pushes initial password out of history
return user.save();
})
.then(() => {
// set the same password as the initial one
user.setPassword('user1');
return user.save();
})
.then(() => {
done();
})
.catch(() => {
fail('should have succeeded because the new password is not in history');
done();
});
});
});
it('should not infinitely loop if maxPasswordHistory is 1 (#4918)', async () => {
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-Rest-API-Key': 'test',
'X-Parse-Maintenance-Key': 'test2',
'Content-Type': 'application/json',
};
const user = new Parse.User();
const query = new Parse.Query(Parse.User);
await reconfigureServer({
appName: 'passwordPolicy',
verifyUserEmails: false,
maintenanceKey: 'test2',
passwordPolicy: {
maxPasswordHistory: 1,
},
publicServerURL: 'http://localhost:8378/1',
});
user.setUsername('user1');
user.setPassword('user1');
user.set('email', 'user1@parse.com');
await user.signUp();
user.setPassword('user2');
await user.save();
const user1 = await query.get(user.id, { useMasterKey: true });
expect(user1.get('_password_history')).toBeUndefined();
const result1 = await request({
method: 'GET',
url: `http://localhost:8378/1/classes/_User/${user.id}`,
json: true,
headers,
}).then(res => res.data);
expect(result1._password_history.length).toBe(1);
user.setPassword('user3');
await user.save();
const result2 = await request({
method: 'GET',
url: `http://localhost:8378/1/classes/_User/${user.id}`,
json: true,
headers,
}).then(res => res.data);
expect(result2._password_history.length).toBe(1);
expect(result1._password_history).not.toEqual(result2._password_history);
});
});