Adds password expiry support to password policy (#3068)

* Adding support for password expiry policy

* Renamed daysBeforeExpiry -> maxPasswordAge
This commit is contained in:
Bhaskar Reddy Yasa
2016-11-21 21:16:38 +05:30
committed by Diwakar Cherukumilli
parent 11c6170ed1
commit edb7b70ced
8 changed files with 397 additions and 89 deletions

View File

@@ -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':

View File

@@ -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) {

View File

@@ -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';
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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;