From 05cc0d90ebd316e31ade0d7460a929134474ceb7 Mon Sep 17 00:00:00 2001 From: Bhaskar Yasa Date: Mon, 7 Nov 2016 12:08:08 +0530 Subject: [PATCH 01/24] Introducing passwordPolicy with resetTokenValidityDuration --- README.md | 4 + spec/PasswordPolicy.spec.js | 138 ++++++++++++++++++ src/Adapters/Storage/Mongo/MongoTransform.js | 14 ++ .../Postgres/PostgresStorageAdapter.js | 8 +- src/Config.js | 22 ++- src/Controllers/DatabaseController.js | 3 +- src/Controllers/UserController.js | 17 ++- src/ParseServer.js | 2 + src/cli/definitions/parse-server.js | 5 + 9 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 spec/PasswordPolicy.spec.js diff --git a/README.md b/README.md index 5b01aba2bd..4f1d9e02c4 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo * `sessionLength` - The length of time in seconds that a session should be valid for. Defaults to 31536000 seconds (1 year). * `revokeSessionOnPasswordReset` - When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions. * `accountLockout` - Lock account when a malicious user is attempting to determine an account password by trial and error. +* `passwordPolicy` - Optional password policy rules to enforce. ##### Logging @@ -277,6 +278,9 @@ var server = ParseServer({ duration: 5, // duration policy setting determines the number of minutes that a locked-out account remains locked out before automatically becoming unlocked. Set it to a value greater than 0 and less than 100000. threshold: 3, // threshold policy setting determines the number of failed sign-in attempts that will cause a user account to be locked. Set it to an integer value greater than 0 and less than 1000. }, + passwordPolicy: { + resetTokenValidityDuration: 24*60*60, // password reset link will expire after the set duration (in seconds) + } }); ``` diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js new file mode 100644 index 0000000000..95e407dd4b --- /dev/null +++ b/spec/PasswordPolicy.spec.js @@ -0,0 +1,138 @@ +"use strict"; + +const request = require('request'); +const Config = require('../src/Config'); + +describe("Password Token Expiry: ", () => { + + 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: 'passwordResetToken', + 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(user => { + Parse.User.requestPasswordReset("user@parse.com"); + }) + .then(() => { + // wait for a bit more than the validity duration set + setTimeout(() => { + expect(sendEmailOptions).not.toBeUndefined(); + + request.get(sendEmailOptions.link, { + followRedirect: false, + }, (error, response, body) => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); + done(); + }); + }, 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: 'passwordResetToken', + 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(user => { + Parse.User.requestPasswordReset("user@parse.com"); + }) + .then(() => { + // wait for a bit but less than the validity duration + setTimeout(() => { + expect(sendEmailOptions).not.toBeUndefined(); + + request.get(sendEmailOptions.link, { + followRedirect: false, + }, (error, response, body) => { + expect(response.statusCode).toEqual(302); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=testResetTokenValidity/; + expect(response.body.match(re)).not.toBe(null); + done(); + }); + }, 1000); + }).catch((err) => { + jfail(err); + done(); + }); + }); + + it('should fail if resetTokenValidityDuration is not a number', done => { + reconfigureServer({ + appName: 'passwordResetToken', + 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 resetTokenValidityDuration is zero or a negative number', done => { + reconfigureServer({ + appName: 'passwordResetToken', + 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(); + }); + }); + +}) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 7d14421272..f3de0b440b 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -66,6 +66,10 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc case '_failed_login_count': key = '_failed_login_count'; break; + case '_perishable_token_expires_at': + key = '_perishable_token_expires_at'; + timeField = true; + break; case '_rperm': case '_wperm': return {key: key, value: restValue}; @@ -171,6 +175,11 @@ function transformQueryKeyValue(className, key, value, schema) { case '_failed_login_count': return {key, value}; case 'sessionToken': return {key: '_session_token', value} + case '_perishable_token_expires_at': + if (valueAsDate(value)) { + return { key: '_perishable_token_expires_at', value: valueAsDate(value) } + } + break; case '_rperm': case '_wperm': case '_perishable_token': @@ -250,6 +259,10 @@ const parseObjectKeyValueToMongoObjectKeyValue = (restKey, restValue, schema) => transformedValue = transformTopLevelAtom(restValue); coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue return {key: '_account_lockout_expires_at', value: coercedToDate}; + case '_perishable_token_expires_at': + transformedValue = transformTopLevelAtom(restValue); + coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue + return { key: '_perishable_token_expires_at', value: coercedToDate }; case '_failed_login_count': case '_rperm': case '_wperm': @@ -748,6 +761,7 @@ const mongoObjectToParseObject = (className, mongoObject, schema) => { break; case '_email_verify_token': case '_perishable_token': + case '_perishable_token_expires_at': case '_tombstone': case '_email_verify_token_expires_at': case '_account_lockout_expires_at': diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 5870e33502..74bc1935fb 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -466,6 +466,7 @@ export class PostgresStorageAdapter { fields._account_lockout_expires_at = {type: 'Date'}; fields._failed_login_count = {type: 'Number'}; fields._perishable_token = {type: 'String'}; + fields._perishable_token_expires_at = {type: 'Date'}; } let index = 2; let relations = []; @@ -691,7 +692,8 @@ export class PostgresStorageAdapter { } } - if (fieldName === '_account_lockout_expires_at') { + if (fieldName === '_account_lockout_expires_at'|| + fieldName === '_perishable_token_expires_at') { if (object[fieldName]) { valuesArray.push(object[fieldName].iso); } else { @@ -1068,6 +1070,10 @@ export class PostgresStorageAdapter { if (object._account_lockout_expires_at) { object._account_lockout_expires_at = { __type: 'Date', iso: object._account_lockout_expires_at.toISOString() }; } + if (object._perishable_token_expires_at) { + object._perishable_token_expires_at = { __type: 'Date', iso: object._perishable_token_expires_at.toISOString() }; + } + for (let fieldName in object) { if (object[fieldName] === null) { diff --git a/src/Config.js b/src/Config.js index 016a3fe0ac..8426bcaf3c 100644 --- a/src/Config.js +++ b/src/Config.js @@ -50,6 +50,7 @@ export class Config { this.preventLoginWithUnverifiedEmail = cacheInfo.preventLoginWithUnverifiedEmail; this.emailVerifyTokenValidityDuration = cacheInfo.emailVerifyTokenValidityDuration; this.accountLockout = cacheInfo.accountLockout; + this.passwordPolicy = cacheInfo.passwordPolicy; this.appName = cacheInfo.appName; this.analyticsController = cacheInfo.analyticsController; @@ -79,7 +80,8 @@ export class Config { expireInactiveSessions, sessionLength, emailVerifyTokenValidityDuration, - accountLockout + accountLockout, + passwordPolicy }) { const emailAdapter = userController.adapter; if (verifyUserEmails) { @@ -88,6 +90,8 @@ export class Config { this.validateAccountLockoutPolicy(accountLockout); + this.validatePasswordPolicy(passwordPolicy); + if (typeof revokeSessionOnPasswordReset !== 'boolean') { throw 'revokeSessionOnPasswordReset must be a boolean value'; } @@ -113,6 +117,14 @@ export class Config { } } + static validatePasswordPolicy(passwordPolicy) { + if (passwordPolicy) { + if (passwordPolicy.resetTokenValidityDuration !== undefined && (typeof passwordPolicy.resetTokenValidityDuration !== 'number' || passwordPolicy.resetTokenValidityDuration <= 0)) { + throw 'passwordPolicy.resetTokenValidityDuration must be a positive number'; + } + } + } + static validateEmailConfiguration({emailAdapter, appName, publicServerURL, emailVerifyTokenValidityDuration}) { if (!emailAdapter) { throw 'An emailAdapter is required for e-mail verification and password resets.'; @@ -163,6 +175,14 @@ export class Config { return new Date(now.getTime() + (this.emailVerifyTokenValidityDuration*1000)); } + generatePasswordResetTokenExpiresAt() { + if (!this.passwordPolicy || !this.passwordPolicy.resetTokenValidityDuration) { + return undefined; + } + const now = new Date(); + return new Date(now.getTime() + (this.passwordPolicy.resetTokenValidityDuration * 1000)); + } + generateSessionExpiresAt() { if (!this.expireInactiveSessions) { return undefined; diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 986a71d5b7..110bd39b5b 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -169,6 +169,7 @@ const filterSensitiveData = (isMaster, aclGroup, className, object) => { } delete object._email_verify_token; delete object._perishable_token; + delete object._perishable_token_expires_at; delete object._tombstone; delete object._email_verify_token_expires_at; delete object._failed_login_count; @@ -189,7 +190,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']; +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 isSpecialUpdateKey = key => { return specialKeysForUpdate.indexOf(key) >= 0; diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index fbdf707a4e..04fafd6d5c 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -77,6 +77,12 @@ export class UserController extends AdaptableController { if (results.length != 1) { throw undefined; } + + if (this.config.passwordPolicy && this.config.passwordPolicy.resetTokenValidityDuration) { + if (results[0]._perishable_token_expires_at < new Date()) + throw 'The password reset link has expired'; + } + return results[0]; }); } @@ -125,7 +131,13 @@ export class UserController extends AdaptableController { } setPasswordResetToken(email) { - return this.config.database.update('_User', { $or: [{email}, {username: email, email: {$exists: false}}] }, { _perishable_token: randomString(25) }, {}, true) + const token = { _perishable_token: randomString(25) }; + + if (this.config.passwordPolicy && this.config.passwordPolicy.resetTokenValidityDuration) { + token._perishable_token_expires_at = Parse._encode(this.config.generatePasswordResetTokenExpiresAt()); + } + + return this.config.database.update('_User', { $or: [{email}, {username: email, email: {$exists: false}}] }, token, {}, true) } sendPasswordResetEmail(email) { @@ -162,7 +174,8 @@ export class UserController extends AdaptableController { .then(user => updateUserPassword(user.objectId, password, this.config)) // clear reset password token .then(() => this.config.database.update('_User', { username }, { - _perishable_token: {__op: 'Delete'} + _perishable_token: {__op: 'Delete'}, + _perishable_token_expires_at: {__op: 'Delete'} })); } diff --git a/src/ParseServer.js b/src/ParseServer.js index faa544dffb..6b94310c73 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -125,6 +125,7 @@ class ParseServer { preventLoginWithUnverifiedEmail = defaults.preventLoginWithUnverifiedEmail, emailVerifyTokenValidityDuration, accountLockout, + passwordPolicy, cacheAdapter, emailAdapter, publicServerURL, @@ -210,6 +211,7 @@ class ParseServer { preventLoginWithUnverifiedEmail: preventLoginWithUnverifiedEmail, emailVerifyTokenValidityDuration: emailVerifyTokenValidityDuration, accountLockout: accountLockout, + passwordPolicy: passwordPolicy, allowClientClassCreation: allowClientClassCreation, authDataManager: authDataManager(oauth, enableAnonymousUsers), appName: appName, diff --git a/src/cli/definitions/parse-server.js b/src/cli/definitions/parse-server.js index 73822dae84..be3f315fc1 100644 --- a/src/cli/definitions/parse-server.js +++ b/src/cli/definitions/parse-server.js @@ -136,6 +136,11 @@ export default { help: "account lockout policy for failed login attempts", action: objectParser }, + "passwordPolicy": { + env: "PARSE_SERVER_PASSWORD_POLICY", + help: "Password policy for reset link expiry", + action: objectParser + }, "appName": { env: "PARSE_SERVER_APP_NAME", help: "Sets the app name" From bbd5d37563a9d1a3a06f541102ef8ab354412fbf Mon Sep 17 00:00:00 2001 From: bhaskaryasa Date: Tue, 8 Nov 2016 23:08:16 +0530 Subject: [PATCH 02/24] validator added to passwordPolicy validator support in passwordPolicy that can be used to enforce strong passwords. --- src/Config.js | 22 ++++++++++++++++++++++ src/Controllers/UserController.js | 11 ++++++++--- src/ParseServer.js | 1 + src/RestWrite.js | 6 ++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/Config.js b/src/Config.js index 8426bcaf3c..281712e392 100644 --- a/src/Config.js +++ b/src/Config.js @@ -122,6 +122,28 @@ export class Config { if (passwordPolicy.resetTokenValidityDuration !== undefined && (typeof passwordPolicy.resetTokenValidityDuration !== 'number' || passwordPolicy.resetTokenValidityDuration <= 0)) { throw 'passwordPolicy.resetTokenValidityDuration must be a positive number'; } + + if(passwordPolicy.validator && typeof passwordPolicy.validator !== 'string' && !(passwordPolicy.validator instanceof RegExp) && typeof passwordPolicy.validator !== 'function' ) { + throw 'passwordPolicy.validator must be a RegExp, string or function.'; + } + } + } + + // if the passwordPolicy.validator is configured as a string or regex then convert it to a function to process the pattern + static setupPasswordValidator(passwordPolicy) { + if (passwordPolicy && passwordPolicy.validator) { + if (typeof passwordPolicy.validator === 'string') { + const pattern = new RegExp(passwordPolicy.validator); + passwordPolicy.validator = (value) => { + return pattern.test(value) + } + } + else if (passwordPolicy.validator instanceof RegExp) { + const pattern = passwordPolicy.validator; + passwordPolicy.validator = (value) => { + return pattern.test(value) + } + } } } diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 04fafd6d5c..d430ceace5 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -202,9 +202,14 @@ export class UserController extends AdaptableController { // Mark this private function updateUserPassword(userId, password, config) { - return rest.update(config, Auth.master(config), '_User', userId, { - password: password - }); + // check if the password confirms to the defined password policy if configured + if (config.passwordPolicy && config.passwordPolicy.validator && !config.passwordPolicy.validator(password)) { + return Promise.reject('Password does not confirm to the Password Policy.') + } + + return rest.update(config, Auth.master(config), '_User', userId, { + password: password + }); } export default UserController; diff --git a/src/ParseServer.js b/src/ParseServer.js index 6b94310c73..4cd4fd3d9a 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -235,6 +235,7 @@ class ParseServer { Config.validate(AppCache.get(appId)); this.config = AppCache.get(appId); + Config.setupPasswordValidator(this.config.passwordPolicy); hooksController.load(); // Note: Tests will start to fail if any validation happens after this is called. diff --git a/src/RestWrite.js b/src/RestWrite.js index 14105e6d89..dcf48c2e7d 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -368,6 +368,12 @@ RestWrite.prototype.transformUser = function() { if (!this.data.password) { return; } + + // check if the password confirms to the defined password policy if configured + if (this.config.passwordPolicy && this.config.passwordPolicy.validator && !this.config.passwordPolicy.validator(this.data.password)) { + return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, 'Password does not confirm to the Password Policy.')) + } + if (this.query && !this.auth.isMaster ) { this.storage['clearSessions'] = true; this.storage['generateNewSession'] = true; From 96920bdb7e728542d60e30934080acd3dd55a065 Mon Sep 17 00:00:00 2001 From: bhaskaryasa Date: Wed, 9 Nov 2016 10:01:58 +0530 Subject: [PATCH 03/24] Add some unit tests for passwordPolicy.validator --- spec/PasswordPolicy.spec.js | 219 +++++++++++++++++++++++++++++++++++- 1 file changed, 215 insertions(+), 4 deletions(-) diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index 95e407dd4b..219ab0da5d 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -17,7 +17,7 @@ describe("Password Token Expiry: ", () => { } } reconfigureServer({ - appName: 'passwordResetToken', + appName: 'passwordPolicy', emailAdapter: emailAdapter, passwordPolicy: { resetTokenValidityDuration: 0.5, // 0.5 second @@ -63,7 +63,7 @@ describe("Password Token Expiry: ", () => { } } reconfigureServer({ - appName: 'passwordResetToken', + appName: 'passwordPolicy', emailAdapter: emailAdapter, passwordPolicy: { resetTokenValidityDuration: 5, // 5 seconds @@ -101,7 +101,7 @@ describe("Password Token Expiry: ", () => { it('should fail if resetTokenValidityDuration is not a number', done => { reconfigureServer({ - appName: 'passwordResetToken', + appName: 'passwordPolicy', passwordPolicy: { resetTokenValidityDuration: "not a number" }, @@ -119,7 +119,7 @@ describe("Password Token Expiry: ", () => { it('should fail if resetTokenValidityDuration is zero or a negative number', done => { reconfigureServer({ - appName: 'passwordResetToken', + appName: 'passwordPolicy', passwordPolicy: { resetTokenValidityDuration: 0 }, @@ -135,4 +135,215 @@ describe("Password Token Expiry: ", () => { }); }); + it('signup should fail if password does not confirm to the policy enforced using RegExp', (done) => { + const user = new Parse.User(); + reconfigureServer({ + appName: 'passwordPolicy', + passwordPolicy: { + validator: /[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 confirm to the policy.'); + done(); + }, (error) => { + expect(error.code).toEqual(142); + done(); + }); + }) + }); + + it('signup should succeed if password confirms to the policy enforced using RegExp', (done) => { + const user = new Parse.User(); + reconfigureServer({ + appName: 'passwordPolicy', + passwordPolicy: { + validator: /[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(() => { + done(); + }, (error) => { + fail('Should have succeeded as password confirms to the policy.'); + done(); + }); + }) + }); + + it('signup should fail if password does not confirm to the policy enforced using regex string', (done) => { + const user = new Parse.User(); + reconfigureServer({ + appName: 'passwordPolicy', + passwordPolicy: { + validator: "[A-Z]+" // password should contain at least one UPPER case letter + }, + 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 confirm to the policy.'); + done(); + }, (error) => { + expect(error.code).toEqual(142); + done(); + }); + }) + }); + + it('signup should succeed if password confirms to the policy enforced using regex string', (done) => { + const user = new Parse.User(); + reconfigureServer({ + appName: 'passwordPolicy', + passwordPolicy: { + validator: /[A-Z]+/ // password should contain at least one digit + }, + publicServerURL: "http://localhost:8378/1" + }).then(() => { + user.setUsername("user1"); + user.setPassword("oneUpper"); + user.set('email', 'user1@parse.com'); + user.signUp().then(() => { + done(); + }, (error) => { + fail('Should have succeeded as password confirms to the policy.'); + done(); + }); + }) + }); + + it('signup should fail if password does not confirm to the policy enforced using a callback function', (done) => { + const user = new Parse.User(); + reconfigureServer({ + appName: 'passwordPolicy', + passwordPolicy: { + validator: password => 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 confirm to the policy.'); + done(); + }, (error) => { + expect(error.code).toEqual(142); + done(); + }); + }) + }); + + it('signup should succeed if password confirms to the policy enforced using a callback function', (done) => { + const user = new Parse.User(); + reconfigureServer({ + appName: 'passwordPolicy', + passwordPolicy: { + validator: password => true // never fail + }, + publicServerURL: "http://localhost:8378/1" + }).then(() => { + user.setUsername("user1"); + user.setPassword("oneUpper"); + user.set('email', 'user1@parse.com'); + user.signUp().then(() => { + done(); + }, (error) => { + fail('Should have succeeded as password confirms to the policy.'); + done(); + }); + }) + }); + + it('should reset password if new password confirms to password policy', done => { + var user = new Parse.User(); + var emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: options => { + request.get(options.link, { + followRedirect: false, + }, (error, response, body) => { + if (error) { + jfail(error); + fail("Failed to get the reset link"); + return; + } + 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); + if (!match) { + fail("should have a token"); + done(); + return; + } + var token = match[1]; + + request.post({ + url: "http://localhost:8378/1/apps/test/request_password_reset", + body: `new_password=has2init&token=${token}&username=user1`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + followRedirect: false, + }, (error, response, body) => { + if (error) { + jfail(error); + fail("Failed to POST request password reset"); + return; + } + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html'); + + Parse.User.logIn("user1", "has2init").then(function (user) { + done(); + }, (err) => { + jfail(err); + fail("should login with new password"); + done(); + }); + + }); + }); + }, + sendMail: () => { + } + } + reconfigureServer({ + appName: 'passwordPolicy', + verifyUserEmails: false, + emailAdapter: emailAdapter, + passwordPolicy: { + validator: /[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', { + error: (err) => { + jfail(err); + fail("Reset password request should not fail"); + done(); + } + }); + }, error => { + jfail(err); + fail("signUp should not fail"); + done(); + }); + }); + }); + }) From e7307be699bda97596c366b6e3070d2c6c826a9b Mon Sep 17 00:00:00 2001 From: bhaskaryasa Date: Wed, 9 Nov 2016 12:30:57 +0530 Subject: [PATCH 04/24] Add unit test for reset password failure for non-conformance --- spec/PasswordPolicy.spec.js | 106 +++++++++++++++++++++++++++++++++++- src/Config.js | 2 +- 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index 219ab0da5d..4c80d7b6b3 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -99,7 +99,7 @@ describe("Password Token Expiry: ", () => { }); }); - it('should fail if resetTokenValidityDuration is not a number', done => { + it('should fail if passwordPolicy.resetTokenValidityDuration is not a number', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { @@ -117,7 +117,7 @@ describe("Password Token Expiry: ", () => { }); }); - it('should fail if resetTokenValidityDuration is zero or a negative number', done => { + it('should fail if passwordPolicy.resetTokenValidityDuration is zero or a negative number', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { @@ -135,6 +135,24 @@ describe("Password Token Expiry: ", () => { }); }); + it('should fail if passwordPolicy.validator setting is invalid type', done => { + reconfigureServer({ + appName: 'passwordPolicy', + passwordPolicy: { + validator: 1 // number is not a valid setting for validator + }, + publicServerURL: "http://localhost:8378/1" + }) + .then(() => { + fail('passwordPolicy.validator type test failed'); + done(); + }) + .catch(err => { + expect(err).toEqual('passwordPolicy.validator must be a RegExp, a string or a function.'); + done(); + }); + }); + it('signup should fail if password does not confirm to the policy enforced using RegExp', (done) => { const user = new Parse.User(); reconfigureServer({ @@ -339,7 +357,89 @@ describe("Password Token Expiry: ", () => { } }); }, error => { - jfail(err); + jfail(error); + fail("signUp should not fail"); + done(); + }); + }); + }); + + it('should fail to reset password if the new password does not confirm to password policy', done => { + var user = new Parse.User(); + var emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: options => { + request.get(options.link, { + followRedirect: false, + }, (error, response, body) => { + if (error) { + jfail(error); + fail("Failed to get the reset link"); + return; + } + 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); + if (!match) { + fail("should have a token"); + done(); + return; + } + var token = match[1]; + + request.post({ + url: "http://localhost:8378/1/apps/test/request_password_reset", + body: `new_password=hasnodigit&token=${token}&username=user1`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + followRedirect: false, + }, (error, response, body) => { + if (error) { + jfail(error); + fail("Failed to POST request password reset"); + return; + } + 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`); + + Parse.User.logIn("user1", "has 1 digit").then(function (user) { + done(); + }, (err) => { + jfail(err); + fail("should login with old password"); + done(); + }); + + }); + }); + }, + sendMail: () => { + } + } + reconfigureServer({ + appName: 'passwordPolicy', + verifyUserEmails: false, + emailAdapter: emailAdapter, + passwordPolicy: { + validator: /[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', { + error: (err) => { + jfail(err); + fail("Reset password request should not fail"); + done(); + } + }); + }, error => { + jfail(error); fail("signUp should not fail"); done(); }); diff --git a/src/Config.js b/src/Config.js index 281712e392..e0933c07b9 100644 --- a/src/Config.js +++ b/src/Config.js @@ -124,7 +124,7 @@ export class Config { } if(passwordPolicy.validator && typeof passwordPolicy.validator !== 'string' && !(passwordPolicy.validator instanceof RegExp) && typeof passwordPolicy.validator !== 'function' ) { - throw 'passwordPolicy.validator must be a RegExp, string or function.'; + throw 'passwordPolicy.validator must be a RegExp, a string or a function.'; } } } From 3206b34ce09d6dc4d1709d56824815544e0dbdeb Mon Sep 17 00:00:00 2001 From: bhaskaryasa Date: Wed, 9 Nov 2016 15:21:15 +0530 Subject: [PATCH 05/24] Update README.md for passwordPolicy --- README.md | 7 ++++++- spec/PasswordPolicy.spec.js | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4f1d9e02c4..e0cb11a7e4 100644 --- a/README.md +++ b/README.md @@ -278,8 +278,13 @@ var server = ParseServer({ duration: 5, // duration policy setting determines the number of minutes that a locked-out account remains locked out before automatically becoming unlocked. Set it to a value greater than 0 and less than 100000. threshold: 3, // threshold policy setting determines the number of failed sign-in attempts that will cause a user account to be locked. Set it to an integer value greater than 0 and less than 1000. }, + // optional settings to enforce password policies passwordPolicy: { - resetTokenValidityDuration: 24*60*60, // password reset link will expire after the set duration (in seconds) + // optional setting to enforce strong passwords + // can be a RegExp/String representing pattern to enforce or a function that return a bool + validator: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/, // enforce password with at least 8 char with at least 1 lower case, 1 upper case and 1 digit + //optional setting to set a validity duration for password reset links (in seconds) + resetTokenValidityDuration: 24*60*60, // expire after 24 hours } }); ``` diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index 4c80d7b6b3..820c27251a 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -3,7 +3,7 @@ const request = require('request'); const Config = require('../src/Config'); -describe("Password Token Expiry: ", () => { +describe("Password Policy: ", () => { 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(); From 052d6eb48eaf7ed346c3363fc54e3d2731436136 Mon Sep 17 00:00:00 2001 From: Bhaskar Reddy Yasa Date: Thu, 10 Nov 2016 16:38:11 +0530 Subject: [PATCH 06/24] Added code to handle Parse.Error from rest.update in UserController.updatePassword thus avoid duplicate check for password validator in updateUserPassword. --- src/Controllers/UserController.js | 13 +++++++------ src/cli/definitions/parse-server.js | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index d430ceace5..637a0ce4b3 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -176,7 +176,13 @@ export class UserController extends AdaptableController { .then(() => this.config.database.update('_User', { username }, { _perishable_token: {__op: 'Delete'}, _perishable_token_expires_at: {__op: 'Delete'} - })); + }),(error) => { + if (error.message) { // in case of Parse.Error, fail with the error message only + return Promise.reject(error.message); + } else { + return Promise.reject(error); + } + }); } defaultVerificationEmail({link, user, appName, }) { @@ -202,11 +208,6 @@ export class UserController extends AdaptableController { // Mark this private function updateUserPassword(userId, password, config) { - // check if the password confirms to the defined password policy if configured - if (config.passwordPolicy && config.passwordPolicy.validator && !config.passwordPolicy.validator(password)) { - return Promise.reject('Password does not confirm to the Password Policy.') - } - return rest.update(config, Auth.master(config), '_User', userId, { password: password }); diff --git a/src/cli/definitions/parse-server.js b/src/cli/definitions/parse-server.js index be3f315fc1..dd250cd46d 100644 --- a/src/cli/definitions/parse-server.js +++ b/src/cli/definitions/parse-server.js @@ -138,7 +138,7 @@ export default { }, "passwordPolicy": { env: "PARSE_SERVER_PASSWORD_POLICY", - help: "Password policy for reset link expiry", + help: "Password policy for enforcing password related rules", action: objectParser }, "appName": { From 838d7cbfe98b8b5f994052ef26e8fa3be85e0914 Mon Sep 17 00:00:00 2001 From: Bhaskar Reddy Yasa Date: Thu, 10 Nov 2016 22:45:03 +0530 Subject: [PATCH 07/24] Added optional setting to disallow username in password --- README.md | 1 + spec/PasswordPolicy.spec.js | 250 +++++++++++++++++++++++++++++++++++- src/Config.js | 4 + src/RestWrite.js | 41 +++++- 4 files changed, 289 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e0cb11a7e4..d66f7ce94f 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,7 @@ var server = ParseServer({ // optional setting to enforce strong passwords // can be a RegExp/String representing pattern to enforce or a function that return a bool validator: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/, // enforce password with at least 8 char with at least 1 lower case, 1 upper case and 1 digit + doNotAllowUsername: true, // optional setting to disallow username in passwords //optional setting to set a validity duration for password reset links (in seconds) resetTokenValidityDuration: 24*60*60, // expire after 24 hours } diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index 820c27251a..367a11b32c 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -3,7 +3,7 @@ const request = require('request'); const Config = require('../src/Config'); -describe("Password Policy: ", () => { +fdescribe("Password Policy: ", () => { 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(); @@ -446,4 +446,252 @@ describe("Password Policy: ", () => { }); }); + 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: { + validator: /[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(); + }, (error) => { + expect(error.code).toEqual(142); + 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(); + }, (error) => { + 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: { + validator: /[0-9]+/ + }, + publicServerURL: "http://localhost:8378/1" + }).then(() => { + user.setUsername("user1"); + user.setPassword("user1"); + user.set('email', 'user1@parse.com'); + user.signUp().then(() => { + done(); + }, (error) => { + 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 => { + var user = new Parse.User(); + var emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: options => { + request.get(options.link, { + followRedirect: false, + }, (error, response, body) => { + if (error) { + jfail(error); + fail("Failed to get the reset link"); + return; + } + 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); + if (!match) { + fail("should have a token"); + done(); + return; + } + var token = match[1]; + + request.post({ + url: "http://localhost:8378/1/apps/test/request_password_reset", + body: `new_password=xuser12&token=${token}&username=user1`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + followRedirect: false, + }, (error, response, body) => { + if (error) { + jfail(error); + fail("Failed to POST request password reset"); + return; + } + 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`); + + Parse.User.logIn("user1", "r@nd0m").then(function (user) { + done(); + }, (err) => { + jfail(err); + fail("should login with old password"); + 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', { + error: (err) => { + jfail(err); + fail("Reset password request should not fail"); + done(); + } + }); + }, error => { + jfail(error); + fail("signUp should not fail"); + done(); + }); + }); + }); + + it('should reset password even if the new password contains user name while the policy allows', done => { + var user = new Parse.User(); + var emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: options => { + request.get(options.link, { + followRedirect: false, + }, (error, response, body) => { + if (error) { + jfail(error); + fail("Failed to get the reset link"); + return; + } + 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); + if (!match) { + fail("should have a token"); + done(); + return; + } + var token = match[1]; + + request.post({ + url: "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, + }, (error, response, body) => { + if (error) { + jfail(error); + fail("Failed to POST request password reset"); + return; + } + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html'); + + Parse.User.logIn("user1", "uuser11").then(function (user) { + done(); + }, (err) => { + jfail(err); + fail("should login with new password"); + done(); + }); + + }); + }); + }, + sendMail: () => { + } + } + reconfigureServer({ + appName: 'passwordPolicy', + verifyUserEmails: false, + emailAdapter: emailAdapter, + passwordPolicy: { + validator: /[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', { + error: (err) => { + jfail(err); + fail("Reset password request should not fail"); + done(); + } + }); + }, error => { + jfail(error); + fail("signUp should not fail"); + done(); + }); + }); + }); + }) diff --git a/src/Config.js b/src/Config.js index e0933c07b9..55345848ba 100644 --- a/src/Config.js +++ b/src/Config.js @@ -126,6 +126,10 @@ export class Config { if(passwordPolicy.validator && typeof passwordPolicy.validator !== 'string' && !(passwordPolicy.validator instanceof RegExp) && typeof passwordPolicy.validator !== 'function' ) { throw 'passwordPolicy.validator must be a RegExp, a string or a function.'; } + + if(passwordPolicy.doNotAllowUsername && typeof passwordPolicy.doNotAllowUsername !== 'boolean') { + throw 'passwordPolicy.doNotAllowUsername must be a boolean value.'; + } } } diff --git a/src/RestWrite.js b/src/RestWrite.js index dcf48c2e7d..10ef60bb58 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -369,18 +369,47 @@ RestWrite.prototype.transformUser = function() { return; } + let defer = Promise.resolve(); + // check if the password confirms to the defined password policy if configured - if (this.config.passwordPolicy && this.config.passwordPolicy.validator && !this.config.passwordPolicy.validator(this.data.password)) { - return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, 'Password does not confirm to the Password Policy.')) + if (this.config.passwordPolicy) { + const policyError = 'Password does not confirm to the Password Policy.'; + + // check whether the password confirms to the policy + if (this.config.passwordPolicy.validator && !this.config.passwordPolicy.validator(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) { this.storage['clearSessions'] = true; this.storage['generateNewSession'] = true; } - return passwordCrypto.hash(this.data.password).then((hashedPassword) => { - this.data._hashed_password = hashedPassword; - delete this.data.password; + + return defer.then(() => { + return passwordCrypto.hash(this.data.password).then((hashedPassword) => { + this.data._hashed_password = hashedPassword; + delete this.data.password; + }); }); }).then(() => { From 54bf4d178d03b056da6a41c1b70fd535ca214c05 Mon Sep 17 00:00:00 2001 From: Bhaskar Reddy Yasa Date: Thu, 10 Nov 2016 22:58:57 +0530 Subject: [PATCH 08/24] fdescribe -> describe --- spec/PasswordPolicy.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index 367a11b32c..36d064e50b 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -3,7 +3,7 @@ const request = require('request'); const Config = require('../src/Config'); -fdescribe("Password Policy: ", () => { +describe("Password Policy: ", () => { 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(); From 59fb9d76b51691424ea7774b0c19e300766dea73 Mon Sep 17 00:00:00 2001 From: Bhaskar Reddy Yasa Date: Fri, 11 Nov 2016 16:03:32 +0530 Subject: [PATCH 09/24] updated PasswordPolicy.spec.js to use request-promise --- spec/PasswordPolicy.spec.js | 449 ++++++++++++++++++------------------ 1 file changed, 225 insertions(+), 224 deletions(-) diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index 36d064e50b..bbdf41370c 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -1,9 +1,9 @@ "use strict"; -const request = require('request'); +const requestp = require('request-promise'); const Config = require('../src/Config'); -describe("Password Policy: ", () => { +fdescribe("Password Policy: ", () => { 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(); @@ -28,24 +28,27 @@ describe("Password Policy: ", () => { user.setPassword("original"); user.set('email', 'user@parse.com'); return user.signUp(); - }) - .then(user => { - Parse.User.requestPasswordReset("user@parse.com"); - }) - .then(() => { - // wait for a bit more than the validity duration set - setTimeout(() => { - expect(sendEmailOptions).not.toBeUndefined(); - - request.get(sendEmailOptions.link, { - followRedirect: false, - }, (error, response, body) => { - expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); - done(); - }); - }, 1000); - }).catch((err) => { + }).then(user => { + Parse.User.requestPasswordReset("user@parse.com"); + }).then(() => { + // wait for a bit more than the validity duration set + setTimeout(() => { + expect(sendEmailOptions).not.toBeUndefined(); + + requestp.get({ + uri: sendEmailOptions.link, + 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/invalid_link.html'); + done(); + }).catch((error) => { + fail(error); + }); + }, 1000); + }).catch((err) => { jfail(err); done(); }); @@ -69,31 +72,33 @@ describe("Password Policy: ", () => { 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(user => { - Parse.User.requestPasswordReset("user@parse.com"); - }) - .then(() => { - // wait for a bit but less than the validity duration - setTimeout(() => { - expect(sendEmailOptions).not.toBeUndefined(); - - request.get(sendEmailOptions.link, { - followRedirect: false, - }, (error, response, body) => { - expect(response.statusCode).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=testResetTokenValidity/; - expect(response.body.match(re)).not.toBe(null); - done(); - }); - }, 1000); - }).catch((err) => { + }).then(() => { + user.setUsername("testResetTokenValidity"); + user.setPassword("original"); + user.set('email', 'user@parse.com'); + return user.signUp(); + }).then(user => { + Parse.User.requestPasswordReset("user@parse.com"); + }).then(() => { + // wait for a bit but less than the validity duration + setTimeout(() => { + expect(sendEmailOptions).not.toBeUndefined(); + + requestp.get({ + uri: sendEmailOptions.link, + simple: false, + resolveWithFullResponse: true, + followRedirect: false + }).then((response) => { + expect(response.statusCode).toEqual(302); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=testResetTokenValidity/; + expect(response.body.match(re)).not.toBe(null); + done(); + }).catch((error) => { + fail(error); + }); + }, 1000); + }).catch((err) => { jfail(err); done(); }); @@ -106,15 +111,13 @@ describe("Password Policy: ", () => { 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(); - }); + }).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 => { @@ -124,15 +127,13 @@ describe("Password Policy: ", () => { 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(); - }); + }).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.validator setting is invalid type', done => { @@ -142,15 +143,13 @@ describe("Password Policy: ", () => { validator: 1 // number is not a valid setting for validator }, publicServerURL: "http://localhost:8378/1" - }) - .then(() => { - fail('passwordPolicy.validator type test failed'); - done(); - }) - .catch(err => { - expect(err).toEqual('passwordPolicy.validator must be a RegExp, a string or a function.'); - done(); - }); + }).then(() => { + fail('passwordPolicy.validator type test failed'); + done(); + }).catch(err => { + expect(err).toEqual('passwordPolicy.validator must be a RegExp, a string or a function.'); + done(); + }); }); it('signup should fail if password does not confirm to the policy enforced using RegExp', (done) => { @@ -168,7 +167,7 @@ describe("Password Policy: ", () => { user.signUp().then(() => { fail('Should have failed as password does not confirm to the policy.'); done(); - }, (error) => { + }).catch((error) => { expect(error.code).toEqual(142); done(); }); @@ -189,7 +188,7 @@ describe("Password Policy: ", () => { user.set('email', 'user1@parse.com'); user.signUp().then(() => { done(); - }, (error) => { + }).catch((error) => { fail('Should have succeeded as password confirms to the policy.'); done(); }); @@ -211,7 +210,7 @@ describe("Password Policy: ", () => { user.signUp().then(() => { fail('Should have failed as password does not confirm to the policy.'); done(); - }, (error) => { + }).catch((error) => { expect(error.code).toEqual(142); done(); }); @@ -232,7 +231,7 @@ describe("Password Policy: ", () => { user.set('email', 'user1@parse.com'); user.signUp().then(() => { done(); - }, (error) => { + }).catch((error) => { fail('Should have succeeded as password confirms to the policy.'); done(); }); @@ -254,7 +253,7 @@ describe("Password Policy: ", () => { user.signUp().then(() => { fail('Should have failed as password does not confirm to the policy.'); done(); - }, (error) => { + }).catch((error) => { expect(error.code).toEqual(142); done(); }); @@ -275,7 +274,7 @@ describe("Password Policy: ", () => { user.set('email', 'user1@parse.com'); user.signUp().then(() => { done(); - }, (error) => { + }).catch((error) => { fail('Should have succeeded as password confirms to the policy.'); done(); }); @@ -287,14 +286,12 @@ describe("Password Policy: ", () => { var emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: options => { - request.get(options.link, { + requestp.get({ + uri: options.link, followRedirect: false, - }, (error, response, body) => { - if (error) { - jfail(error); - fail("Failed to get the reset link"); - return; - } + simple: false, + 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); @@ -305,31 +302,35 @@ describe("Password Policy: ", () => { } var token = match[1]; - request.post({ - url: "http://localhost:8378/1/apps/test/request_password_reset", + requestp.post({ + uri: "http://localhost:8378/1/apps/test/request_password_reset", body: `new_password=has2init&token=${token}&username=user1`, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, followRedirect: false, - }, (error, response, body) => { - if (error) { - jfail(error); - fail("Failed to POST request password reset"); - return; - } + 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'); Parse.User.logIn("user1", "has2init").then(function (user) { done(); - }, (err) => { + }).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: () => { @@ -343,25 +344,24 @@ describe("Password Policy: ", () => { validator: /[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', { - error: (err) => { - jfail(err); - fail("Reset password request should not fail"); - done(); - } - }); - }, error => { - jfail(error); - fail("signUp should not fail"); - done(); + }).then(() => { + user.setUsername("user1"); + 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(); + } }); + }).catch(error => { + jfail(error); + fail("signUp should not fail"); + done(); }); + }); }); it('should fail to reset password if the new password does not confirm to password policy', done => { @@ -369,14 +369,12 @@ describe("Password Policy: ", () => { var emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: options => { - request.get(options.link, { + requestp.get({ + uri: options.link, followRedirect: false, - }, (error, response, body) => { - if (error) { - jfail(error); - fail("Failed to get the reset link"); - return; - } + simple: false, + 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); @@ -387,31 +385,35 @@ describe("Password Policy: ", () => { } var token = match[1]; - request.post({ - url: "http://localhost:8378/1/apps/test/request_password_reset", + requestp.post({ + uri: "http://localhost:8378/1/apps/test/request_password_reset", body: `new_password=hasnodigit&token=${token}&username=user1`, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, followRedirect: false, - }, (error, response, body) => { - if (error) { - jfail(error); - fail("Failed to POST request password reset"); - return; - } + simple: false, + 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`); Parse.User.logIn("user1", "has 1 digit").then(function (user) { done(); - }, (err) => { + }).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: () => { @@ -425,25 +427,24 @@ describe("Password Policy: ", () => { validator: /[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', { - error: (err) => { - jfail(err); - fail("Reset password request should not fail"); - done(); - } - }); - }, error => { - jfail(error); - fail("signUp should not fail"); - done(); + }).then(() => { + user.setUsername("user1"); + 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(); + } }); + }).catch(error => { + jfail(error); + fail("signUp should not fail"); + done(); }); + }); }); it('should fail if passwordPolicy.doNotAllowUsername is not a boolean value', (done) => { @@ -453,15 +454,13 @@ describe("Password Policy: ", () => { 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(); - }); + }).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) => { @@ -480,7 +479,7 @@ describe("Password Policy: ", () => { user.signUp().then(() => { fail('Should have failed as password contains username.'); done(); - }, (error) => { + }).catch((error) => { expect(error.code).toEqual(142); done(); }); @@ -501,7 +500,7 @@ describe("Password Policy: ", () => { user.set('email', 'user1@parse.com'); user.signUp().then(() => { done(); - }, (error) => { + }).catch((error) => { fail('Should have succeeded as password does not contain username.'); done(); }); @@ -522,7 +521,7 @@ describe("Password Policy: ", () => { user.set('email', 'user1@parse.com'); user.signUp().then(() => { done(); - }, (error) => { + }).catch((error) => { fail('Should have succeeded as policy allows username in password.'); done(); }); @@ -534,14 +533,12 @@ describe("Password Policy: ", () => { var emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: options => { - request.get(options.link, { + requestp.get({ + uri: options.link, followRedirect: false, - }, (error, response, body) => { - if (error) { - jfail(error); - fail("Failed to get the reset link"); - return; - } + simple: false, + 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); @@ -552,31 +549,36 @@ describe("Password Policy: ", () => { } var token = match[1]; - request.post({ - url: "http://localhost:8378/1/apps/test/request_password_reset", + requestp.post({ + uri: "http://localhost:8378/1/apps/test/request_password_reset", body: `new_password=xuser12&token=${token}&username=user1`, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, followRedirect: false, - }, (error, response, body) => { - if (error) { - jfail(error); - fail("Failed to POST request password reset"); - return; - } + simple: false, + 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`); Parse.User.logIn("user1", "r@nd0m").then(function (user) { done(); - }, (err) => { + }).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: () => { @@ -590,25 +592,24 @@ describe("Password Policy: ", () => { 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', { - error: (err) => { - jfail(err); - fail("Reset password request should not fail"); - done(); - } - }); - }, error => { - jfail(error); - fail("signUp should not fail"); - done(); + }).then(() => { + user.setUsername("user1"); + 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(); + } }); + }).catch(error => { + jfail(error); + fail("signUp should not fail"); + done(); }); + }); }); it('should reset password even if the new password contains user name while the policy allows', done => { @@ -616,14 +617,12 @@ describe("Password Policy: ", () => { var emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: options => { - request.get(options.link, { + requestp.get({ + uri: options.link, followRedirect: false, - }, (error, response, body) => { - if (error) { - jfail(error); - fail("Failed to get the reset link"); - return; - } + simple: false, + 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); @@ -634,31 +633,34 @@ describe("Password Policy: ", () => { } var token = match[1]; - request.post({ - url: "http://localhost:8378/1/apps/test/request_password_reset", + 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, - }, (error, response, body) => { - if (error) { - jfail(error); - fail("Failed to POST request password reset"); - return; - } + 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'); Parse.User.logIn("user1", "uuser11").then(function (user) { done(); - }, (err) => { + }).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: () => { @@ -673,25 +675,24 @@ describe("Password Policy: ", () => { 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', { - error: (err) => { - jfail(err); - fail("Reset password request should not fail"); - done(); - } - }); - }, error => { - jfail(error); - fail("signUp should not fail"); - done(); + }).then(() => { + user.setUsername("user1"); + 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(); + } }); + }).catch(error => { + jfail(error); + fail("signUp should not fail"); + done(); }); + }); }); }) From e82e1bf7b95b7c943ce215063c71ad278fc239f2 Mon Sep 17 00:00:00 2001 From: Bhaskar Reddy Yasa Date: Fri, 11 Nov 2016 18:13:52 +0530 Subject: [PATCH 10/24] passwordPolicy.validator split into two separate options - RegExp and Callback --- README.md | 9 ++- spec/PasswordPolicy.spec.js | 114 +++++++++++++++++++++++------- src/Config.js | 25 +++---- src/Controllers/UserController.js | 24 +++---- src/RestWrite.js | 3 +- 5 files changed, 118 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index d66f7ce94f..43edc155a4 100644 --- a/README.md +++ b/README.md @@ -280,9 +280,12 @@ var server = ParseServer({ }, // optional settings to enforce password policies passwordPolicy: { - // optional setting to enforce strong passwords - // can be a RegExp/String representing pattern to enforce or a function that return a bool - validator: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/, // enforce password with at least 8 char with at least 1 lower case, 1 upper case and 1 digit + // Two optional settings to enforce strong passwords. Either one or both can be specified. + // If both are specified, both checks must pass to accept the password + // 1. a RegExp representing the pattern to enforce + validatorPattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/, // enforce password with at least 8 char with at least 1 lower case, 1 upper case and 1 digit + // 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 //optional setting to set a validity duration for password reset links (in seconds) resetTokenValidityDuration: 24*60*60, // expire after 24 hours diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index bbdf41370c..eb1cf20f6d 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -3,7 +3,7 @@ const requestp = require('request-promise'); const Config = require('../src/Config'); -fdescribe("Password Policy: ", () => { +describe("Password Policy: ", () => { 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(); @@ -136,28 +136,44 @@ fdescribe("Password Policy: ", () => { }); }); - it('should fail if passwordPolicy.validator setting is invalid type', done => { + it('should fail if passwordPolicy.validatorPattern setting is invalid type', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validator: 1 // number is not a valid setting for validator + validatorPattern: "abc" // string is not a valid setting }, publicServerURL: "http://localhost:8378/1" }).then(() => { - fail('passwordPolicy.validator type test failed'); + fail('passwordPolicy.validatorPattern type test failed'); done(); }).catch(err => { - expect(err).toEqual('passwordPolicy.validator must be a RegExp, a string or a function.'); + expect(err).toEqual('passwordPolicy.validatorPattern must be a RegExp.'); done(); }); }); - it('signup should fail if password does not confirm to the policy enforced using RegExp', (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 confirm to the policy enforced using validatorPattern', (done) => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validator: /[0-9]+/ // password should contain at least one digit + validatorPattern: /[0-9]+/ // password should contain at least one digit }, publicServerURL: "http://localhost:8378/1" }).then(() => { @@ -174,12 +190,12 @@ fdescribe("Password Policy: ", () => { }) }); - it('signup should succeed if password confirms to the policy enforced using RegExp', (done) => { + it('signup should succeed if password confirms to the policy enforced using validatorPattern', (done) => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validator: /[0-9]+/ // password should contain at least one digit + validatorPattern: /[0-9]+/ // password should contain at least one digit }, publicServerURL: "http://localhost:8378/1" }).then(() => { @@ -187,7 +203,14 @@ fdescribe("Password Policy: ", () => { user.setPassword("1digit"); user.set('email', 'user1@parse.com'); user.signUp().then(() => { - done(); + Parse.User.logOut(); + Parse.User.logIn("user1", "1digit").then(function (user) { + done(); + }).catch((err) => { + jfail(err); + fail("Should be able to login"); + done(); + }); }).catch((error) => { fail('Should have succeeded as password confirms to the policy.'); done(); @@ -195,17 +218,17 @@ fdescribe("Password Policy: ", () => { }) }); - it('signup should fail if password does not confirm to the policy enforced using regex string', (done) => { + it('signup should fail if password does not confirm to the policy enforced using validatorCallback', (done) => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validator: "[A-Z]+" // password should contain at least one UPPER case letter + validatorCallback: password => false // just fail }, publicServerURL: "http://localhost:8378/1" }).then(() => { user.setUsername("user1"); - user.setPassword("all lower"); + user.setPassword("any"); user.set('email', 'user1@parse.com'); user.signUp().then(() => { fail('Should have failed as password does not confirm to the policy.'); @@ -217,12 +240,12 @@ fdescribe("Password Policy: ", () => { }) }); - it('signup should succeed if password confirms to the policy enforced using regex string', (done) => { + it('signup should succeed if password confirms to the policy enforced using validatorCallback', (done) => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validator: /[A-Z]+/ // password should contain at least one digit + validatorCallback: password => true // never fail }, publicServerURL: "http://localhost:8378/1" }).then(() => { @@ -230,7 +253,14 @@ fdescribe("Password Policy: ", () => { user.setPassword("oneUpper"); user.set('email', 'user1@parse.com'); user.signUp().then(() => { - done(); + Parse.User.logOut(); + Parse.User.logIn("user1", "oneUpper").then(function (user) { + done(); + }).catch((err) => { + jfail(err); + fail("Should be able to login"); + done(); + }); }).catch((error) => { fail('Should have succeeded as password confirms to the policy.'); done(); @@ -238,17 +268,18 @@ fdescribe("Password Policy: ", () => { }) }); - it('signup should fail if password does not confirm to the policy enforced using a callback function', (done) => { + it('signup should fail if password does not confirm to validatorPattern but succeeds validatorCallback', (done) => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validator: password => false // just fail + validatorPattern: /[A-Z]+/, // password should contain at least one UPPER case letter + validatorCallback: value => true }, publicServerURL: "http://localhost:8378/1" }).then(() => { user.setUsername("user1"); - user.setPassword("any"); + user.setPassword("all lower"); user.set('email', 'user1@parse.com'); user.signUp().then(() => { fail('Should have failed as password does not confirm to the policy.'); @@ -260,12 +291,13 @@ fdescribe("Password Policy: ", () => { }) }); - it('signup should succeed if password confirms to the policy enforced using a callback function', (done) => { + it('signup should fail if password does confirms to validatorPattern but fails validatorCallback', (done) => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validator: password => true // never fail + validatorPattern: /[A-Z]+/, // password should contain at least one UPPER case letter + validatorCallback: value => false }, publicServerURL: "http://localhost:8378/1" }).then(() => { @@ -273,7 +305,37 @@ fdescribe("Password Policy: ", () => { user.setPassword("oneUpper"); user.set('email', 'user1@parse.com'); user.signUp().then(() => { + fail('Should have failed as password does not confirm to the policy.'); + done(); + }).catch((error) => { + expect(error.code).toEqual(142); done(); + }); + }) + }); + + it('signup should succeed if password confirms 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: value => 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(); + Parse.User.logIn("user1", "oneUpper").then(function (user) { + done(); + }).catch((err) => { + jfail(err); + fail("Should be able to login"); + done(); + }); }).catch((error) => { fail('Should have succeeded as password confirms to the policy.'); done(); @@ -341,7 +403,7 @@ fdescribe("Password Policy: ", () => { verifyUserEmails: false, emailAdapter: emailAdapter, passwordPolicy: { - validator: /[0-9]+/ // password should contain at least one digit + validatorPattern: /[0-9]+/ // password should contain at least one digit }, publicServerURL: "http://localhost:8378/1" }).then(() => { @@ -424,7 +486,7 @@ fdescribe("Password Policy: ", () => { verifyUserEmails: false, emailAdapter: emailAdapter, passwordPolicy: { - validator: /[0-9]+/ // password should contain at least one digit + validatorPattern: /[0-9]+/ // password should contain at least one digit }, publicServerURL: "http://localhost:8378/1" }).then(() => { @@ -468,7 +530,7 @@ fdescribe("Password Policy: ", () => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validator: /[0-9]+/, + validatorPattern: /[0-9]+/, doNotAllowUsername: true }, publicServerURL: "http://localhost:8378/1" @@ -512,7 +574,7 @@ fdescribe("Password Policy: ", () => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validator: /[0-9]+/ + validatorPattern: /[0-9]+/ }, publicServerURL: "http://localhost:8378/1" }).then(() => { @@ -671,7 +733,7 @@ fdescribe("Password Policy: ", () => { verifyUserEmails: false, emailAdapter: emailAdapter, passwordPolicy: { - validator: /[0-9]+/, + validatorPattern: /[0-9]+/, doNotAllowUsername: false }, publicServerURL: "http://localhost:8378/1" diff --git a/src/Config.js b/src/Config.js index 55345848ba..1204d92087 100644 --- a/src/Config.js +++ b/src/Config.js @@ -123,8 +123,12 @@ export class Config { throw 'passwordPolicy.resetTokenValidityDuration must be a positive number'; } - if(passwordPolicy.validator && typeof passwordPolicy.validator !== 'string' && !(passwordPolicy.validator instanceof RegExp) && typeof passwordPolicy.validator !== 'function' ) { - throw 'passwordPolicy.validator must be a RegExp, a string or a function.'; + if(passwordPolicy.validatorPattern && !(passwordPolicy.validatorPattern instanceof RegExp)) { + throw 'passwordPolicy.validatorPattern must be a RegExp.'; + } + + if(passwordPolicy.validatorCallback && typeof passwordPolicy.validatorCallback !== 'function' ) { + throw 'passwordPolicy.validatorCallback must be a function.'; } if(passwordPolicy.doNotAllowUsername && typeof passwordPolicy.doNotAllowUsername !== 'boolean') { @@ -133,20 +137,11 @@ export class Config { } } - // if the passwordPolicy.validator is configured as a string or regex then convert it to a function to process the pattern + // if the passwordPolicy.validatorPattern is configured then setup a callback to process the pattern static setupPasswordValidator(passwordPolicy) { - if (passwordPolicy && passwordPolicy.validator) { - if (typeof passwordPolicy.validator === 'string') { - const pattern = new RegExp(passwordPolicy.validator); - passwordPolicy.validator = (value) => { - return pattern.test(value) - } - } - else if (passwordPolicy.validator instanceof RegExp) { - const pattern = passwordPolicy.validator; - passwordPolicy.validator = (value) => { - return pattern.test(value) - } + if (passwordPolicy && passwordPolicy.validatorPattern) { + passwordPolicy.patternValidator = (value) => { + return passwordPolicy.validatorPattern.test(value) } } } diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 637a0ce4b3..147abc704a 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -171,18 +171,18 @@ export class UserController extends AdaptableController { updatePassword(username, token, password, config) { return this.checkResetTokenValidity(username, token) - .then(user => updateUserPassword(user.objectId, password, this.config)) - // clear reset password token - .then(() => this.config.database.update('_User', { username }, { - _perishable_token: {__op: 'Delete'}, - _perishable_token_expires_at: {__op: 'Delete'} - }),(error) => { - if (error.message) { // in case of Parse.Error, fail with the error message only - return Promise.reject(error.message); - } else { - return Promise.reject(error); - } - }); + .then(user => updateUserPassword(user.objectId, password, this.config)) + // clear reset password token + .then(() => this.config.database.update('_User', {username}, { + _perishable_token: {__op: 'Delete'}, + _perishable_token_expires_at: {__op: 'Delete'} + })).catch((error) => { + if (error.message) { // in case of Parse.Error, fail with the error message only + return Promise.reject(error.message); + } else { + return Promise.reject(error); + } + }); } defaultVerificationEmail({link, user, appName, }) { diff --git a/src/RestWrite.js b/src/RestWrite.js index 10ef60bb58..17112afc84 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -376,7 +376,8 @@ RestWrite.prototype.transformUser = function() { const policyError = 'Password does not confirm to the Password Policy.'; // check whether the password confirms to the policy - if (this.config.passwordPolicy.validator && !this.config.passwordPolicy.validator(this.data.password)) { + 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)) } From 2f0fcd7916849aa94668af038130c1dd5128f65c Mon Sep 17 00:00:00 2001 From: Bhaskar Yasa Date: Mon, 7 Nov 2016 12:08:08 +0530 Subject: [PATCH 11/24] Introducing passwordPolicy with resetTokenValidityDuration --- README.md | 4 + spec/PasswordPolicy.spec.js | 138 ++++++++++++++++++ src/Adapters/Storage/Mongo/MongoTransform.js | 14 ++ .../Postgres/PostgresStorageAdapter.js | 8 +- src/Config.js | 22 ++- src/Controllers/DatabaseController.js | 3 +- src/Controllers/UserController.js | 17 ++- src/ParseServer.js | 2 + src/cli/definitions/parse-server.js | 5 + 9 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 spec/PasswordPolicy.spec.js diff --git a/README.md b/README.md index 5b01aba2bd..4f1d9e02c4 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo * `sessionLength` - The length of time in seconds that a session should be valid for. Defaults to 31536000 seconds (1 year). * `revokeSessionOnPasswordReset` - When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions. * `accountLockout` - Lock account when a malicious user is attempting to determine an account password by trial and error. +* `passwordPolicy` - Optional password policy rules to enforce. ##### Logging @@ -277,6 +278,9 @@ var server = ParseServer({ duration: 5, // duration policy setting determines the number of minutes that a locked-out account remains locked out before automatically becoming unlocked. Set it to a value greater than 0 and less than 100000. threshold: 3, // threshold policy setting determines the number of failed sign-in attempts that will cause a user account to be locked. Set it to an integer value greater than 0 and less than 1000. }, + passwordPolicy: { + resetTokenValidityDuration: 24*60*60, // password reset link will expire after the set duration (in seconds) + } }); ``` diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js new file mode 100644 index 0000000000..95e407dd4b --- /dev/null +++ b/spec/PasswordPolicy.spec.js @@ -0,0 +1,138 @@ +"use strict"; + +const request = require('request'); +const Config = require('../src/Config'); + +describe("Password Token Expiry: ", () => { + + 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: 'passwordResetToken', + 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(user => { + Parse.User.requestPasswordReset("user@parse.com"); + }) + .then(() => { + // wait for a bit more than the validity duration set + setTimeout(() => { + expect(sendEmailOptions).not.toBeUndefined(); + + request.get(sendEmailOptions.link, { + followRedirect: false, + }, (error, response, body) => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); + done(); + }); + }, 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: 'passwordResetToken', + 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(user => { + Parse.User.requestPasswordReset("user@parse.com"); + }) + .then(() => { + // wait for a bit but less than the validity duration + setTimeout(() => { + expect(sendEmailOptions).not.toBeUndefined(); + + request.get(sendEmailOptions.link, { + followRedirect: false, + }, (error, response, body) => { + expect(response.statusCode).toEqual(302); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=testResetTokenValidity/; + expect(response.body.match(re)).not.toBe(null); + done(); + }); + }, 1000); + }).catch((err) => { + jfail(err); + done(); + }); + }); + + it('should fail if resetTokenValidityDuration is not a number', done => { + reconfigureServer({ + appName: 'passwordResetToken', + 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 resetTokenValidityDuration is zero or a negative number', done => { + reconfigureServer({ + appName: 'passwordResetToken', + 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(); + }); + }); + +}) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 7d14421272..f3de0b440b 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -66,6 +66,10 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc case '_failed_login_count': key = '_failed_login_count'; break; + case '_perishable_token_expires_at': + key = '_perishable_token_expires_at'; + timeField = true; + break; case '_rperm': case '_wperm': return {key: key, value: restValue}; @@ -171,6 +175,11 @@ function transformQueryKeyValue(className, key, value, schema) { case '_failed_login_count': return {key, value}; case 'sessionToken': return {key: '_session_token', value} + case '_perishable_token_expires_at': + if (valueAsDate(value)) { + return { key: '_perishable_token_expires_at', value: valueAsDate(value) } + } + break; case '_rperm': case '_wperm': case '_perishable_token': @@ -250,6 +259,10 @@ const parseObjectKeyValueToMongoObjectKeyValue = (restKey, restValue, schema) => transformedValue = transformTopLevelAtom(restValue); coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue return {key: '_account_lockout_expires_at', value: coercedToDate}; + case '_perishable_token_expires_at': + transformedValue = transformTopLevelAtom(restValue); + coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue + return { key: '_perishable_token_expires_at', value: coercedToDate }; case '_failed_login_count': case '_rperm': case '_wperm': @@ -748,6 +761,7 @@ const mongoObjectToParseObject = (className, mongoObject, schema) => { break; case '_email_verify_token': case '_perishable_token': + case '_perishable_token_expires_at': case '_tombstone': case '_email_verify_token_expires_at': case '_account_lockout_expires_at': diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 5870e33502..74bc1935fb 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -466,6 +466,7 @@ export class PostgresStorageAdapter { fields._account_lockout_expires_at = {type: 'Date'}; fields._failed_login_count = {type: 'Number'}; fields._perishable_token = {type: 'String'}; + fields._perishable_token_expires_at = {type: 'Date'}; } let index = 2; let relations = []; @@ -691,7 +692,8 @@ export class PostgresStorageAdapter { } } - if (fieldName === '_account_lockout_expires_at') { + if (fieldName === '_account_lockout_expires_at'|| + fieldName === '_perishable_token_expires_at') { if (object[fieldName]) { valuesArray.push(object[fieldName].iso); } else { @@ -1068,6 +1070,10 @@ export class PostgresStorageAdapter { if (object._account_lockout_expires_at) { object._account_lockout_expires_at = { __type: 'Date', iso: object._account_lockout_expires_at.toISOString() }; } + if (object._perishable_token_expires_at) { + object._perishable_token_expires_at = { __type: 'Date', iso: object._perishable_token_expires_at.toISOString() }; + } + for (let fieldName in object) { if (object[fieldName] === null) { diff --git a/src/Config.js b/src/Config.js index 016a3fe0ac..8426bcaf3c 100644 --- a/src/Config.js +++ b/src/Config.js @@ -50,6 +50,7 @@ export class Config { this.preventLoginWithUnverifiedEmail = cacheInfo.preventLoginWithUnverifiedEmail; this.emailVerifyTokenValidityDuration = cacheInfo.emailVerifyTokenValidityDuration; this.accountLockout = cacheInfo.accountLockout; + this.passwordPolicy = cacheInfo.passwordPolicy; this.appName = cacheInfo.appName; this.analyticsController = cacheInfo.analyticsController; @@ -79,7 +80,8 @@ export class Config { expireInactiveSessions, sessionLength, emailVerifyTokenValidityDuration, - accountLockout + accountLockout, + passwordPolicy }) { const emailAdapter = userController.adapter; if (verifyUserEmails) { @@ -88,6 +90,8 @@ export class Config { this.validateAccountLockoutPolicy(accountLockout); + this.validatePasswordPolicy(passwordPolicy); + if (typeof revokeSessionOnPasswordReset !== 'boolean') { throw 'revokeSessionOnPasswordReset must be a boolean value'; } @@ -113,6 +117,14 @@ export class Config { } } + static validatePasswordPolicy(passwordPolicy) { + if (passwordPolicy) { + if (passwordPolicy.resetTokenValidityDuration !== undefined && (typeof passwordPolicy.resetTokenValidityDuration !== 'number' || passwordPolicy.resetTokenValidityDuration <= 0)) { + throw 'passwordPolicy.resetTokenValidityDuration must be a positive number'; + } + } + } + static validateEmailConfiguration({emailAdapter, appName, publicServerURL, emailVerifyTokenValidityDuration}) { if (!emailAdapter) { throw 'An emailAdapter is required for e-mail verification and password resets.'; @@ -163,6 +175,14 @@ export class Config { return new Date(now.getTime() + (this.emailVerifyTokenValidityDuration*1000)); } + generatePasswordResetTokenExpiresAt() { + if (!this.passwordPolicy || !this.passwordPolicy.resetTokenValidityDuration) { + return undefined; + } + const now = new Date(); + return new Date(now.getTime() + (this.passwordPolicy.resetTokenValidityDuration * 1000)); + } + generateSessionExpiresAt() { if (!this.expireInactiveSessions) { return undefined; diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 986a71d5b7..110bd39b5b 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -169,6 +169,7 @@ const filterSensitiveData = (isMaster, aclGroup, className, object) => { } delete object._email_verify_token; delete object._perishable_token; + delete object._perishable_token_expires_at; delete object._tombstone; delete object._email_verify_token_expires_at; delete object._failed_login_count; @@ -189,7 +190,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']; +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 isSpecialUpdateKey = key => { return specialKeysForUpdate.indexOf(key) >= 0; diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index fbdf707a4e..04fafd6d5c 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -77,6 +77,12 @@ export class UserController extends AdaptableController { if (results.length != 1) { throw undefined; } + + if (this.config.passwordPolicy && this.config.passwordPolicy.resetTokenValidityDuration) { + if (results[0]._perishable_token_expires_at < new Date()) + throw 'The password reset link has expired'; + } + return results[0]; }); } @@ -125,7 +131,13 @@ export class UserController extends AdaptableController { } setPasswordResetToken(email) { - return this.config.database.update('_User', { $or: [{email}, {username: email, email: {$exists: false}}] }, { _perishable_token: randomString(25) }, {}, true) + const token = { _perishable_token: randomString(25) }; + + if (this.config.passwordPolicy && this.config.passwordPolicy.resetTokenValidityDuration) { + token._perishable_token_expires_at = Parse._encode(this.config.generatePasswordResetTokenExpiresAt()); + } + + return this.config.database.update('_User', { $or: [{email}, {username: email, email: {$exists: false}}] }, token, {}, true) } sendPasswordResetEmail(email) { @@ -162,7 +174,8 @@ export class UserController extends AdaptableController { .then(user => updateUserPassword(user.objectId, password, this.config)) // clear reset password token .then(() => this.config.database.update('_User', { username }, { - _perishable_token: {__op: 'Delete'} + _perishable_token: {__op: 'Delete'}, + _perishable_token_expires_at: {__op: 'Delete'} })); } diff --git a/src/ParseServer.js b/src/ParseServer.js index faa544dffb..6b94310c73 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -125,6 +125,7 @@ class ParseServer { preventLoginWithUnverifiedEmail = defaults.preventLoginWithUnverifiedEmail, emailVerifyTokenValidityDuration, accountLockout, + passwordPolicy, cacheAdapter, emailAdapter, publicServerURL, @@ -210,6 +211,7 @@ class ParseServer { preventLoginWithUnverifiedEmail: preventLoginWithUnverifiedEmail, emailVerifyTokenValidityDuration: emailVerifyTokenValidityDuration, accountLockout: accountLockout, + passwordPolicy: passwordPolicy, allowClientClassCreation: allowClientClassCreation, authDataManager: authDataManager(oauth, enableAnonymousUsers), appName: appName, diff --git a/src/cli/definitions/parse-server.js b/src/cli/definitions/parse-server.js index 7bd1aff02c..3dd1c54afb 100644 --- a/src/cli/definitions/parse-server.js +++ b/src/cli/definitions/parse-server.js @@ -136,6 +136,11 @@ export default { help: "account lockout policy for failed login attempts", action: objectParser }, + "passwordPolicy": { + env: "PARSE_SERVER_PASSWORD_POLICY", + help: "Password policy for reset link expiry", + action: objectParser + }, "appName": { env: "PARSE_SERVER_APP_NAME", help: "Sets the app name" From f41747bac4c1b8c321177373127acedd2f78b107 Mon Sep 17 00:00:00 2001 From: bhaskaryasa Date: Tue, 8 Nov 2016 23:08:16 +0530 Subject: [PATCH 12/24] validator added to passwordPolicy validator support in passwordPolicy that can be used to enforce strong passwords. --- src/Config.js | 22 ++++++++++++++++++++++ src/Controllers/UserController.js | 11 ++++++++--- src/ParseServer.js | 1 + src/RestWrite.js | 6 ++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/Config.js b/src/Config.js index 8426bcaf3c..281712e392 100644 --- a/src/Config.js +++ b/src/Config.js @@ -122,6 +122,28 @@ export class Config { if (passwordPolicy.resetTokenValidityDuration !== undefined && (typeof passwordPolicy.resetTokenValidityDuration !== 'number' || passwordPolicy.resetTokenValidityDuration <= 0)) { throw 'passwordPolicy.resetTokenValidityDuration must be a positive number'; } + + if(passwordPolicy.validator && typeof passwordPolicy.validator !== 'string' && !(passwordPolicy.validator instanceof RegExp) && typeof passwordPolicy.validator !== 'function' ) { + throw 'passwordPolicy.validator must be a RegExp, string or function.'; + } + } + } + + // if the passwordPolicy.validator is configured as a string or regex then convert it to a function to process the pattern + static setupPasswordValidator(passwordPolicy) { + if (passwordPolicy && passwordPolicy.validator) { + if (typeof passwordPolicy.validator === 'string') { + const pattern = new RegExp(passwordPolicy.validator); + passwordPolicy.validator = (value) => { + return pattern.test(value) + } + } + else if (passwordPolicy.validator instanceof RegExp) { + const pattern = passwordPolicy.validator; + passwordPolicy.validator = (value) => { + return pattern.test(value) + } + } } } diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 04fafd6d5c..d430ceace5 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -202,9 +202,14 @@ export class UserController extends AdaptableController { // Mark this private function updateUserPassword(userId, password, config) { - return rest.update(config, Auth.master(config), '_User', userId, { - password: password - }); + // check if the password confirms to the defined password policy if configured + if (config.passwordPolicy && config.passwordPolicy.validator && !config.passwordPolicy.validator(password)) { + return Promise.reject('Password does not confirm to the Password Policy.') + } + + return rest.update(config, Auth.master(config), '_User', userId, { + password: password + }); } export default UserController; diff --git a/src/ParseServer.js b/src/ParseServer.js index 6b94310c73..4cd4fd3d9a 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -235,6 +235,7 @@ class ParseServer { Config.validate(AppCache.get(appId)); this.config = AppCache.get(appId); + Config.setupPasswordValidator(this.config.passwordPolicy); hooksController.load(); // Note: Tests will start to fail if any validation happens after this is called. diff --git a/src/RestWrite.js b/src/RestWrite.js index 14105e6d89..dcf48c2e7d 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -368,6 +368,12 @@ RestWrite.prototype.transformUser = function() { if (!this.data.password) { return; } + + // check if the password confirms to the defined password policy if configured + if (this.config.passwordPolicy && this.config.passwordPolicy.validator && !this.config.passwordPolicy.validator(this.data.password)) { + return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, 'Password does not confirm to the Password Policy.')) + } + if (this.query && !this.auth.isMaster ) { this.storage['clearSessions'] = true; this.storage['generateNewSession'] = true; From 3cd904eb0cfd84f37c3724e0a829c62b70725bed Mon Sep 17 00:00:00 2001 From: bhaskaryasa Date: Wed, 9 Nov 2016 10:01:58 +0530 Subject: [PATCH 13/24] Add some unit tests for passwordPolicy.validator --- spec/PasswordPolicy.spec.js | 219 +++++++++++++++++++++++++++++++++++- 1 file changed, 215 insertions(+), 4 deletions(-) diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index 95e407dd4b..219ab0da5d 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -17,7 +17,7 @@ describe("Password Token Expiry: ", () => { } } reconfigureServer({ - appName: 'passwordResetToken', + appName: 'passwordPolicy', emailAdapter: emailAdapter, passwordPolicy: { resetTokenValidityDuration: 0.5, // 0.5 second @@ -63,7 +63,7 @@ describe("Password Token Expiry: ", () => { } } reconfigureServer({ - appName: 'passwordResetToken', + appName: 'passwordPolicy', emailAdapter: emailAdapter, passwordPolicy: { resetTokenValidityDuration: 5, // 5 seconds @@ -101,7 +101,7 @@ describe("Password Token Expiry: ", () => { it('should fail if resetTokenValidityDuration is not a number', done => { reconfigureServer({ - appName: 'passwordResetToken', + appName: 'passwordPolicy', passwordPolicy: { resetTokenValidityDuration: "not a number" }, @@ -119,7 +119,7 @@ describe("Password Token Expiry: ", () => { it('should fail if resetTokenValidityDuration is zero or a negative number', done => { reconfigureServer({ - appName: 'passwordResetToken', + appName: 'passwordPolicy', passwordPolicy: { resetTokenValidityDuration: 0 }, @@ -135,4 +135,215 @@ describe("Password Token Expiry: ", () => { }); }); + it('signup should fail if password does not confirm to the policy enforced using RegExp', (done) => { + const user = new Parse.User(); + reconfigureServer({ + appName: 'passwordPolicy', + passwordPolicy: { + validator: /[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 confirm to the policy.'); + done(); + }, (error) => { + expect(error.code).toEqual(142); + done(); + }); + }) + }); + + it('signup should succeed if password confirms to the policy enforced using RegExp', (done) => { + const user = new Parse.User(); + reconfigureServer({ + appName: 'passwordPolicy', + passwordPolicy: { + validator: /[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(() => { + done(); + }, (error) => { + fail('Should have succeeded as password confirms to the policy.'); + done(); + }); + }) + }); + + it('signup should fail if password does not confirm to the policy enforced using regex string', (done) => { + const user = new Parse.User(); + reconfigureServer({ + appName: 'passwordPolicy', + passwordPolicy: { + validator: "[A-Z]+" // password should contain at least one UPPER case letter + }, + 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 confirm to the policy.'); + done(); + }, (error) => { + expect(error.code).toEqual(142); + done(); + }); + }) + }); + + it('signup should succeed if password confirms to the policy enforced using regex string', (done) => { + const user = new Parse.User(); + reconfigureServer({ + appName: 'passwordPolicy', + passwordPolicy: { + validator: /[A-Z]+/ // password should contain at least one digit + }, + publicServerURL: "http://localhost:8378/1" + }).then(() => { + user.setUsername("user1"); + user.setPassword("oneUpper"); + user.set('email', 'user1@parse.com'); + user.signUp().then(() => { + done(); + }, (error) => { + fail('Should have succeeded as password confirms to the policy.'); + done(); + }); + }) + }); + + it('signup should fail if password does not confirm to the policy enforced using a callback function', (done) => { + const user = new Parse.User(); + reconfigureServer({ + appName: 'passwordPolicy', + passwordPolicy: { + validator: password => 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 confirm to the policy.'); + done(); + }, (error) => { + expect(error.code).toEqual(142); + done(); + }); + }) + }); + + it('signup should succeed if password confirms to the policy enforced using a callback function', (done) => { + const user = new Parse.User(); + reconfigureServer({ + appName: 'passwordPolicy', + passwordPolicy: { + validator: password => true // never fail + }, + publicServerURL: "http://localhost:8378/1" + }).then(() => { + user.setUsername("user1"); + user.setPassword("oneUpper"); + user.set('email', 'user1@parse.com'); + user.signUp().then(() => { + done(); + }, (error) => { + fail('Should have succeeded as password confirms to the policy.'); + done(); + }); + }) + }); + + it('should reset password if new password confirms to password policy', done => { + var user = new Parse.User(); + var emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: options => { + request.get(options.link, { + followRedirect: false, + }, (error, response, body) => { + if (error) { + jfail(error); + fail("Failed to get the reset link"); + return; + } + 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); + if (!match) { + fail("should have a token"); + done(); + return; + } + var token = match[1]; + + request.post({ + url: "http://localhost:8378/1/apps/test/request_password_reset", + body: `new_password=has2init&token=${token}&username=user1`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + followRedirect: false, + }, (error, response, body) => { + if (error) { + jfail(error); + fail("Failed to POST request password reset"); + return; + } + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html'); + + Parse.User.logIn("user1", "has2init").then(function (user) { + done(); + }, (err) => { + jfail(err); + fail("should login with new password"); + done(); + }); + + }); + }); + }, + sendMail: () => { + } + } + reconfigureServer({ + appName: 'passwordPolicy', + verifyUserEmails: false, + emailAdapter: emailAdapter, + passwordPolicy: { + validator: /[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', { + error: (err) => { + jfail(err); + fail("Reset password request should not fail"); + done(); + } + }); + }, error => { + jfail(err); + fail("signUp should not fail"); + done(); + }); + }); + }); + }) From f385f6a7cb12cb8340c6a9b4a73adece4144a0c9 Mon Sep 17 00:00:00 2001 From: bhaskaryasa Date: Wed, 9 Nov 2016 12:30:57 +0530 Subject: [PATCH 14/24] Add unit test for reset password failure for non-conformance --- spec/PasswordPolicy.spec.js | 106 +++++++++++++++++++++++++++++++++++- src/Config.js | 2 +- 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index 219ab0da5d..4c80d7b6b3 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -99,7 +99,7 @@ describe("Password Token Expiry: ", () => { }); }); - it('should fail if resetTokenValidityDuration is not a number', done => { + it('should fail if passwordPolicy.resetTokenValidityDuration is not a number', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { @@ -117,7 +117,7 @@ describe("Password Token Expiry: ", () => { }); }); - it('should fail if resetTokenValidityDuration is zero or a negative number', done => { + it('should fail if passwordPolicy.resetTokenValidityDuration is zero or a negative number', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { @@ -135,6 +135,24 @@ describe("Password Token Expiry: ", () => { }); }); + it('should fail if passwordPolicy.validator setting is invalid type', done => { + reconfigureServer({ + appName: 'passwordPolicy', + passwordPolicy: { + validator: 1 // number is not a valid setting for validator + }, + publicServerURL: "http://localhost:8378/1" + }) + .then(() => { + fail('passwordPolicy.validator type test failed'); + done(); + }) + .catch(err => { + expect(err).toEqual('passwordPolicy.validator must be a RegExp, a string or a function.'); + done(); + }); + }); + it('signup should fail if password does not confirm to the policy enforced using RegExp', (done) => { const user = new Parse.User(); reconfigureServer({ @@ -339,7 +357,89 @@ describe("Password Token Expiry: ", () => { } }); }, error => { - jfail(err); + jfail(error); + fail("signUp should not fail"); + done(); + }); + }); + }); + + it('should fail to reset password if the new password does not confirm to password policy', done => { + var user = new Parse.User(); + var emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: options => { + request.get(options.link, { + followRedirect: false, + }, (error, response, body) => { + if (error) { + jfail(error); + fail("Failed to get the reset link"); + return; + } + 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); + if (!match) { + fail("should have a token"); + done(); + return; + } + var token = match[1]; + + request.post({ + url: "http://localhost:8378/1/apps/test/request_password_reset", + body: `new_password=hasnodigit&token=${token}&username=user1`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + followRedirect: false, + }, (error, response, body) => { + if (error) { + jfail(error); + fail("Failed to POST request password reset"); + return; + } + 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`); + + Parse.User.logIn("user1", "has 1 digit").then(function (user) { + done(); + }, (err) => { + jfail(err); + fail("should login with old password"); + done(); + }); + + }); + }); + }, + sendMail: () => { + } + } + reconfigureServer({ + appName: 'passwordPolicy', + verifyUserEmails: false, + emailAdapter: emailAdapter, + passwordPolicy: { + validator: /[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', { + error: (err) => { + jfail(err); + fail("Reset password request should not fail"); + done(); + } + }); + }, error => { + jfail(error); fail("signUp should not fail"); done(); }); diff --git a/src/Config.js b/src/Config.js index 281712e392..e0933c07b9 100644 --- a/src/Config.js +++ b/src/Config.js @@ -124,7 +124,7 @@ export class Config { } if(passwordPolicy.validator && typeof passwordPolicy.validator !== 'string' && !(passwordPolicy.validator instanceof RegExp) && typeof passwordPolicy.validator !== 'function' ) { - throw 'passwordPolicy.validator must be a RegExp, string or function.'; + throw 'passwordPolicy.validator must be a RegExp, a string or a function.'; } } } From 5b868f68b0b097a0b5fb819674da84186f11afca Mon Sep 17 00:00:00 2001 From: bhaskaryasa Date: Wed, 9 Nov 2016 15:21:15 +0530 Subject: [PATCH 15/24] Update README.md for passwordPolicy --- README.md | 7 ++++++- spec/PasswordPolicy.spec.js | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4f1d9e02c4..e0cb11a7e4 100644 --- a/README.md +++ b/README.md @@ -278,8 +278,13 @@ var server = ParseServer({ duration: 5, // duration policy setting determines the number of minutes that a locked-out account remains locked out before automatically becoming unlocked. Set it to a value greater than 0 and less than 100000. threshold: 3, // threshold policy setting determines the number of failed sign-in attempts that will cause a user account to be locked. Set it to an integer value greater than 0 and less than 1000. }, + // optional settings to enforce password policies passwordPolicy: { - resetTokenValidityDuration: 24*60*60, // password reset link will expire after the set duration (in seconds) + // optional setting to enforce strong passwords + // can be a RegExp/String representing pattern to enforce or a function that return a bool + validator: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/, // enforce password with at least 8 char with at least 1 lower case, 1 upper case and 1 digit + //optional setting to set a validity duration for password reset links (in seconds) + resetTokenValidityDuration: 24*60*60, // expire after 24 hours } }); ``` diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index 4c80d7b6b3..820c27251a 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -3,7 +3,7 @@ const request = require('request'); const Config = require('../src/Config'); -describe("Password Token Expiry: ", () => { +describe("Password Policy: ", () => { 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(); From 1c1a515ea873a8c291ccb3d7f6279dc3ff831bfb Mon Sep 17 00:00:00 2001 From: Bhaskar Reddy Yasa Date: Thu, 10 Nov 2016 16:38:11 +0530 Subject: [PATCH 16/24] Added code to handle Parse.Error from rest.update in UserController.updatePassword thus avoid duplicate check for password validator in updateUserPassword. --- src/Controllers/UserController.js | 13 +++++++------ src/cli/definitions/parse-server.js | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index d430ceace5..637a0ce4b3 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -176,7 +176,13 @@ export class UserController extends AdaptableController { .then(() => this.config.database.update('_User', { username }, { _perishable_token: {__op: 'Delete'}, _perishable_token_expires_at: {__op: 'Delete'} - })); + }),(error) => { + if (error.message) { // in case of Parse.Error, fail with the error message only + return Promise.reject(error.message); + } else { + return Promise.reject(error); + } + }); } defaultVerificationEmail({link, user, appName, }) { @@ -202,11 +208,6 @@ export class UserController extends AdaptableController { // Mark this private function updateUserPassword(userId, password, config) { - // check if the password confirms to the defined password policy if configured - if (config.passwordPolicy && config.passwordPolicy.validator && !config.passwordPolicy.validator(password)) { - return Promise.reject('Password does not confirm to the Password Policy.') - } - return rest.update(config, Auth.master(config), '_User', userId, { password: password }); diff --git a/src/cli/definitions/parse-server.js b/src/cli/definitions/parse-server.js index 3dd1c54afb..e4c760f407 100644 --- a/src/cli/definitions/parse-server.js +++ b/src/cli/definitions/parse-server.js @@ -138,7 +138,7 @@ export default { }, "passwordPolicy": { env: "PARSE_SERVER_PASSWORD_POLICY", - help: "Password policy for reset link expiry", + help: "Password policy for enforcing password related rules", action: objectParser }, "appName": { From bd1673d6e958bee7bd4992a66532daff5e9a1659 Mon Sep 17 00:00:00 2001 From: Bhaskar Reddy Yasa Date: Thu, 10 Nov 2016 22:45:03 +0530 Subject: [PATCH 17/24] Added optional setting to disallow username in password --- README.md | 1 + spec/PasswordPolicy.spec.js | 250 +++++++++++++++++++++++++++++++++++- src/Config.js | 4 + src/RestWrite.js | 41 +++++- 4 files changed, 289 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e0cb11a7e4..d66f7ce94f 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,7 @@ var server = ParseServer({ // optional setting to enforce strong passwords // can be a RegExp/String representing pattern to enforce or a function that return a bool validator: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/, // enforce password with at least 8 char with at least 1 lower case, 1 upper case and 1 digit + doNotAllowUsername: true, // optional setting to disallow username in passwords //optional setting to set a validity duration for password reset links (in seconds) resetTokenValidityDuration: 24*60*60, // expire after 24 hours } diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index 820c27251a..367a11b32c 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -3,7 +3,7 @@ const request = require('request'); const Config = require('../src/Config'); -describe("Password Policy: ", () => { +fdescribe("Password Policy: ", () => { 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(); @@ -446,4 +446,252 @@ describe("Password Policy: ", () => { }); }); + 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: { + validator: /[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(); + }, (error) => { + expect(error.code).toEqual(142); + 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(); + }, (error) => { + 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: { + validator: /[0-9]+/ + }, + publicServerURL: "http://localhost:8378/1" + }).then(() => { + user.setUsername("user1"); + user.setPassword("user1"); + user.set('email', 'user1@parse.com'); + user.signUp().then(() => { + done(); + }, (error) => { + 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 => { + var user = new Parse.User(); + var emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: options => { + request.get(options.link, { + followRedirect: false, + }, (error, response, body) => { + if (error) { + jfail(error); + fail("Failed to get the reset link"); + return; + } + 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); + if (!match) { + fail("should have a token"); + done(); + return; + } + var token = match[1]; + + request.post({ + url: "http://localhost:8378/1/apps/test/request_password_reset", + body: `new_password=xuser12&token=${token}&username=user1`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + followRedirect: false, + }, (error, response, body) => { + if (error) { + jfail(error); + fail("Failed to POST request password reset"); + return; + } + 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`); + + Parse.User.logIn("user1", "r@nd0m").then(function (user) { + done(); + }, (err) => { + jfail(err); + fail("should login with old password"); + 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', { + error: (err) => { + jfail(err); + fail("Reset password request should not fail"); + done(); + } + }); + }, error => { + jfail(error); + fail("signUp should not fail"); + done(); + }); + }); + }); + + it('should reset password even if the new password contains user name while the policy allows', done => { + var user = new Parse.User(); + var emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: options => { + request.get(options.link, { + followRedirect: false, + }, (error, response, body) => { + if (error) { + jfail(error); + fail("Failed to get the reset link"); + return; + } + 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); + if (!match) { + fail("should have a token"); + done(); + return; + } + var token = match[1]; + + request.post({ + url: "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, + }, (error, response, body) => { + if (error) { + jfail(error); + fail("Failed to POST request password reset"); + return; + } + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html'); + + Parse.User.logIn("user1", "uuser11").then(function (user) { + done(); + }, (err) => { + jfail(err); + fail("should login with new password"); + done(); + }); + + }); + }); + }, + sendMail: () => { + } + } + reconfigureServer({ + appName: 'passwordPolicy', + verifyUserEmails: false, + emailAdapter: emailAdapter, + passwordPolicy: { + validator: /[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', { + error: (err) => { + jfail(err); + fail("Reset password request should not fail"); + done(); + } + }); + }, error => { + jfail(error); + fail("signUp should not fail"); + done(); + }); + }); + }); + }) diff --git a/src/Config.js b/src/Config.js index e0933c07b9..55345848ba 100644 --- a/src/Config.js +++ b/src/Config.js @@ -126,6 +126,10 @@ export class Config { if(passwordPolicy.validator && typeof passwordPolicy.validator !== 'string' && !(passwordPolicy.validator instanceof RegExp) && typeof passwordPolicy.validator !== 'function' ) { throw 'passwordPolicy.validator must be a RegExp, a string or a function.'; } + + if(passwordPolicy.doNotAllowUsername && typeof passwordPolicy.doNotAllowUsername !== 'boolean') { + throw 'passwordPolicy.doNotAllowUsername must be a boolean value.'; + } } } diff --git a/src/RestWrite.js b/src/RestWrite.js index dcf48c2e7d..10ef60bb58 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -369,18 +369,47 @@ RestWrite.prototype.transformUser = function() { return; } + let defer = Promise.resolve(); + // check if the password confirms to the defined password policy if configured - if (this.config.passwordPolicy && this.config.passwordPolicy.validator && !this.config.passwordPolicy.validator(this.data.password)) { - return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, 'Password does not confirm to the Password Policy.')) + if (this.config.passwordPolicy) { + const policyError = 'Password does not confirm to the Password Policy.'; + + // check whether the password confirms to the policy + if (this.config.passwordPolicy.validator && !this.config.passwordPolicy.validator(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) { this.storage['clearSessions'] = true; this.storage['generateNewSession'] = true; } - return passwordCrypto.hash(this.data.password).then((hashedPassword) => { - this.data._hashed_password = hashedPassword; - delete this.data.password; + + return defer.then(() => { + return passwordCrypto.hash(this.data.password).then((hashedPassword) => { + this.data._hashed_password = hashedPassword; + delete this.data.password; + }); }); }).then(() => { From 45ee8b59a1dc603420344c5d6d4b2e96a138956b Mon Sep 17 00:00:00 2001 From: Bhaskar Reddy Yasa Date: Thu, 10 Nov 2016 22:58:57 +0530 Subject: [PATCH 18/24] fdescribe -> describe --- spec/PasswordPolicy.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index 367a11b32c..36d064e50b 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -3,7 +3,7 @@ const request = require('request'); const Config = require('../src/Config'); -fdescribe("Password Policy: ", () => { +describe("Password Policy: ", () => { 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(); From f7ce2c7557664e8f56beaa985fa5df2f8e0877c0 Mon Sep 17 00:00:00 2001 From: Bhaskar Reddy Yasa Date: Fri, 11 Nov 2016 16:03:32 +0530 Subject: [PATCH 19/24] updated PasswordPolicy.spec.js to use request-promise --- spec/PasswordPolicy.spec.js | 449 ++++++++++++++++++------------------ 1 file changed, 225 insertions(+), 224 deletions(-) diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index 36d064e50b..bbdf41370c 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -1,9 +1,9 @@ "use strict"; -const request = require('request'); +const requestp = require('request-promise'); const Config = require('../src/Config'); -describe("Password Policy: ", () => { +fdescribe("Password Policy: ", () => { 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(); @@ -28,24 +28,27 @@ describe("Password Policy: ", () => { user.setPassword("original"); user.set('email', 'user@parse.com'); return user.signUp(); - }) - .then(user => { - Parse.User.requestPasswordReset("user@parse.com"); - }) - .then(() => { - // wait for a bit more than the validity duration set - setTimeout(() => { - expect(sendEmailOptions).not.toBeUndefined(); - - request.get(sendEmailOptions.link, { - followRedirect: false, - }, (error, response, body) => { - expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); - done(); - }); - }, 1000); - }).catch((err) => { + }).then(user => { + Parse.User.requestPasswordReset("user@parse.com"); + }).then(() => { + // wait for a bit more than the validity duration set + setTimeout(() => { + expect(sendEmailOptions).not.toBeUndefined(); + + requestp.get({ + uri: sendEmailOptions.link, + 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/invalid_link.html'); + done(); + }).catch((error) => { + fail(error); + }); + }, 1000); + }).catch((err) => { jfail(err); done(); }); @@ -69,31 +72,33 @@ describe("Password Policy: ", () => { 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(user => { - Parse.User.requestPasswordReset("user@parse.com"); - }) - .then(() => { - // wait for a bit but less than the validity duration - setTimeout(() => { - expect(sendEmailOptions).not.toBeUndefined(); - - request.get(sendEmailOptions.link, { - followRedirect: false, - }, (error, response, body) => { - expect(response.statusCode).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=testResetTokenValidity/; - expect(response.body.match(re)).not.toBe(null); - done(); - }); - }, 1000); - }).catch((err) => { + }).then(() => { + user.setUsername("testResetTokenValidity"); + user.setPassword("original"); + user.set('email', 'user@parse.com'); + return user.signUp(); + }).then(user => { + Parse.User.requestPasswordReset("user@parse.com"); + }).then(() => { + // wait for a bit but less than the validity duration + setTimeout(() => { + expect(sendEmailOptions).not.toBeUndefined(); + + requestp.get({ + uri: sendEmailOptions.link, + simple: false, + resolveWithFullResponse: true, + followRedirect: false + }).then((response) => { + expect(response.statusCode).toEqual(302); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=testResetTokenValidity/; + expect(response.body.match(re)).not.toBe(null); + done(); + }).catch((error) => { + fail(error); + }); + }, 1000); + }).catch((err) => { jfail(err); done(); }); @@ -106,15 +111,13 @@ describe("Password Policy: ", () => { 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(); - }); + }).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 => { @@ -124,15 +127,13 @@ describe("Password Policy: ", () => { 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(); - }); + }).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.validator setting is invalid type', done => { @@ -142,15 +143,13 @@ describe("Password Policy: ", () => { validator: 1 // number is not a valid setting for validator }, publicServerURL: "http://localhost:8378/1" - }) - .then(() => { - fail('passwordPolicy.validator type test failed'); - done(); - }) - .catch(err => { - expect(err).toEqual('passwordPolicy.validator must be a RegExp, a string or a function.'); - done(); - }); + }).then(() => { + fail('passwordPolicy.validator type test failed'); + done(); + }).catch(err => { + expect(err).toEqual('passwordPolicy.validator must be a RegExp, a string or a function.'); + done(); + }); }); it('signup should fail if password does not confirm to the policy enforced using RegExp', (done) => { @@ -168,7 +167,7 @@ describe("Password Policy: ", () => { user.signUp().then(() => { fail('Should have failed as password does not confirm to the policy.'); done(); - }, (error) => { + }).catch((error) => { expect(error.code).toEqual(142); done(); }); @@ -189,7 +188,7 @@ describe("Password Policy: ", () => { user.set('email', 'user1@parse.com'); user.signUp().then(() => { done(); - }, (error) => { + }).catch((error) => { fail('Should have succeeded as password confirms to the policy.'); done(); }); @@ -211,7 +210,7 @@ describe("Password Policy: ", () => { user.signUp().then(() => { fail('Should have failed as password does not confirm to the policy.'); done(); - }, (error) => { + }).catch((error) => { expect(error.code).toEqual(142); done(); }); @@ -232,7 +231,7 @@ describe("Password Policy: ", () => { user.set('email', 'user1@parse.com'); user.signUp().then(() => { done(); - }, (error) => { + }).catch((error) => { fail('Should have succeeded as password confirms to the policy.'); done(); }); @@ -254,7 +253,7 @@ describe("Password Policy: ", () => { user.signUp().then(() => { fail('Should have failed as password does not confirm to the policy.'); done(); - }, (error) => { + }).catch((error) => { expect(error.code).toEqual(142); done(); }); @@ -275,7 +274,7 @@ describe("Password Policy: ", () => { user.set('email', 'user1@parse.com'); user.signUp().then(() => { done(); - }, (error) => { + }).catch((error) => { fail('Should have succeeded as password confirms to the policy.'); done(); }); @@ -287,14 +286,12 @@ describe("Password Policy: ", () => { var emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: options => { - request.get(options.link, { + requestp.get({ + uri: options.link, followRedirect: false, - }, (error, response, body) => { - if (error) { - jfail(error); - fail("Failed to get the reset link"); - return; - } + simple: false, + 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); @@ -305,31 +302,35 @@ describe("Password Policy: ", () => { } var token = match[1]; - request.post({ - url: "http://localhost:8378/1/apps/test/request_password_reset", + requestp.post({ + uri: "http://localhost:8378/1/apps/test/request_password_reset", body: `new_password=has2init&token=${token}&username=user1`, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, followRedirect: false, - }, (error, response, body) => { - if (error) { - jfail(error); - fail("Failed to POST request password reset"); - return; - } + 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'); Parse.User.logIn("user1", "has2init").then(function (user) { done(); - }, (err) => { + }).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: () => { @@ -343,25 +344,24 @@ describe("Password Policy: ", () => { validator: /[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', { - error: (err) => { - jfail(err); - fail("Reset password request should not fail"); - done(); - } - }); - }, error => { - jfail(error); - fail("signUp should not fail"); - done(); + }).then(() => { + user.setUsername("user1"); + 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(); + } }); + }).catch(error => { + jfail(error); + fail("signUp should not fail"); + done(); }); + }); }); it('should fail to reset password if the new password does not confirm to password policy', done => { @@ -369,14 +369,12 @@ describe("Password Policy: ", () => { var emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: options => { - request.get(options.link, { + requestp.get({ + uri: options.link, followRedirect: false, - }, (error, response, body) => { - if (error) { - jfail(error); - fail("Failed to get the reset link"); - return; - } + simple: false, + 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); @@ -387,31 +385,35 @@ describe("Password Policy: ", () => { } var token = match[1]; - request.post({ - url: "http://localhost:8378/1/apps/test/request_password_reset", + requestp.post({ + uri: "http://localhost:8378/1/apps/test/request_password_reset", body: `new_password=hasnodigit&token=${token}&username=user1`, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, followRedirect: false, - }, (error, response, body) => { - if (error) { - jfail(error); - fail("Failed to POST request password reset"); - return; - } + simple: false, + 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`); Parse.User.logIn("user1", "has 1 digit").then(function (user) { done(); - }, (err) => { + }).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: () => { @@ -425,25 +427,24 @@ describe("Password Policy: ", () => { validator: /[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', { - error: (err) => { - jfail(err); - fail("Reset password request should not fail"); - done(); - } - }); - }, error => { - jfail(error); - fail("signUp should not fail"); - done(); + }).then(() => { + user.setUsername("user1"); + 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(); + } }); + }).catch(error => { + jfail(error); + fail("signUp should not fail"); + done(); }); + }); }); it('should fail if passwordPolicy.doNotAllowUsername is not a boolean value', (done) => { @@ -453,15 +454,13 @@ describe("Password Policy: ", () => { 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(); - }); + }).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) => { @@ -480,7 +479,7 @@ describe("Password Policy: ", () => { user.signUp().then(() => { fail('Should have failed as password contains username.'); done(); - }, (error) => { + }).catch((error) => { expect(error.code).toEqual(142); done(); }); @@ -501,7 +500,7 @@ describe("Password Policy: ", () => { user.set('email', 'user1@parse.com'); user.signUp().then(() => { done(); - }, (error) => { + }).catch((error) => { fail('Should have succeeded as password does not contain username.'); done(); }); @@ -522,7 +521,7 @@ describe("Password Policy: ", () => { user.set('email', 'user1@parse.com'); user.signUp().then(() => { done(); - }, (error) => { + }).catch((error) => { fail('Should have succeeded as policy allows username in password.'); done(); }); @@ -534,14 +533,12 @@ describe("Password Policy: ", () => { var emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: options => { - request.get(options.link, { + requestp.get({ + uri: options.link, followRedirect: false, - }, (error, response, body) => { - if (error) { - jfail(error); - fail("Failed to get the reset link"); - return; - } + simple: false, + 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); @@ -552,31 +549,36 @@ describe("Password Policy: ", () => { } var token = match[1]; - request.post({ - url: "http://localhost:8378/1/apps/test/request_password_reset", + requestp.post({ + uri: "http://localhost:8378/1/apps/test/request_password_reset", body: `new_password=xuser12&token=${token}&username=user1`, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, followRedirect: false, - }, (error, response, body) => { - if (error) { - jfail(error); - fail("Failed to POST request password reset"); - return; - } + simple: false, + 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`); Parse.User.logIn("user1", "r@nd0m").then(function (user) { done(); - }, (err) => { + }).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: () => { @@ -590,25 +592,24 @@ describe("Password Policy: ", () => { 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', { - error: (err) => { - jfail(err); - fail("Reset password request should not fail"); - done(); - } - }); - }, error => { - jfail(error); - fail("signUp should not fail"); - done(); + }).then(() => { + user.setUsername("user1"); + 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(); + } }); + }).catch(error => { + jfail(error); + fail("signUp should not fail"); + done(); }); + }); }); it('should reset password even if the new password contains user name while the policy allows', done => { @@ -616,14 +617,12 @@ describe("Password Policy: ", () => { var emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: options => { - request.get(options.link, { + requestp.get({ + uri: options.link, followRedirect: false, - }, (error, response, body) => { - if (error) { - jfail(error); - fail("Failed to get the reset link"); - return; - } + simple: false, + 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); @@ -634,31 +633,34 @@ describe("Password Policy: ", () => { } var token = match[1]; - request.post({ - url: "http://localhost:8378/1/apps/test/request_password_reset", + 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, - }, (error, response, body) => { - if (error) { - jfail(error); - fail("Failed to POST request password reset"); - return; - } + 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'); Parse.User.logIn("user1", "uuser11").then(function (user) { done(); - }, (err) => { + }).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: () => { @@ -673,25 +675,24 @@ describe("Password Policy: ", () => { 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', { - error: (err) => { - jfail(err); - fail("Reset password request should not fail"); - done(); - } - }); - }, error => { - jfail(error); - fail("signUp should not fail"); - done(); + }).then(() => { + user.setUsername("user1"); + 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(); + } }); + }).catch(error => { + jfail(error); + fail("signUp should not fail"); + done(); }); + }); }); }) From 838eb27ed5e2cd68d1d29b99e886e2978fca1de1 Mon Sep 17 00:00:00 2001 From: Bhaskar Reddy Yasa Date: Fri, 11 Nov 2016 18:13:52 +0530 Subject: [PATCH 20/24] passwordPolicy.validator split into two separate options - RegExp and Callback --- README.md | 9 ++- spec/PasswordPolicy.spec.js | 114 +++++++++++++++++++++++------- src/Config.js | 25 +++---- src/Controllers/UserController.js | 24 +++---- src/RestWrite.js | 3 +- 5 files changed, 118 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index d66f7ce94f..43edc155a4 100644 --- a/README.md +++ b/README.md @@ -280,9 +280,12 @@ var server = ParseServer({ }, // optional settings to enforce password policies passwordPolicy: { - // optional setting to enforce strong passwords - // can be a RegExp/String representing pattern to enforce or a function that return a bool - validator: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/, // enforce password with at least 8 char with at least 1 lower case, 1 upper case and 1 digit + // Two optional settings to enforce strong passwords. Either one or both can be specified. + // If both are specified, both checks must pass to accept the password + // 1. a RegExp representing the pattern to enforce + validatorPattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/, // enforce password with at least 8 char with at least 1 lower case, 1 upper case and 1 digit + // 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 //optional setting to set a validity duration for password reset links (in seconds) resetTokenValidityDuration: 24*60*60, // expire after 24 hours diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index bbdf41370c..eb1cf20f6d 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -3,7 +3,7 @@ const requestp = require('request-promise'); const Config = require('../src/Config'); -fdescribe("Password Policy: ", () => { +describe("Password Policy: ", () => { 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(); @@ -136,28 +136,44 @@ fdescribe("Password Policy: ", () => { }); }); - it('should fail if passwordPolicy.validator setting is invalid type', done => { + it('should fail if passwordPolicy.validatorPattern setting is invalid type', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validator: 1 // number is not a valid setting for validator + validatorPattern: "abc" // string is not a valid setting }, publicServerURL: "http://localhost:8378/1" }).then(() => { - fail('passwordPolicy.validator type test failed'); + fail('passwordPolicy.validatorPattern type test failed'); done(); }).catch(err => { - expect(err).toEqual('passwordPolicy.validator must be a RegExp, a string or a function.'); + expect(err).toEqual('passwordPolicy.validatorPattern must be a RegExp.'); done(); }); }); - it('signup should fail if password does not confirm to the policy enforced using RegExp', (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 confirm to the policy enforced using validatorPattern', (done) => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validator: /[0-9]+/ // password should contain at least one digit + validatorPattern: /[0-9]+/ // password should contain at least one digit }, publicServerURL: "http://localhost:8378/1" }).then(() => { @@ -174,12 +190,12 @@ fdescribe("Password Policy: ", () => { }) }); - it('signup should succeed if password confirms to the policy enforced using RegExp', (done) => { + it('signup should succeed if password confirms to the policy enforced using validatorPattern', (done) => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validator: /[0-9]+/ // password should contain at least one digit + validatorPattern: /[0-9]+/ // password should contain at least one digit }, publicServerURL: "http://localhost:8378/1" }).then(() => { @@ -187,7 +203,14 @@ fdescribe("Password Policy: ", () => { user.setPassword("1digit"); user.set('email', 'user1@parse.com'); user.signUp().then(() => { - done(); + Parse.User.logOut(); + Parse.User.logIn("user1", "1digit").then(function (user) { + done(); + }).catch((err) => { + jfail(err); + fail("Should be able to login"); + done(); + }); }).catch((error) => { fail('Should have succeeded as password confirms to the policy.'); done(); @@ -195,17 +218,17 @@ fdescribe("Password Policy: ", () => { }) }); - it('signup should fail if password does not confirm to the policy enforced using regex string', (done) => { + it('signup should fail if password does not confirm to the policy enforced using validatorCallback', (done) => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validator: "[A-Z]+" // password should contain at least one UPPER case letter + validatorCallback: password => false // just fail }, publicServerURL: "http://localhost:8378/1" }).then(() => { user.setUsername("user1"); - user.setPassword("all lower"); + user.setPassword("any"); user.set('email', 'user1@parse.com'); user.signUp().then(() => { fail('Should have failed as password does not confirm to the policy.'); @@ -217,12 +240,12 @@ fdescribe("Password Policy: ", () => { }) }); - it('signup should succeed if password confirms to the policy enforced using regex string', (done) => { + it('signup should succeed if password confirms to the policy enforced using validatorCallback', (done) => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validator: /[A-Z]+/ // password should contain at least one digit + validatorCallback: password => true // never fail }, publicServerURL: "http://localhost:8378/1" }).then(() => { @@ -230,7 +253,14 @@ fdescribe("Password Policy: ", () => { user.setPassword("oneUpper"); user.set('email', 'user1@parse.com'); user.signUp().then(() => { - done(); + Parse.User.logOut(); + Parse.User.logIn("user1", "oneUpper").then(function (user) { + done(); + }).catch((err) => { + jfail(err); + fail("Should be able to login"); + done(); + }); }).catch((error) => { fail('Should have succeeded as password confirms to the policy.'); done(); @@ -238,17 +268,18 @@ fdescribe("Password Policy: ", () => { }) }); - it('signup should fail if password does not confirm to the policy enforced using a callback function', (done) => { + it('signup should fail if password does not confirm to validatorPattern but succeeds validatorCallback', (done) => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validator: password => false // just fail + validatorPattern: /[A-Z]+/, // password should contain at least one UPPER case letter + validatorCallback: value => true }, publicServerURL: "http://localhost:8378/1" }).then(() => { user.setUsername("user1"); - user.setPassword("any"); + user.setPassword("all lower"); user.set('email', 'user1@parse.com'); user.signUp().then(() => { fail('Should have failed as password does not confirm to the policy.'); @@ -260,12 +291,13 @@ fdescribe("Password Policy: ", () => { }) }); - it('signup should succeed if password confirms to the policy enforced using a callback function', (done) => { + it('signup should fail if password does confirms to validatorPattern but fails validatorCallback', (done) => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validator: password => true // never fail + validatorPattern: /[A-Z]+/, // password should contain at least one UPPER case letter + validatorCallback: value => false }, publicServerURL: "http://localhost:8378/1" }).then(() => { @@ -273,7 +305,37 @@ fdescribe("Password Policy: ", () => { user.setPassword("oneUpper"); user.set('email', 'user1@parse.com'); user.signUp().then(() => { + fail('Should have failed as password does not confirm to the policy.'); + done(); + }).catch((error) => { + expect(error.code).toEqual(142); done(); + }); + }) + }); + + it('signup should succeed if password confirms 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: value => 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(); + Parse.User.logIn("user1", "oneUpper").then(function (user) { + done(); + }).catch((err) => { + jfail(err); + fail("Should be able to login"); + done(); + }); }).catch((error) => { fail('Should have succeeded as password confirms to the policy.'); done(); @@ -341,7 +403,7 @@ fdescribe("Password Policy: ", () => { verifyUserEmails: false, emailAdapter: emailAdapter, passwordPolicy: { - validator: /[0-9]+/ // password should contain at least one digit + validatorPattern: /[0-9]+/ // password should contain at least one digit }, publicServerURL: "http://localhost:8378/1" }).then(() => { @@ -424,7 +486,7 @@ fdescribe("Password Policy: ", () => { verifyUserEmails: false, emailAdapter: emailAdapter, passwordPolicy: { - validator: /[0-9]+/ // password should contain at least one digit + validatorPattern: /[0-9]+/ // password should contain at least one digit }, publicServerURL: "http://localhost:8378/1" }).then(() => { @@ -468,7 +530,7 @@ fdescribe("Password Policy: ", () => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validator: /[0-9]+/, + validatorPattern: /[0-9]+/, doNotAllowUsername: true }, publicServerURL: "http://localhost:8378/1" @@ -512,7 +574,7 @@ fdescribe("Password Policy: ", () => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validator: /[0-9]+/ + validatorPattern: /[0-9]+/ }, publicServerURL: "http://localhost:8378/1" }).then(() => { @@ -671,7 +733,7 @@ fdescribe("Password Policy: ", () => { verifyUserEmails: false, emailAdapter: emailAdapter, passwordPolicy: { - validator: /[0-9]+/, + validatorPattern: /[0-9]+/, doNotAllowUsername: false }, publicServerURL: "http://localhost:8378/1" diff --git a/src/Config.js b/src/Config.js index 55345848ba..1204d92087 100644 --- a/src/Config.js +++ b/src/Config.js @@ -123,8 +123,12 @@ export class Config { throw 'passwordPolicy.resetTokenValidityDuration must be a positive number'; } - if(passwordPolicy.validator && typeof passwordPolicy.validator !== 'string' && !(passwordPolicy.validator instanceof RegExp) && typeof passwordPolicy.validator !== 'function' ) { - throw 'passwordPolicy.validator must be a RegExp, a string or a function.'; + if(passwordPolicy.validatorPattern && !(passwordPolicy.validatorPattern instanceof RegExp)) { + throw 'passwordPolicy.validatorPattern must be a RegExp.'; + } + + if(passwordPolicy.validatorCallback && typeof passwordPolicy.validatorCallback !== 'function' ) { + throw 'passwordPolicy.validatorCallback must be a function.'; } if(passwordPolicy.doNotAllowUsername && typeof passwordPolicy.doNotAllowUsername !== 'boolean') { @@ -133,20 +137,11 @@ export class Config { } } - // if the passwordPolicy.validator is configured as a string or regex then convert it to a function to process the pattern + // if the passwordPolicy.validatorPattern is configured then setup a callback to process the pattern static setupPasswordValidator(passwordPolicy) { - if (passwordPolicy && passwordPolicy.validator) { - if (typeof passwordPolicy.validator === 'string') { - const pattern = new RegExp(passwordPolicy.validator); - passwordPolicy.validator = (value) => { - return pattern.test(value) - } - } - else if (passwordPolicy.validator instanceof RegExp) { - const pattern = passwordPolicy.validator; - passwordPolicy.validator = (value) => { - return pattern.test(value) - } + if (passwordPolicy && passwordPolicy.validatorPattern) { + passwordPolicy.patternValidator = (value) => { + return passwordPolicy.validatorPattern.test(value) } } } diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 637a0ce4b3..147abc704a 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -171,18 +171,18 @@ export class UserController extends AdaptableController { updatePassword(username, token, password, config) { return this.checkResetTokenValidity(username, token) - .then(user => updateUserPassword(user.objectId, password, this.config)) - // clear reset password token - .then(() => this.config.database.update('_User', { username }, { - _perishable_token: {__op: 'Delete'}, - _perishable_token_expires_at: {__op: 'Delete'} - }),(error) => { - if (error.message) { // in case of Parse.Error, fail with the error message only - return Promise.reject(error.message); - } else { - return Promise.reject(error); - } - }); + .then(user => updateUserPassword(user.objectId, password, this.config)) + // clear reset password token + .then(() => this.config.database.update('_User', {username}, { + _perishable_token: {__op: 'Delete'}, + _perishable_token_expires_at: {__op: 'Delete'} + })).catch((error) => { + if (error.message) { // in case of Parse.Error, fail with the error message only + return Promise.reject(error.message); + } else { + return Promise.reject(error); + } + }); } defaultVerificationEmail({link, user, appName, }) { diff --git a/src/RestWrite.js b/src/RestWrite.js index 10ef60bb58..17112afc84 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -376,7 +376,8 @@ RestWrite.prototype.transformUser = function() { const policyError = 'Password does not confirm to the Password Policy.'; // check whether the password confirms to the policy - if (this.config.passwordPolicy.validator && !this.config.passwordPolicy.validator(this.data.password)) { + 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)) } From a9f55f848d450a0faa48cd0ec70b668e0de0a279 Mon Sep 17 00:00:00 2001 From: Bhaskar Reddy Yasa Date: Fri, 11 Nov 2016 21:40:19 +0530 Subject: [PATCH 21/24] fixed some typos --- spec/PasswordPolicy.spec.js | 16 ++++++++-------- src/Config.js | 2 +- src/RestWrite.js | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index eb1cf20f6d..ffa5536b57 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -168,7 +168,7 @@ describe("Password Policy: ", () => { }); }); - it('signup should fail if password does not confirm to the policy enforced using validatorPattern', (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', @@ -181,7 +181,7 @@ describe("Password Policy: ", () => { user.setPassword("nodigit"); user.set('email', 'user1@parse.com'); user.signUp().then(() => { - fail('Should have failed as password does not confirm to the policy.'); + fail('Should have failed as password does not conform to the policy.'); done(); }).catch((error) => { expect(error.code).toEqual(142); @@ -218,7 +218,7 @@ describe("Password Policy: ", () => { }) }); - it('signup should fail if password does not confirm to the policy enforced using validatorCallback', (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', @@ -231,7 +231,7 @@ describe("Password Policy: ", () => { user.setPassword("any"); user.set('email', 'user1@parse.com'); user.signUp().then(() => { - fail('Should have failed as password does not confirm to the policy.'); + fail('Should have failed as password does not conform to the policy.'); done(); }).catch((error) => { expect(error.code).toEqual(142); @@ -268,7 +268,7 @@ describe("Password Policy: ", () => { }) }); - it('signup should fail if password does not confirm to validatorPattern but succeeds validatorCallback', (done) => { + it('signup should fail if password does not match validatorPattern but succeeds validatorCallback', (done) => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', @@ -282,7 +282,7 @@ describe("Password Policy: ", () => { user.setPassword("all lower"); user.set('email', 'user1@parse.com'); user.signUp().then(() => { - fail('Should have failed as password does not confirm to the policy.'); + fail('Should have failed as password does not conform to the policy.'); done(); }).catch((error) => { expect(error.code).toEqual(142); @@ -305,7 +305,7 @@ describe("Password Policy: ", () => { user.setPassword("oneUpper"); user.set('email', 'user1@parse.com'); user.signUp().then(() => { - fail('Should have failed as password does not confirm to the policy.'); + fail('Should have failed as password does not conform to the policy.'); done(); }).catch((error) => { expect(error.code).toEqual(142); @@ -426,7 +426,7 @@ describe("Password Policy: ", () => { }); }); - it('should fail to reset password if the new password does not confirm to password policy', done => { + it('should fail to reset password if the new password does not conform to password policy', done => { var user = new Parse.User(); var emailAdapter = { sendVerificationEmail: () => Promise.resolve(), diff --git a/src/Config.js b/src/Config.js index 1204d92087..dd1bfd538c 100644 --- a/src/Config.js +++ b/src/Config.js @@ -141,7 +141,7 @@ export class Config { static setupPasswordValidator(passwordPolicy) { if (passwordPolicy && passwordPolicy.validatorPattern) { passwordPolicy.patternValidator = (value) => { - return passwordPolicy.validatorPattern.test(value) + return passwordPolicy.validatorPattern.test(value); } } } diff --git a/src/RestWrite.js b/src/RestWrite.js index 17112afc84..4859c48a4f 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -378,7 +378,7 @@ RestWrite.prototype.transformUser = function() { // check whether the password confirms 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)) + return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, policyError)); } // check whether password contain username From 74010000b5d1d8151742bffb2bc61fb17687bdca Mon Sep 17 00:00:00 2001 From: Bhaskar Reddy Yasa Date: Fri, 11 Nov 2016 21:48:31 +0530 Subject: [PATCH 22/24] expect username parameter in redirect to password_reset_success --- spec/PasswordPolicy.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index ffa5536b57..d35f03a0c3 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -375,7 +375,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/password_reset_success.html'); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1'); Parse.User.logIn("user1", "has2init").then(function (user) { done(); @@ -706,7 +706,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/password_reset_success.html'); + 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(); From 9ed141c72e8c5440af0b4039e65ec7de06514580 Mon Sep 17 00:00:00 2001 From: Bhaskar Reddy Yasa Date: Sat, 12 Nov 2016 15:49:33 +0530 Subject: [PATCH 23/24] Fix postgres issue for _perishable_token_expires_at --- src/Controllers/UserController.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 147abc704a..9aead6bc7f 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -79,8 +79,12 @@ export class UserController extends AdaptableController { } if (this.config.passwordPolicy && this.config.passwordPolicy.resetTokenValidityDuration) { - if (results[0]._perishable_token_expires_at < new Date()) - throw 'The password reset link has expired'; + let expiresDate = results[0]._perishable_token_expires_at; + if (expiresDate && expiresDate.__type == 'Date') { + expiresDate = new Date(expiresDate.iso); + if (expiresDate < new Date()) + throw 'The password reset link has expired'; + } } return results[0]; From 72a0670deac8cd4b350a7c47e499ead9a4132485 Mon Sep 17 00:00:00 2001 From: Bhaskar Reddy Yasa Date: Sat, 12 Nov 2016 18:18:20 +0530 Subject: [PATCH 24/24] fix for _perishable_token_expires_at --- src/Controllers/UserController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 9aead6bc7f..d54f036b45 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -82,9 +82,9 @@ export class UserController extends AdaptableController { let expiresDate = results[0]._perishable_token_expires_at; if (expiresDate && expiresDate.__type == 'Date') { expiresDate = new Date(expiresDate.iso); - if (expiresDate < new Date()) - throw 'The password reset link has expired'; } + if (expiresDate < new Date()) + throw 'The password reset link has expired'; } return results[0];