Adds password expiry support to password policy (#3068)
* Adding support for password expiry policy * Renamed daysBeforeExpiry -> maxPasswordAge
This commit is contained in:
committed by
Diwakar Cherukumilli
parent
11c6170ed1
commit
edb7b70ced
@@ -287,6 +287,7 @@ var server = ParseServer({
|
||||
// 2. a callback function to be invoked to validate the password
|
||||
validatorCallback: (password) => { return validatePassword(password) },
|
||||
doNotAllowUsername: true, // optional setting to disallow username in passwords
|
||||
maxPasswordAge: 90, // optional setting in days for password expiry. Login fails if user does not reset the password within this period after signup/last reset.
|
||||
//optional setting to set a validity duration for password reset links (in seconds)
|
||||
resetTokenValidityDuration: 24*60*60, // expire after 24 hours
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ describe("Password Policy: ", () => {
|
||||
},
|
||||
sendMail: () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
emailAdapter: emailAdapter,
|
||||
@@ -29,7 +29,11 @@ describe("Password Policy: ", () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
}).then(user => {
|
||||
Parse.User.requestPasswordReset("user@parse.com");
|
||||
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(() => {
|
||||
@@ -64,7 +68,7 @@ describe("Password Policy: ", () => {
|
||||
},
|
||||
sendMail: () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
emailAdapter: emailAdapter,
|
||||
@@ -78,7 +82,11 @@ describe("Password Policy: ", () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
}).then(user => {
|
||||
Parse.User.requestPasswordReset("user@parse.com");
|
||||
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(() => {
|
||||
@@ -190,7 +198,7 @@ describe("Password Policy: ", () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('signup should succeed if password confirms to the policy enforced using validatorPattern', (done) => {
|
||||
it('signup should succeed if password conforms to the policy enforced using validatorPattern', (done) => {
|
||||
const user = new Parse.User();
|
||||
reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
@@ -203,16 +211,22 @@ describe("Password Policy: ", () => {
|
||||
user.setPassword("1digit");
|
||||
user.set('email', 'user1@parse.com');
|
||||
user.signUp().then(() => {
|
||||
Parse.User.logOut();
|
||||
Parse.User.logIn("user1", "1digit").then(function (user) {
|
||||
done();
|
||||
}).catch((err) => {
|
||||
jfail(err);
|
||||
fail("Should be able to login");
|
||||
Parse.User.logOut().then(() => {
|
||||
Parse.User.logIn("user1", "1digit").then(function (user) {
|
||||
done();
|
||||
}).catch((err) => {
|
||||
jfail(err);
|
||||
fail("Should be able to login");
|
||||
done();
|
||||
});
|
||||
}).catch((error) => {
|
||||
jfail(error);
|
||||
fail('logout should have succeeded');
|
||||
done();
|
||||
});
|
||||
}).catch((error) => {
|
||||
fail('Should have succeeded as password confirms to the policy.');
|
||||
jfail(error);
|
||||
fail('Signup should have succeeded as password conforms to the policy.');
|
||||
done();
|
||||
});
|
||||
})
|
||||
@@ -240,7 +254,7 @@ describe("Password Policy: ", () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('signup should succeed if password confirms to the policy enforced using validatorCallback', (done) => {
|
||||
it('signup should succeed if password conforms to the policy enforced using validatorCallback', (done) => {
|
||||
const user = new Parse.User();
|
||||
reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
@@ -253,16 +267,22 @@ describe("Password Policy: ", () => {
|
||||
user.setPassword("oneUpper");
|
||||
user.set('email', 'user1@parse.com');
|
||||
user.signUp().then(() => {
|
||||
Parse.User.logOut();
|
||||
Parse.User.logIn("user1", "oneUpper").then(function (user) {
|
||||
done();
|
||||
}).catch((err) => {
|
||||
jfail(err);
|
||||
fail("Should be able to login");
|
||||
Parse.User.logOut().then(() => {
|
||||
Parse.User.logIn("user1", "oneUpper").then(function (user) {
|
||||
done();
|
||||
}).catch((err) => {
|
||||
jfail(err);
|
||||
fail("Should be able to login");
|
||||
done();
|
||||
});
|
||||
}).catch(error => {
|
||||
jfail(error);
|
||||
fail("Logout should have succeeded");
|
||||
done();
|
||||
});
|
||||
}).catch((error) => {
|
||||
fail('Should have succeeded as password confirms to the policy.');
|
||||
jfail(error);
|
||||
fail('Should have succeeded as password conforms to the policy.');
|
||||
done();
|
||||
});
|
||||
})
|
||||
@@ -291,7 +311,7 @@ describe("Password Policy: ", () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('signup should fail if password does confirms to validatorPattern but fails validatorCallback', (done) => {
|
||||
it('signup should fail if password matches validatorPattern but fails validatorCallback', (done) => {
|
||||
const user = new Parse.User();
|
||||
reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
@@ -314,7 +334,7 @@ describe("Password Policy: ", () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('signup should succeed if password confirms to both validatorPattern and validatorCallback', (done) => {
|
||||
it('signup should succeed if password conforms to both validatorPattern and validatorCallback', (done) => {
|
||||
const user = new Parse.User();
|
||||
reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
@@ -328,24 +348,30 @@ describe("Password Policy: ", () => {
|
||||
user.setPassword("oneUpper");
|
||||
user.set('email', 'user1@parse.com');
|
||||
user.signUp().then(() => {
|
||||
Parse.User.logOut();
|
||||
Parse.User.logIn("user1", "oneUpper").then(function (user) {
|
||||
done();
|
||||
}).catch((err) => {
|
||||
jfail(err);
|
||||
fail("Should be able to login");
|
||||
Parse.User.logOut().then(() => {
|
||||
Parse.User.logIn("user1", "oneUpper").then(function (user) {
|
||||
done();
|
||||
}).catch((err) => {
|
||||
jfail(err);
|
||||
fail("Should be able to login");
|
||||
done();
|
||||
});
|
||||
}).catch(error => {
|
||||
jfail(error);
|
||||
fail("logout should have succeeded");
|
||||
done();
|
||||
});
|
||||
}).catch((error) => {
|
||||
fail('Should have succeeded as password confirms to the policy.');
|
||||
jfail(error);
|
||||
fail('Should have succeeded as password conforms to the policy.');
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('should reset password if new password confirms to password policy', done => {
|
||||
var user = new Parse.User();
|
||||
var emailAdapter = {
|
||||
it('should reset password if new password conforms to password policy', done => {
|
||||
const user = new Parse.User();
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: options => {
|
||||
requestp.get({
|
||||
@@ -355,14 +381,14 @@ describe("Password Policy: ", () => {
|
||||
resolveWithFullResponse: true
|
||||
}).then((response) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
var re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
var match = response.body.match(re);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
const match = response.body.match(re);
|
||||
if (!match) {
|
||||
fail("should have a token");
|
||||
done();
|
||||
return;
|
||||
}
|
||||
var token = match[1];
|
||||
const token = match[1];
|
||||
|
||||
requestp.post({
|
||||
uri: "http://localhost:8378/1/apps/test/request_password_reset",
|
||||
@@ -397,7 +423,7 @@ describe("Password Policy: ", () => {
|
||||
},
|
||||
sendMail: () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
verifyUserEmails: false,
|
||||
@@ -411,12 +437,10 @@ describe("Password Policy: ", () => {
|
||||
user.setPassword("has 1 digit");
|
||||
user.set('email', 'user1@parse.com');
|
||||
user.signUp().then(() => {
|
||||
Parse.User.requestPasswordReset('user1@parse.com', {
|
||||
error: (err) => {
|
||||
jfail(err);
|
||||
fail("Reset password request should not fail");
|
||||
done();
|
||||
}
|
||||
Parse.User.requestPasswordReset('user1@parse.com').catch((err) => {
|
||||
jfail(err);
|
||||
fail("Reset password request should not fail");
|
||||
done();
|
||||
});
|
||||
}).catch(error => {
|
||||
jfail(error);
|
||||
@@ -427,8 +451,8 @@ describe("Password Policy: ", () => {
|
||||
});
|
||||
|
||||
it('should fail to reset password if the new password does not conform to password policy', done => {
|
||||
var user = new Parse.User();
|
||||
var emailAdapter = {
|
||||
const user = new Parse.User();
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: options => {
|
||||
requestp.get({
|
||||
@@ -438,14 +462,14 @@ describe("Password Policy: ", () => {
|
||||
resolveWithFullResponse: true
|
||||
}).then((response) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
var re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
var match = response.body.match(re);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
const match = response.body.match(re);
|
||||
if (!match) {
|
||||
fail("should have a token");
|
||||
done();
|
||||
return;
|
||||
}
|
||||
var token = match[1];
|
||||
const token = match[1];
|
||||
|
||||
requestp.post({
|
||||
uri: "http://localhost:8378/1/apps/test/request_password_reset",
|
||||
@@ -458,7 +482,7 @@ describe("Password Policy: ", () => {
|
||||
resolveWithFullResponse: true
|
||||
}).then((response) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual(`Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20does%20not%20confirm%20to%20the%20Password%20Policy.&app=passwordPolicy`);
|
||||
expect(response.body).toEqual(`Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20does%20not%20meet%20the%20Password%20Policy%20requirements.&app=passwordPolicy`);
|
||||
|
||||
Parse.User.logIn("user1", "has 1 digit").then(function (user) {
|
||||
done();
|
||||
@@ -480,7 +504,7 @@ describe("Password Policy: ", () => {
|
||||
},
|
||||
sendMail: () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
verifyUserEmails: false,
|
||||
@@ -494,12 +518,10 @@ describe("Password Policy: ", () => {
|
||||
user.setPassword("has 1 digit");
|
||||
user.set('email', 'user1@parse.com');
|
||||
user.signUp().then(() => {
|
||||
Parse.User.requestPasswordReset('user1@parse.com', {
|
||||
error: (err) => {
|
||||
jfail(err);
|
||||
fail("Reset password request should not fail");
|
||||
done();
|
||||
}
|
||||
Parse.User.requestPasswordReset('user1@parse.com').catch((err) => {
|
||||
jfail(err);
|
||||
fail("Reset password request should not fail");
|
||||
done();
|
||||
});
|
||||
}).catch(error => {
|
||||
jfail(error);
|
||||
@@ -591,8 +613,8 @@ describe("Password Policy: ", () => {
|
||||
});
|
||||
|
||||
it('should fail to reset password if the new password contains username and not allowed by password policy', done => {
|
||||
var user = new Parse.User();
|
||||
var emailAdapter = {
|
||||
const user = new Parse.User();
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: options => {
|
||||
requestp.get({
|
||||
@@ -602,14 +624,14 @@ describe("Password Policy: ", () => {
|
||||
resolveWithFullResponse: true
|
||||
}).then((response) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
var re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
var match = response.body.match(re);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
const match = response.body.match(re);
|
||||
if (!match) {
|
||||
fail("should have a token");
|
||||
done();
|
||||
return;
|
||||
}
|
||||
var token = match[1];
|
||||
const token = match[1];
|
||||
|
||||
requestp.post({
|
||||
uri: "http://localhost:8378/1/apps/test/request_password_reset",
|
||||
@@ -622,7 +644,7 @@ describe("Password Policy: ", () => {
|
||||
resolveWithFullResponse: true
|
||||
}).then((response) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual(`Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20does%20not%20confirm%20to%20the%20Password%20Policy.&app=passwordPolicy`);
|
||||
expect(response.body).toEqual(`Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20does%20not%20meet%20the%20Password%20Policy%20requirements.&app=passwordPolicy`);
|
||||
|
||||
Parse.User.logIn("user1", "r@nd0m").then(function (user) {
|
||||
done();
|
||||
@@ -645,7 +667,7 @@ describe("Password Policy: ", () => {
|
||||
},
|
||||
sendMail: () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
verifyUserEmails: false,
|
||||
@@ -659,12 +681,10 @@ describe("Password Policy: ", () => {
|
||||
user.setPassword("r@nd0m");
|
||||
user.set('email', 'user1@parse.com');
|
||||
user.signUp().then(() => {
|
||||
Parse.User.requestPasswordReset('user1@parse.com', {
|
||||
error: (err) => {
|
||||
jfail(err);
|
||||
fail("Reset password request should not fail");
|
||||
done();
|
||||
}
|
||||
Parse.User.requestPasswordReset('user1@parse.com').catch((err) => {
|
||||
jfail(err);
|
||||
fail("Reset password request should not fail");
|
||||
done();
|
||||
});
|
||||
}).catch(error => {
|
||||
jfail(error);
|
||||
@@ -675,8 +695,8 @@ describe("Password Policy: ", () => {
|
||||
});
|
||||
|
||||
it('should reset password even if the new password contains user name while the policy allows', done => {
|
||||
var user = new Parse.User();
|
||||
var emailAdapter = {
|
||||
const user = new Parse.User();
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: options => {
|
||||
requestp.get({
|
||||
@@ -686,14 +706,14 @@ describe("Password Policy: ", () => {
|
||||
resolveWithFullResponse: true
|
||||
}).then(response => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
var re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
var match = response.body.match(re);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
const match = response.body.match(re);
|
||||
if (!match) {
|
||||
fail("should have a token");
|
||||
done();
|
||||
return;
|
||||
}
|
||||
var token = match[1];
|
||||
const token = match[1];
|
||||
|
||||
requestp.post({
|
||||
uri: "http://localhost:8378/1/apps/test/request_password_reset",
|
||||
@@ -727,7 +747,7 @@ describe("Password Policy: ", () => {
|
||||
},
|
||||
sendMail: () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
verifyUserEmails: false,
|
||||
@@ -742,16 +762,250 @@ describe("Password Policy: ", () => {
|
||||
user.setPassword("has 1 digit");
|
||||
user.set('email', 'user1@parse.com');
|
||||
user.signUp().then(() => {
|
||||
Parse.User.requestPasswordReset('user1@parse.com', {
|
||||
error: (err) => {
|
||||
jfail(err);
|
||||
fail("Reset password request should not fail");
|
||||
done();
|
||||
}
|
||||
Parse.User.requestPasswordReset('user1@parse.com').catch((err) => {
|
||||
jfail(err);
|
||||
fail("Reset password request should not fail");
|
||||
done();
|
||||
});
|
||||
}).catch(error => {
|
||||
});
|
||||
}).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('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((u) => {
|
||||
Parse.User.logIn("user1", "user1").then((user) => {
|
||||
done();
|
||||
}).catch((error) => {
|
||||
jfail(error);
|
||||
fail('Login should have succeeded before password expiry.');
|
||||
done();
|
||||
});
|
||||
}).catch((error) => {
|
||||
jfail(error);
|
||||
fail("signUp should not fail");
|
||||
fail('Signup failed.');
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
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('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((u) => {
|
||||
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('should reset password timestamp when password is reset', done => {
|
||||
const user = new Parse.User();
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: options => {
|
||||
requestp.get({
|
||||
uri: options.link,
|
||||
followRedirect: false,
|
||||
simple: false,
|
||||
resolveWithFullResponse: true
|
||||
}).then(response => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
const match = response.body.match(re);
|
||||
if (!match) {
|
||||
fail("should have a token");
|
||||
done();
|
||||
return;
|
||||
}
|
||||
const token = match[1];
|
||||
|
||||
requestp.post({
|
||||
uri: "http://localhost:8378/1/apps/test/request_password_reset",
|
||||
body: `new_password=uuser11&token=${token}&username=user1`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
followRedirect: false,
|
||||
simple: false,
|
||||
resolveWithFullResponse: true
|
||||
}).then(response => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1');
|
||||
|
||||
Parse.User.logIn("user1", "uuser11").then(function (user) {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,6 +70,10 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc
|
||||
key = '_perishable_token_expires_at';
|
||||
timeField = true;
|
||||
break;
|
||||
case '_password_changed_at':
|
||||
key = '_password_changed_at';
|
||||
timeField = true;
|
||||
break;
|
||||
case '_rperm':
|
||||
case '_wperm':
|
||||
return {key: key, value: restValue};
|
||||
@@ -180,6 +184,11 @@ function transformQueryKeyValue(className, key, value, schema) {
|
||||
return { key: '_perishable_token_expires_at', value: valueAsDate(value) }
|
||||
}
|
||||
break;
|
||||
case '_password_changed_at':
|
||||
if (valueAsDate(value)) {
|
||||
return { key: '_password_changed_at', value: valueAsDate(value) }
|
||||
}
|
||||
break;
|
||||
case '_rperm':
|
||||
case '_wperm':
|
||||
case '_perishable_token':
|
||||
@@ -263,6 +272,10 @@ const parseObjectKeyValueToMongoObjectKeyValue = (restKey, restValue, schema) =>
|
||||
transformedValue = transformTopLevelAtom(restValue);
|
||||
coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue
|
||||
return { key: '_perishable_token_expires_at', value: coercedToDate };
|
||||
case '_password_changed_at':
|
||||
transformedValue = transformTopLevelAtom(restValue);
|
||||
coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue
|
||||
return { key: '_password_changed_at', value: coercedToDate };
|
||||
case '_failed_login_count':
|
||||
case '_rperm':
|
||||
case '_wperm':
|
||||
@@ -768,6 +781,7 @@ const mongoObjectToParseObject = (className, mongoObject, schema) => {
|
||||
case '_email_verify_token':
|
||||
case '_perishable_token':
|
||||
case '_perishable_token_expires_at':
|
||||
case '_password_changed_at':
|
||||
case '_tombstone':
|
||||
case '_email_verify_token_expires_at':
|
||||
case '_account_lockout_expires_at':
|
||||
|
||||
@@ -467,6 +467,7 @@ export class PostgresStorageAdapter {
|
||||
fields._failed_login_count = {type: 'Number'};
|
||||
fields._perishable_token = {type: 'String'};
|
||||
fields._perishable_token_expires_at = {type: 'Date'};
|
||||
fields._password_changed_at = {type: 'Date'};
|
||||
}
|
||||
let index = 2;
|
||||
let relations = [];
|
||||
@@ -693,7 +694,8 @@ export class PostgresStorageAdapter {
|
||||
}
|
||||
|
||||
if (fieldName === '_account_lockout_expires_at'||
|
||||
fieldName === '_perishable_token_expires_at') {
|
||||
fieldName === '_perishable_token_expires_at'||
|
||||
fieldName === '_password_changed_at') {
|
||||
if (object[fieldName]) {
|
||||
valuesArray.push(object[fieldName].iso);
|
||||
} else {
|
||||
@@ -1075,7 +1077,9 @@ export class PostgresStorageAdapter {
|
||||
if (object._perishable_token_expires_at) {
|
||||
object._perishable_token_expires_at = { __type: 'Date', iso: object._perishable_token_expires_at.toISOString() };
|
||||
}
|
||||
|
||||
if (object._password_changed_at) {
|
||||
object._password_changed_at = { __type: 'Date', iso: object._password_changed_at.toISOString() };
|
||||
}
|
||||
|
||||
for (let fieldName in object) {
|
||||
if (object[fieldName] === null) {
|
||||
|
||||
@@ -119,6 +119,10 @@ export class Config {
|
||||
|
||||
static validatePasswordPolicy(passwordPolicy) {
|
||||
if (passwordPolicy) {
|
||||
if (passwordPolicy.maxPasswordAge !== undefined && (typeof passwordPolicy.maxPasswordAge !== 'number' || passwordPolicy.maxPasswordAge < 0)) {
|
||||
throw 'passwordPolicy.maxPasswordAge must be a positive number';
|
||||
}
|
||||
|
||||
if (passwordPolicy.resetTokenValidityDuration !== undefined && (typeof passwordPolicy.resetTokenValidityDuration !== 'number' || passwordPolicy.resetTokenValidityDuration <= 0)) {
|
||||
throw 'passwordPolicy.resetTokenValidityDuration must be a positive number';
|
||||
}
|
||||
|
||||
@@ -174,6 +174,7 @@ const filterSensitiveData = (isMaster, aclGroup, className, object) => {
|
||||
delete object._email_verify_token_expires_at;
|
||||
delete object._failed_login_count;
|
||||
delete object._account_lockout_expires_at;
|
||||
delete object._password_changed_at;
|
||||
|
||||
if ((aclGroup.indexOf(object.objectId) > -1)) {
|
||||
return object;
|
||||
@@ -190,7 +191,7 @@ const filterSensitiveData = (isMaster, aclGroup, className, object) => {
|
||||
// acl: a list of strings. If the object to be updated has an ACL,
|
||||
// one of the provided strings must provide the caller with
|
||||
// write permissions.
|
||||
const specialKeysForUpdate = ['_hashed_password', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count', '_perishable_token_expires_at'];
|
||||
const specialKeysForUpdate = ['_hashed_password', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count', '_perishable_token_expires_at', '_password_changed_at'];
|
||||
|
||||
const isSpecialUpdateKey = key => {
|
||||
return specialKeysForUpdate.indexOf(key) >= 0;
|
||||
|
||||
@@ -371,11 +371,11 @@ RestWrite.prototype.transformUser = function() {
|
||||
|
||||
let defer = Promise.resolve();
|
||||
|
||||
// check if the password confirms to the defined password policy if configured
|
||||
// check if the password conforms to the defined password policy if configured
|
||||
if (this.config.passwordPolicy) {
|
||||
const policyError = 'Password does not confirm to the Password Policy.';
|
||||
const policyError = 'Password does not meet the Password Policy requirements.';
|
||||
|
||||
// check whether the password confirms to the policy
|
||||
// check whether the password conforms to the policy
|
||||
if (this.config.passwordPolicy.patternValidator && !this.config.passwordPolicy.patternValidator(this.data.password) ||
|
||||
this.config.passwordPolicy.validatorCallback && !this.config.passwordPolicy.validatorCallback(this.data.password)) {
|
||||
return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, policyError));
|
||||
@@ -839,6 +839,10 @@ RestWrite.prototype.runDatabaseOperation = function() {
|
||||
if (this.className === '_User' && this.data.ACL) {
|
||||
this.data.ACL[this.query.objectId] = { read: true, write: true };
|
||||
}
|
||||
// update password timestamp if user password is being changed
|
||||
if (this.className === '_User' && this.data._hashed_password && this.config.passwordPolicy && this.config.passwordPolicy.maxPasswordAge) {
|
||||
this.data._password_changed_at = Parse._encode(new Date());
|
||||
}
|
||||
// Run an update
|
||||
return this.config.database.update(this.className, this.query, this.data, this.runOptions)
|
||||
.then(response => {
|
||||
@@ -847,7 +851,7 @@ RestWrite.prototype.runDatabaseOperation = function() {
|
||||
this.response = { response };
|
||||
});
|
||||
} else {
|
||||
// Set the default ACL for the new _User
|
||||
// Set the default ACL and password timestamp for the new _User
|
||||
if (this.className === '_User') {
|
||||
var ACL = this.data.ACL;
|
||||
// default public r/w ACL
|
||||
@@ -858,6 +862,10 @@ RestWrite.prototype.runDatabaseOperation = function() {
|
||||
// make sure the user is not locked down
|
||||
ACL[this.data.objectId] = { read: true, write: true };
|
||||
this.data.ACL = ACL;
|
||||
// password timestamp to be used when password expiry policy is enforced
|
||||
if (this.config.passwordPolicy && this.config.passwordPolicy.maxPasswordAge) {
|
||||
this.data._password_changed_at = Parse._encode(new Date());
|
||||
}
|
||||
}
|
||||
|
||||
// Run a create
|
||||
|
||||
@@ -105,6 +105,28 @@ export class UsersRouter extends ClassesRouter {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
|
||||
}
|
||||
|
||||
// handle password expiry policy
|
||||
if (req.config.passwordPolicy && req.config.passwordPolicy.maxPasswordAge) {
|
||||
let changedAt = user._password_changed_at;
|
||||
|
||||
if (!changedAt) {
|
||||
// password was created before expiry policy was enabled.
|
||||
// simply update _User object so that it will start enforcing from now
|
||||
changedAt = new Date();
|
||||
req.config.database.update('_User', {username: user.username},
|
||||
{_password_changed_at: Parse._encode(changedAt)});
|
||||
} else {
|
||||
// check whether the password has expired
|
||||
if (changedAt.__type == 'Date') {
|
||||
changedAt = new Date(changedAt.iso);
|
||||
}
|
||||
// Calculate the expiry time.
|
||||
const expiresAt = new Date(changedAt.getTime() + 86400000 * req.config.passwordPolicy.maxPasswordAge);
|
||||
if (expiresAt < new Date()) // fail of current time is past password expiry time
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Your password has expired. Please reset your password.');
|
||||
}
|
||||
}
|
||||
|
||||
let token = 'r:' + cryptoUtils.newToken();
|
||||
user.sessionToken = token;
|
||||
delete user.password;
|
||||
|
||||
Reference in New Issue
Block a user