From 95d82345ccdd3580939cf393c061baea477c27d6 Mon Sep 17 00:00:00 2001 From: Jeremy May Date: Sat, 12 Mar 2016 04:33:11 -0500 Subject: [PATCH] Add sessionLength option to server configuration. Added tests for verification. --- README.md | 2 +- spec/ParseUser.spec.js | 41 +++++++++++++++++++++++ spec/RestCreate.spec.js | 68 ++++++++++++++++++++++++++++++++++++++ spec/index.spec.js | 33 ++++++++++++++++++ src/Auth.js | 7 ++++ src/Config.js | 22 ++++++++++-- src/ParseServer.js | 7 ++-- src/RestWrite.js | 7 ++-- src/Routers/UsersRouter.js | 4 +-- src/middlewares.js | 12 +++++-- 10 files changed, 188 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 55c16c7aa4..e11b3019d3 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo * `filesAdapter` - The default behavior (GridStore) can be changed by creating an adapter class (see [`FilesAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Files/FilesAdapter.js)). * `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)). -* `databaseAdapter` - The backing store can be changed by creating an adapter class (see `DatabaseAdapter.js`). Defaults to `MongoStorageAdapter`. +* `sessionLength` - The length of time in seconds that a session should be valid for. Defaults to 31536000 seconds (1 year). ##### Email verification and password reset diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 941e1a0c55..aacb24099c 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -26,6 +26,7 @@ function verifyACL(user) { } describe('Parse.User testing', () => { + it("user sign up class method", (done) => { Parse.User.signUp("asdf", "zxcv", null, { success: function(user) { @@ -2160,4 +2161,44 @@ describe('Parse.User testing', () => { }); }); + + it('should fail to become user with expired token', (done) => { + Parse.User.signUp("auser", "somepass", null, { + success: function(user) { + request.get({ + url: 'http://localhost:8378/1/classes/_Session', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }, (error, response, body) => { + var id = body.results[0].objectId; + var expiresAt = new Date((new Date()).setYear(2015)); + var token = body.results[0].sessionToken; + request.put({ + url: "http://localhost:8378/1/classes/_Session/" + id, + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + body: { + expiresAt: { __type: "Date", iso: expiresAt.toISOString() }, + }, + }, (error, response, body) => { + Parse.User.become(token) + .then(() => { fail("Should not have succeded"); }) + .fail((err) => { + expect(err.code).toEqual(209); + expect(err.message).toEqual("Session token is expired."); + Parse.User.logOut() // Logout to prevent polluting CLI with messages + .then(done()); + }); + }); + }); + } + }); + }); + }); diff --git a/spec/RestCreate.spec.js b/spec/RestCreate.spec.js index 7bf9d22b8d..ffc19e4f26 100644 --- a/spec/RestCreate.spec.js +++ b/spec/RestCreate.spec.js @@ -284,4 +284,72 @@ describe('rest create', () => { }); }); + it("test default session length", (done) => { + var user = { + username: 'asdf', + password: 'zxcv', + foo: 'bar', + }; + var now = new Date(); + + rest.create(config, auth.nobody(config), '_User', user) + .then((r) => { + expect(Object.keys(r.response).length).toEqual(3); + expect(typeof r.response.objectId).toEqual('string'); + expect(typeof r.response.createdAt).toEqual('string'); + expect(typeof r.response.sessionToken).toEqual('string'); + return rest.find(config, auth.master(config), + '_Session', {sessionToken: r.response.sessionToken}); + }) + .then((r) => { + expect(r.results.length).toEqual(1); + + var session = r.results[0]; + var actual = new Date(session.expiresAt.iso); + var expected = new Date(now.getTime() + (1000 * 3600 * 24 * 365)); + + expect(actual.getFullYear()).toEqual(expected.getFullYear()); + expect(actual.getMonth()).toEqual(expected.getMonth()); + expect(actual.getDate()).toEqual(expected.getDate()); + expect(actual.getMinutes()).toEqual(expected.getMinutes()); + + done(); + }); + }); + + it("test specified session length", (done) => { + var user = { + username: 'asdf', + password: 'zxcv', + foo: 'bar', + }; + var sessionLength = 3600, // 1 Hour ahead + now = new Date(); // For reference later + config.sessionLength = sessionLength; + + rest.create(config, auth.nobody(config), '_User', user) + .then((r) => { + expect(Object.keys(r.response).length).toEqual(3); + expect(typeof r.response.objectId).toEqual('string'); + expect(typeof r.response.createdAt).toEqual('string'); + expect(typeof r.response.sessionToken).toEqual('string'); + return rest.find(config, auth.master(config), + '_Session', {sessionToken: r.response.sessionToken}); + }) + .then((r) => { + expect(r.results.length).toEqual(1); + + var session = r.results[0]; + var actual = new Date(session.expiresAt.iso); + var expected = new Date(now.getTime() + (sessionLength*1000)); + + expect(actual.getFullYear()).toEqual(expected.getFullYear()); + expect(actual.getMonth()).toEqual(expected.getMonth()); + expect(actual.getDate()).toEqual(expected.getDate()); + expect(actual.getHours()).toEqual(expected.getHours()); + expect(actual.getMinutes()).toEqual(expected.getMinutes()); + + done(); + }); + }); }); diff --git a/spec/index.spec.js b/spec/index.spec.js index b4ca4a8cf0..b58cb2f2a6 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -280,4 +280,37 @@ describe('server', () => { }) ).toThrow("publicServerURL should be a valid HTTPS URL starting with https://"); done(); }); + + it('fails if the session length is not a number', (done) => { + expect(() => setServerConfiguration({ + serverURL: 'http://localhost:8378/1', + appId: 'test', + appName: 'unused', + javascriptKey: 'test', + masterKey: 'test', + sessionLength: 'test' + })).toThrow('Session length must be a valid number.'); + done(); + }); + + it('fails if the session length is less than or equal to 0', (done) => { + expect(() => setServerConfiguration({ + serverURL: 'http://localhost:8378/1', + appId: 'test', + appName: 'unused', + javascriptKey: 'test', + masterKey: 'test', + sessionLength: '-33' + })).toThrow('Session length must be a value greater than 0.'); + + expect(() => setServerConfiguration({ + serverURL: 'http://localhost:8378/1', + appId: 'test', + appName: 'unused', + javascriptKey: 'test', + masterKey: 'test', + sessionLength: '0' + })).toThrow('Session length must be a value greater than 0.'); + done(); + }) }); diff --git a/src/Auth.js b/src/Auth.js index b45f93f3f7..cd5da9ff40 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -62,6 +62,13 @@ var getAuthForSessionToken = function({ config, sessionToken, installationId } = if (results.length !== 1 || !results[0]['user']) { return nobody(config); } + + var now = new Date(), + expiresAt = new Date(results[0].expiresAt.iso); + if(expiresAt < now) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, + 'Session token is expired.'); + } var obj = results[0]['user']; delete obj.password; obj['className'] = '_User'; diff --git a/src/Config.js b/src/Config.js index b9f0d007c5..3e2ac36834 100644 --- a/src/Config.js +++ b/src/Config.js @@ -47,17 +47,21 @@ export class Config { this.customPages = cacheInfo.customPages || {}; this.mount = removeTrailingSlash(mount); this.liveQueryController = cacheInfo.liveQueryController; + this.sessionLength = cacheInfo.sessionLength; + this.generateSessionExpiresAt = this.generateSessionExpiresAt.bind(this); } 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://")) { throw "publicServerURL should be a valid HTTPS URL starting with https://" } } + + this.validateSessionLength(options.sessionLength); } static validateEmailConfiguration({verifyUserEmails, appName, publicServerURL}) { @@ -83,6 +87,20 @@ export class Config { this._mount = newValue; } + static validateSessionLength(sessionLength) { + if(isNaN(sessionLength)) { + throw 'Session length must be a valid number.'; + } + else if(sessionLength <= 0) { + throw 'Session length must be a value greater than 0.' + } + } + + generateSessionExpiresAt() { + var now = new Date(); + return new Date(now.getTime() + (this.sessionLength*1000)); + } + get invalidLinkURL() { return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`; } diff --git a/src/ParseServer.js b/src/ParseServer.js index 85f8a98b7f..6a35bd2a81 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -75,6 +75,7 @@ addParseCloud(); // "restAPIKey": optional key from Parse dashboard // "javascriptKey": optional key from Parse dashboard // "push": optional key from configure push +// "sessionLength": optional length in seconds for how long Sessions should be valid for class ParseServer { @@ -111,7 +112,8 @@ class ParseServer { choosePassword: undefined, passwordResetSuccess: undefined }, - liveQuery = {} + liveQuery = {}, + sessionLength = 31536000, // 1 Year in seconds }) { // Initialize the node client SDK automatically Parse.initialize(appId, javascriptKey || 'unused', masterKey); @@ -185,7 +187,8 @@ class ParseServer { publicServerURL: publicServerURL, customPages: customPages, maxUploadSize: maxUploadSize, - liveQueryController: liveQueryController + liveQueryController: liveQueryController, + sessionLength : Number(sessionLength), }); // To maintain compatibility. TODO: Remove in some version that breaks backwards compatability diff --git a/src/RestWrite.js b/src/RestWrite.js index ba1bfdca1c..e18c660d96 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -319,8 +319,7 @@ RestWrite.prototype.transformUser = function() { var token = 'r:' + cryptoUtils.newToken(); this.storage['token'] = token; promise = promise.then(() => { - var expiresAt = new Date(); - expiresAt.setFullYear(expiresAt.getFullYear() + 1); + var expiresAt = this.config.generateSessionExpiresAt(); var sessionData = { sessionToken: token, user: { @@ -474,8 +473,7 @@ RestWrite.prototype.handleSession = function() { if (!this.query && !this.auth.isMaster) { var token = 'r:' + cryptoUtils.newToken(); - var expiresAt = new Date(); - expiresAt.setFullYear(expiresAt.getFullYear() + 1); + var expiresAt = this.config.generateSessionExpiresAt(); var sessionData = { sessionToken: token, user: { @@ -739,6 +737,7 @@ RestWrite.prototype.runDatabaseOperation = function() { ACL['*'] = { read: true, write: false }; this.data.ACL = ACL; } + // Run a create return this.config.database.create(this.className, this.data, this.runOptions) .then((resp) => { diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index d9fe439652..4d4cb5034c 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -108,9 +108,7 @@ export class UsersRouter extends ClassesRouter { req.config.filesController.expandFilesInObject(req.config, user); - let expiresAt = new Date(); - expiresAt.setFullYear(expiresAt.getFullYear() + 1); - + let expiresAt = req.config.generateSessionExpiresAt(); let sessionData = { sessionToken: token, user: { diff --git a/src/middlewares.js b/src/middlewares.js index 60a1a666b2..cb625f3f4c 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -128,9 +128,15 @@ function handleParseHeaders(req, res, next) { } }) .catch((error) => { - // TODO: Determine the correct error scenario. - log.error('error getting auth for sessionToken', error); - throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error); + if(error instanceof Parse.Error) { + next(error); + return; + } + else { + // TODO: Determine the correct error scenario. + log.error('error getting auth for sessionToken', error); + throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error); + } }); }