From a996bd7ef49c50c2510bdbe5e090cc426788cb8a Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Fri, 22 Apr 2016 11:30:20 -0700 Subject: [PATCH 1/2] Add revokeSessionOnPasswordReset option --- README.md | 3 +- spec/ParseUser.spec.js | 70 +++++++++++++++++++++++++++++++++++--- spec/helper.js | 4 +-- spec/index.spec.js | 5 +++ src/Config.js | 5 +-- src/ParseServer.js | 9 +++-- src/RestWrite.js | 3 +- src/cli/cli-definitions.js | 5 +++ src/rest.js | 4 +-- 9 files changed, 91 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f741ecbf07..7dbab03740 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ For the full list of available options, run `parse-server --help`. * `appId` **(required)** - The application id to host with this server instance. You can use any arbitrary string. For migrated apps, this should match your hosted Parse app. * `masterKey` **(required)** - The master key to use for overriding ACL security. You can use any arbitrary string. Keep it secret! For migrated apps, this should match your hosted Parse app. -* `databaseURI` **(required)** - The connection string for your database, i.e. `mongodb://user:pass@host.com/dbname`. Be sure to [URL encode your password](https://app.zencoder.com/docs/guides/getting-started/special-characters-in-usernames-and-passwords) if your password has special charachters. +* `databaseURI` **(required)** - The connection string for your database, i.e. `mongodb://user:pass@host.com/dbname`. Be sure to [URL encode your password](https://app.zencoder.com/docs/guides/getting-started/special-characters-in-usernames-and-passwords) if your password has special charachters. * `port` - The default port is 1337, specify this parameter to use a different port. * `serverURL` - URL to your Parse Server (don't forget to specify http:// or https://). This URL will be used when making requests to Parse Server from Cloud Code. * `cloud` - The absolute path to your cloud code `main.js` file. @@ -188,6 +188,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo * `maxUploadSize` - Max file size for uploads. Defaults to 20 MB. * `loggerAdapter` - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js)). * `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. ##### Email verification and password reset diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index e93c6a015a..0873f4426f 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -2115,9 +2115,7 @@ describe('Parse.User testing', () => { }); }); - // Sometimes the authData still has null on that keys - // https://github.com/ParsePlatform/parse-server/issues/935 - it('should cleanup null authData keys', (done) => { + it('should cleanup null authData keys (regression test for #935)', (done) => { let database = new Config(Parse.applicationId).database; database.create('_User', { username: 'user', @@ -2151,8 +2149,7 @@ describe('Parse.User testing', () => { }) }); - // https://github.com/ParsePlatform/parse-server/issues/1198 - it('should cleanup null authData keys ParseUser update', (done) => { + it('should cleanup null authData keys ParseUser update (regression test for #1198)', (done) => { Parse.Cloud.beforeSave('_User', (req, res) => { req.object.set('foo', 'bar'); res.success(); @@ -2347,4 +2344,67 @@ describe('Parse.User testing', () => { done(); }); }); + + it('should revoke sessions when converting anonymous user to "normal" user', done => { + request.post({ + url: 'http://localhost:8378/1/classes/_User', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + json: {authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}} + }, (err, res, body) => { + Parse.User.become(body.sessionToken) + .then(user => { + let obj = new Parse.Object('TestObject'); + obj.setACL(new Parse.ACL(user)); + return obj.save() + .then(() => { + // Change password, revoking session + user.set('username', 'no longer anonymous'); + user.set('password', 'password'); + return user.save() + }) + .then(() => obj.fetch()) + .catch(error => { + expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + done(); + }); + }) + }); + }); + + it('should not revoke session tokens if the server is configures to not revoke session tokens', done => { + setServerConfiguration({ + serverURL: 'http://localhost:8378/1', + appId: 'test', + masterKey: 'test', + cloud: './spec/cloud/main.js', + revokeSessionOnPasswordReset: false, + }) + request.post({ + url: 'http://localhost:8378/1/classes/_User', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + json: {authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}} + }, (err, res, body) => { + Parse.User.become(body.sessionToken) + .then(user => { + let obj = new Parse.Object('TestObject'); + obj.setACL(new Parse.ACL(user)); + return obj.save() + .then(() => { + // Change password, revoking session + user.set('username', 'no longer anonymous'); + user.set('password', 'password'); + return user.save() + }) + .then(() => obj.fetch()) + // fetch should succeed as we still have our session token + .then(done, fail); + }) + }); + }) }); diff --git a/spec/helper.js b/spec/helper.js index 03ddff9743..ef893f1697 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -40,7 +40,7 @@ var defaultConfiguration = { myoauth: { module: path.resolve(__dirname, "myoauth") // relative path as it's run from src } - } + }, }; // Set up a default API server for testing with default configuration. @@ -54,7 +54,7 @@ delete defaultConfiguration.cloud; var currentConfiguration; // Allows testing specific configurations of Parse Server -var setServerConfiguration = configuration => { +const setServerConfiguration = configuration => { // the configuration hasn't changed if (configuration === currentConfiguration) { return; diff --git a/spec/index.spec.js b/spec/index.spec.js index c219e3ad6f..d2c8984d71 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -333,4 +333,9 @@ describe('server', () => { })).toThrow('Session length must be a value greater than 0.'); done(); }) + + it('fails if you try to set revokeSessionOnPasswordReset to non-boolean', done => { + expect(() => setServerConfiguration({ revokeSessionOnPasswordReset: 'non-bool' })).toThrow(); + done(); + }); }); diff --git a/src/Config.js b/src/Config.js index 3e2ac36834..ac36bb4893 100644 --- a/src/Config.js +++ b/src/Config.js @@ -49,11 +49,12 @@ export class Config { this.liveQueryController = cacheInfo.liveQueryController; this.sessionLength = cacheInfo.sessionLength; this.generateSessionExpiresAt = this.generateSessionExpiresAt.bind(this); + this.revokeSessionOnPasswordReset = cacheInfo.revokeSessionOnPasswordReset; } static validate(options) { - this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails, - appName: options.appName, + this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails, + appName: options.appName, publicServerURL: options.publicServerURL}) if (options.publicServerURL) { if (!options.publicServerURL.startsWith("http://") && !options.publicServerURL.startsWith("https://")) { diff --git a/src/ParseServer.js b/src/ParseServer.js index 6db13cffbe..cd46fd8e6a 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -9,7 +9,7 @@ var batch = require('./batch'), Parse = require('parse/node').Parse, path = require('path'), authDataManager = require('./authDataManager'); - + if (!global._babelPolyfill) { require('babel-polyfill'); } @@ -115,7 +115,11 @@ class ParseServer { liveQuery = {}, sessionLength = 31536000, // 1 Year in seconds verbose = false, + revokeSessionOnPasswordReset = true, }) { + if (typeof revokeSessionOnPasswordReset !== 'boolean') { + throw 'revokeSessionOnPasswordReset must be a boolean value'; + } // Initialize the node client SDK automatically Parse.initialize(appId, javascriptKey || 'unused', masterKey); Parse.serverURL = serverURL; @@ -186,7 +190,8 @@ class ParseServer { customPages: customPages, maxUploadSize: maxUploadSize, liveQueryController: liveQueryController, - sessionLength : Number(sessionLength), + sessionLength: Number(sessionLength), + revokeSessionOnPasswordReset }); // To maintain compatibility. TODO: Remove in some version that breaks backwards compatability diff --git a/src/RestWrite.js b/src/RestWrite.js index 2399751bd6..03138e44f4 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -420,8 +420,7 @@ RestWrite.prototype.createSessionTokenIfNeeded = function() { // Handles any followup logic RestWrite.prototype.handleFollowup = function() { - - if (this.storage && this.storage['clearSessions']) { + if (this.storage && this.storage['clearSessions'] && this.config.revokeSessionOnPasswordReset) { var sessionQuery = { user: { __type: 'Pointer', diff --git a/src/cli/cli-definitions.js b/src/cli/cli-definitions.js index dd908bcd04..c1f6961c1a 100644 --- a/src/cli/cli-definitions.js +++ b/src/cli/cli-definitions.js @@ -174,5 +174,10 @@ export default { "verbose": { env: "VERBOSE", help: "Set the logging to verbose" + }, + "revokeSessionOnPasswordReset": { + env: "PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET", + help: "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.", + action: booleanParser } }; diff --git a/src/rest.js b/src/rest.js index f372cdfb6b..4a79b9da93 100644 --- a/src/rest.js +++ b/src/rest.js @@ -9,7 +9,7 @@ var Parse = require('parse/node').Parse; import cache from './cache'; -import Auth from './Auth'; +import Auth from './Auth'; var RestQuery = require('./RestQuery'); var RestWrite = require('./RestWrite'); @@ -95,8 +95,6 @@ function create(config, auth, className, restObject) { // REST API is supposed to return. // Usually, this is just updatedAt. function update(config, auth, className, objectId, restObject) { - enforceRoleSecurity('update', className, auth); - return Promise.resolve().then(() => { if (triggers.getTrigger(className, triggers.Types.beforeSave, config.applicationId) || triggers.getTrigger(className, triggers.Types.afterSave, config.applicationId) || From 7274fdcba507d895e7b696c229b6106d4418326a Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Fri, 22 Apr 2016 13:55:41 -0700 Subject: [PATCH 2/2] Fix nits --- src/Config.js | 13 ++++++++++--- src/ParseServer.js | 3 --- src/rest.js | 1 + 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Config.js b/src/Config.js index ac36bb4893..5c3ca4e08e 100644 --- a/src/Config.js +++ b/src/Config.js @@ -53,9 +53,16 @@ export class Config { } static validate(options) { - this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails, - appName: options.appName, - publicServerURL: options.publicServerURL}) + this.validateEmailConfiguration({ + verifyUserEmails: options.verifyUserEmails, + appName: options.appName, + publicServerURL: options.publicServerURL + }) + + if (typeof options.revokeSessionOnPasswordReset !== 'boolean') { + throw 'revokeSessionOnPasswordReset must be a boolean value'; + } + if (options.publicServerURL) { if (!options.publicServerURL.startsWith("http://") && !options.publicServerURL.startsWith("https://")) { throw "publicServerURL should be a valid HTTPS URL starting with https://" diff --git a/src/ParseServer.js b/src/ParseServer.js index cd46fd8e6a..ad8efd19da 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -117,9 +117,6 @@ class ParseServer { verbose = false, revokeSessionOnPasswordReset = true, }) { - if (typeof revokeSessionOnPasswordReset !== 'boolean') { - throw 'revokeSessionOnPasswordReset must be a boolean value'; - } // Initialize the node client SDK automatically Parse.initialize(appId, javascriptKey || 'unused', masterKey); Parse.serverURL = serverURL; diff --git a/src/rest.js b/src/rest.js index 4a79b9da93..60f017213e 100644 --- a/src/rest.js +++ b/src/rest.js @@ -95,6 +95,7 @@ function create(config, auth, className, restObject) { // REST API is supposed to return. // Usually, this is just updatedAt. function update(config, auth, className, objectId, restObject) { + enforceRoleSecurity('update', className, auth); return Promise.resolve().then(() => { if (triggers.getTrigger(className, triggers.Types.beforeSave, config.applicationId) || triggers.getTrigger(className, triggers.Types.afterSave, config.applicationId) ||