Adds password history support to passwordPolicy (#3102)
* password history support in passwordPolicy * Refactor RestWrite.transformUser * fix eslint issues
This commit is contained in:
committed by
Diwakar Cherukumilli
parent
08bac9f790
commit
bd1689190f
@@ -288,6 +288,7 @@ var server = ParseServer({
|
|||||||
validatorCallback: (password) => { return validatePassword(password) },
|
validatorCallback: (password) => { return validatePassword(password) },
|
||||||
doNotAllowUsername: true, // optional setting to disallow username in passwords
|
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.
|
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.
|
||||||
|
maxPasswordHistory: 5, // optional setting to prevent reuse of previous n passwords. Maximum value that can be specified is 20. Not specifying it or specifying 0 will not enforce history.
|
||||||
//optional setting to set a validity duration for password reset links (in seconds)
|
//optional setting to set a validity duration for password reset links (in seconds)
|
||||||
resetTokenValidityDuration: 24*60*60, // expire after 24 hours
|
resetTokenValidityDuration: 24*60*60, // expire after 24 hours
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1010,4 +1010,242 @@ describe("Password Policy: ", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 => {
|
||||||
|
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");
|
||||||
|
return Promise.reject("Invalid password link");
|
||||||
|
}
|
||||||
|
return Promise.resolve(match[1]); // token
|
||||||
|
}).then(token => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
requestp.post({
|
||||||
|
uri: "http://localhost:8378/1/apps/test/request_password_reset",
|
||||||
|
body: `new_password=user1&token=${token}&username=user1`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
followRedirect: false,
|
||||||
|
simple: false,
|
||||||
|
resolveWithFullResponse: true
|
||||||
|
}).then(response => {
|
||||||
|
resolve([response, token]);
|
||||||
|
}).catch(error => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).then(data => {
|
||||||
|
const response = data[0];
|
||||||
|
const token = data[1];
|
||||||
|
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=New%20password%20should%20not%20be%20the%20same%20as%20last%201%20passwords.&app=passwordPolicy`);
|
||||||
|
done();
|
||||||
|
return Promise.resolve();
|
||||||
|
}).catch(error => {
|
||||||
|
jfail(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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -787,6 +787,7 @@ const mongoObjectToParseObject = (className, mongoObject, schema) => {
|
|||||||
case '_email_verify_token_expires_at':
|
case '_email_verify_token_expires_at':
|
||||||
case '_account_lockout_expires_at':
|
case '_account_lockout_expires_at':
|
||||||
case '_failed_login_count':
|
case '_failed_login_count':
|
||||||
|
case '_password_history':
|
||||||
// Those keys will be deleted if needed in the DB Controller
|
// Those keys will be deleted if needed in the DB Controller
|
||||||
restObject[key] = mongoObject[key];
|
restObject[key] = mongoObject[key];
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ const toPostgresSchema = (schema) => {
|
|||||||
schema.fields._rperm = {type: 'Array', contents: {type: 'String'}}
|
schema.fields._rperm = {type: 'Array', contents: {type: 'String'}}
|
||||||
if (schema.className === '_User') {
|
if (schema.className === '_User') {
|
||||||
schema.fields._hashed_password = {type: 'String'};
|
schema.fields._hashed_password = {type: 'String'};
|
||||||
|
schema.fields._password_history = {type: 'Array'};
|
||||||
}
|
}
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
@@ -471,6 +472,7 @@ export class PostgresStorageAdapter {
|
|||||||
fields._perishable_token = {type: 'String'};
|
fields._perishable_token = {type: 'String'};
|
||||||
fields._perishable_token_expires_at = {type: 'Date'};
|
fields._perishable_token_expires_at = {type: 'Date'};
|
||||||
fields._password_changed_at = {type: 'Date'};
|
fields._password_changed_at = {type: 'Date'};
|
||||||
|
fields._password_history = { type: 'Array'};
|
||||||
}
|
}
|
||||||
let index = 2;
|
let index = 2;
|
||||||
let relations = [];
|
let relations = [];
|
||||||
@@ -683,7 +685,8 @@ export class PostgresStorageAdapter {
|
|||||||
if (!schema.fields[fieldName] && className === '_User') {
|
if (!schema.fields[fieldName] && className === '_User') {
|
||||||
if (fieldName === '_email_verify_token' ||
|
if (fieldName === '_email_verify_token' ||
|
||||||
fieldName === '_failed_login_count' ||
|
fieldName === '_failed_login_count' ||
|
||||||
fieldName === '_perishable_token') {
|
fieldName === '_perishable_token' ||
|
||||||
|
fieldName === '_password_history'){
|
||||||
valuesArray.push(object[fieldName]);
|
valuesArray.push(object[fieldName]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,10 @@ export class Config {
|
|||||||
if(passwordPolicy.doNotAllowUsername && typeof passwordPolicy.doNotAllowUsername !== 'boolean') {
|
if(passwordPolicy.doNotAllowUsername && typeof passwordPolicy.doNotAllowUsername !== 'boolean') {
|
||||||
throw 'passwordPolicy.doNotAllowUsername must be a boolean value.';
|
throw 'passwordPolicy.doNotAllowUsername must be a boolean value.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (passwordPolicy.maxPasswordHistory && (!Number.isInteger(passwordPolicy.maxPasswordHistory) || passwordPolicy.maxPasswordHistory <= 0 || passwordPolicy.maxPasswordHistory > 20)) {
|
||||||
|
throw 'passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ const filterSensitiveData = (isMaster, aclGroup, className, object) => {
|
|||||||
// acl: a list of strings. If the object to be updated has an ACL,
|
// acl: a list of strings. If the object to be updated has an ACL,
|
||||||
// one of the provided strings must provide the caller with
|
// one of the provided strings must provide the caller with
|
||||||
// write permissions.
|
// 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', '_password_changed_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', '_password_history'];
|
||||||
|
|
||||||
const isSpecialUpdateKey = key => {
|
const isSpecialUpdateKey = key => {
|
||||||
return specialKeysForUpdate.indexOf(key) >= 0;
|
return specialKeysForUpdate.indexOf(key) >= 0;
|
||||||
|
|||||||
260
src/RestWrite.js
260
src/RestWrite.js
@@ -343,61 +343,31 @@ RestWrite.prototype.handleAuthData = function(authData) {
|
|||||||
|
|
||||||
// The non-third-party parts of User transformation
|
// The non-third-party parts of User transformation
|
||||||
RestWrite.prototype.transformUser = function() {
|
RestWrite.prototype.transformUser = function() {
|
||||||
if (this.className !== '_User') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var promise = Promise.resolve();
|
var promise = Promise.resolve();
|
||||||
|
|
||||||
|
if (this.className !== '_User') {
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.query) {
|
if (this.query) {
|
||||||
// If we're updating a _User object, we need to clear out the cache for that user. Find all their
|
// If we're updating a _User object, we need to clear out the cache for that user. Find all their
|
||||||
// session tokens, and remove them from the cache.
|
// session tokens, and remove them from the cache.
|
||||||
promise = new RestQuery(this.config, Auth.master(this.config), '_Session', { user: {
|
promise = new RestQuery(this.config, Auth.master(this.config), '_Session', {
|
||||||
__type: "Pointer",
|
user: {
|
||||||
className: "_User",
|
__type: "Pointer",
|
||||||
objectId: this.objectId(),
|
className: "_User",
|
||||||
}}).execute()
|
objectId: this.objectId(),
|
||||||
.then(results => {
|
}
|
||||||
results.results.forEach(session => this.config.cacheController.user.del(session.sessionToken));
|
}).execute()
|
||||||
});
|
.then(results => {
|
||||||
|
results.results.forEach(session => this.config.cacheController.user.del(session.sessionToken));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise.then(() => {
|
return promise.then(() => {
|
||||||
// Transform the password
|
// Transform the password
|
||||||
if (!this.data.password) {
|
if (!this.data.password) {
|
||||||
return;
|
return Promise.resolve();
|
||||||
}
|
|
||||||
|
|
||||||
let defer = Promise.resolve();
|
|
||||||
|
|
||||||
// check if the password conforms to the defined password policy if configured
|
|
||||||
if (this.config.passwordPolicy) {
|
|
||||||
const policyError = 'Password does not meet the Password Policy requirements.';
|
|
||||||
|
|
||||||
// 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));
|
|
||||||
}
|
|
||||||
|
|
||||||
// check whether password contain username
|
|
||||||
if (this.config.passwordPolicy.doNotAllowUsername === true) {
|
|
||||||
if (this.data.username) { // username is not passed during password reset
|
|
||||||
if (this.data.password.indexOf(this.data.username) >= 0)
|
|
||||||
return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, policyError));
|
|
||||||
|
|
||||||
} else { // retrieve the User object using objectId during password reset
|
|
||||||
defer = this.config.database.find('_User', {objectId: this.objectId()})
|
|
||||||
.then(results => {
|
|
||||||
if (results.length != 1) {
|
|
||||||
throw undefined;
|
|
||||||
}
|
|
||||||
if (this.data.password.indexOf(results[0].username) >= 0)
|
|
||||||
return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, policyError));
|
|
||||||
return Promise.resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.query && !this.auth.isMaster) {
|
if (this.query && !this.auth.isMaster) {
|
||||||
@@ -405,7 +375,7 @@ RestWrite.prototype.transformUser = function() {
|
|||||||
this.storage['generateNewSession'] = true;
|
this.storage['generateNewSession'] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return defer.then(() => {
|
return this._validatePasswordPolicy().then(() => {
|
||||||
return passwordCrypto.hash(this.data.password).then((hashedPassword) => {
|
return passwordCrypto.hash(this.data.password).then((hashedPassword) => {
|
||||||
this.data._hashed_password = hashedPassword;
|
this.data._hashed_password = hashedPassword;
|
||||||
delete this.data.password;
|
delete this.data.password;
|
||||||
@@ -413,51 +383,130 @@ RestWrite.prototype.transformUser = function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// Check for username uniqueness
|
return this._validateUserName();
|
||||||
if (!this.data.username) {
|
}).then(() => {
|
||||||
if (!this.query) {
|
return this._validateEmail();
|
||||||
this.data.username = cryptoUtils.randomString(25);
|
});
|
||||||
this.responseShouldHaveUsername = true;
|
};
|
||||||
}
|
|
||||||
return;
|
RestWrite.prototype._validateUserName = function () {
|
||||||
|
// Check for username uniqueness
|
||||||
|
if (!this.data.username) {
|
||||||
|
if (!this.query) {
|
||||||
|
this.data.username = cryptoUtils.randomString(25);
|
||||||
|
this.responseShouldHaveUsername = true;
|
||||||
}
|
}
|
||||||
// We need to a find to check for duplicate username in case they are missing the unique index on usernames
|
return Promise.resolve();
|
||||||
// TODO: Check if there is a unique index, and if so, skip this query.
|
}
|
||||||
return this.config.database.find(
|
// We need to a find to check for duplicate username in case they are missing the unique index on usernames
|
||||||
this.className,
|
// TODO: Check if there is a unique index, and if so, skip this query.
|
||||||
{ username: this.data.username, objectId: {'$ne': this.objectId()} },
|
return this.config.database.find(
|
||||||
{ limit: 1 }
|
this.className,
|
||||||
)
|
{username: this.data.username, objectId: {'$ne': this.objectId()}},
|
||||||
.then(results => {
|
{limit: 1}
|
||||||
if (results.length > 0) {
|
).then(results => {
|
||||||
throw new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.');
|
if (results.length > 0) {
|
||||||
}
|
throw new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.');
|
||||||
return;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
if (!this.data.email || this.data.email.__op === 'Delete') {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// Validate basic email address format
|
return;
|
||||||
if (!this.data.email.match(/^.+@.+$/)) {
|
});
|
||||||
throw new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS, 'Email address format is invalid.');
|
};
|
||||||
|
|
||||||
|
RestWrite.prototype._validateEmail = function() {
|
||||||
|
if (!this.data.email || this.data.email.__op === 'Delete') {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
// Validate basic email address format
|
||||||
|
if (!this.data.email.match(/^.+@.+$/)) {
|
||||||
|
return Promise.reject(new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS, 'Email address format is invalid.'));
|
||||||
|
}
|
||||||
|
// Same problem for email as above for username
|
||||||
|
return this.config.database.find(
|
||||||
|
this.className,
|
||||||
|
{email: this.data.email, objectId: {'$ne': this.objectId()}},
|
||||||
|
{limit: 1}
|
||||||
|
).then(results => {
|
||||||
|
if (results.length > 0) {
|
||||||
|
throw new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.');
|
||||||
}
|
}
|
||||||
// Same problem for email as above for username
|
// We updated the email, send a new validation
|
||||||
return this.config.database.find(
|
this.storage['sendVerificationEmail'] = true;
|
||||||
this.className,
|
this.config.userController.setEmailVerifyToken(this.data);
|
||||||
{ email: this.data.email, objectId: {'$ne': this.objectId()} },
|
});
|
||||||
{ limit: 1 }
|
};
|
||||||
)
|
|
||||||
.then(results => {
|
RestWrite.prototype._validatePasswordPolicy = function() {
|
||||||
if (results.length > 0) {
|
if (!this.config.passwordPolicy)
|
||||||
throw new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.');
|
return Promise.resolve();
|
||||||
}
|
return this._validatePasswordRequirements().then(() => {
|
||||||
// We updated the email, send a new validation
|
return this._validatePasswordHistory();
|
||||||
this.storage['sendVerificationEmail'] = true;
|
});
|
||||||
this.config.userController.setEmailVerifyToken(this.data);
|
};
|
||||||
});
|
|
||||||
})
|
|
||||||
|
RestWrite.prototype._validatePasswordRequirements = function() {
|
||||||
|
// check if the password conforms to the defined password policy if configured
|
||||||
|
const policyError = 'Password does not meet the Password Policy requirements.';
|
||||||
|
|
||||||
|
// check whether the password meets the password strength requirements
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// check whether password contain username
|
||||||
|
if (this.config.passwordPolicy.doNotAllowUsername === true) {
|
||||||
|
if (this.data.username) { // username is not passed during password reset
|
||||||
|
if (this.data.password.indexOf(this.data.username) >= 0)
|
||||||
|
return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, policyError));
|
||||||
|
} else { // retrieve the User object using objectId during password reset
|
||||||
|
return this.config.database.find('_User', {objectId: this.objectId()})
|
||||||
|
.then(results => {
|
||||||
|
if (results.length != 1) {
|
||||||
|
throw undefined;
|
||||||
|
}
|
||||||
|
if (this.data.password.indexOf(results[0].username) >= 0)
|
||||||
|
return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, policyError));
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
RestWrite.prototype._validatePasswordHistory = function() {
|
||||||
|
// check whether password is repeating from specified history
|
||||||
|
if (this.query && this.config.passwordPolicy.maxPasswordHistory) {
|
||||||
|
return this.config.database.find('_User', {objectId: this.objectId()}, {keys: ["_password_history", "_hashed_password"]})
|
||||||
|
.then(results => {
|
||||||
|
if (results.length != 1) {
|
||||||
|
throw undefined;
|
||||||
|
}
|
||||||
|
const user = results[0];
|
||||||
|
let oldPasswords = [];
|
||||||
|
if (user._password_history)
|
||||||
|
oldPasswords = _.take(user._password_history, this.config.passwordPolicy.maxPasswordHistory - 1);
|
||||||
|
oldPasswords.push(user.password);
|
||||||
|
const newPassword = this.data.password;
|
||||||
|
// compare the new password hash with all old password hashes
|
||||||
|
let promises = oldPasswords.map(function (hash) {
|
||||||
|
return passwordCrypto.compare(newPassword, hash).then((result) => {
|
||||||
|
if (result) // reject if there is a match
|
||||||
|
return Promise.reject("REPEAT_PASSWORD");
|
||||||
|
return Promise.resolve();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
// wait for all comparisons to complete
|
||||||
|
return Promise.all(promises).then(() => {
|
||||||
|
return Promise.resolve();
|
||||||
|
}).catch(err => {
|
||||||
|
if (err === "REPEAT_PASSWORD") // a match was found
|
||||||
|
return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, `New password should not be the same as last ${this.config.passwordPolicy.maxPasswordHistory} passwords.`));
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
RestWrite.prototype.createSessionTokenIfNeeded = function() {
|
RestWrite.prototype.createSessionTokenIfNeeded = function() {
|
||||||
@@ -851,12 +900,35 @@ RestWrite.prototype.runDatabaseOperation = function() {
|
|||||||
// Ignore createdAt when update
|
// Ignore createdAt when update
|
||||||
delete this.data.createdAt;
|
delete this.data.createdAt;
|
||||||
|
|
||||||
// Run an update
|
let defer = Promise.resolve();
|
||||||
return this.config.database.update(this.className, this.query, this.data, this.runOptions)
|
// if password history is enabled then save the current password to history
|
||||||
.then(response => {
|
if (this.className === '_User' && this.data._hashed_password && this.config.passwordPolicy && this.config.passwordPolicy.maxPasswordHistory) {
|
||||||
response.updatedAt = this.updatedAt;
|
defer = this.config.database.find('_User', {objectId: this.objectId()}, {keys: ["_password_history", "_hashed_password"]}).then(results => {
|
||||||
this._updateResponseWithData(response, this.data);
|
if (results.length != 1) {
|
||||||
this.response = { response };
|
throw undefined;
|
||||||
|
}
|
||||||
|
const user = results[0];
|
||||||
|
let oldPasswords = [];
|
||||||
|
if (user._password_history) {
|
||||||
|
oldPasswords = _.take(user._password_history, this.config.passwordPolicy.maxPasswordHistory);
|
||||||
|
}
|
||||||
|
//n-1 passwords go into history including last password
|
||||||
|
while (oldPasswords.length > this.config.passwordPolicy.maxPasswordHistory - 2) {
|
||||||
|
oldPasswords.shift();
|
||||||
|
}
|
||||||
|
oldPasswords.push(user.password);
|
||||||
|
this.data._password_history = oldPasswords;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return defer.then(() => {
|
||||||
|
// Run an update
|
||||||
|
return this.config.database.update(this.className, this.query, this.data, this.runOptions)
|
||||||
|
.then(response => {
|
||||||
|
response.updatedAt = this.updatedAt;
|
||||||
|
this._updateResponseWithData(response, this.data);
|
||||||
|
this.response = { response };
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Set the default ACL and password timestamp for the new _User
|
// Set the default ACL and password timestamp for the new _User
|
||||||
|
|||||||
Reference in New Issue
Block a user