Added session length option for session tokens to server configuration

This commit is contained in:
Jeremy May
2016-04-02 11:36:47 -04:00
committed by Florent Vilmart
parent 51664c8f33
commit f99b5588ab
10 changed files with 188 additions and 15 deletions

View File

@@ -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)). * `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. * `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)). * `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 ##### Email verification and password reset

View File

@@ -26,6 +26,7 @@ function verifyACL(user) {
} }
describe('Parse.User testing', () => { describe('Parse.User testing', () => {
it("user sign up class method", (done) => { it("user sign up class method", (done) => {
Parse.User.signUp("asdf", "zxcv", null, { Parse.User.signUp("asdf", "zxcv", null, {
success: function(user) { 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());
});
});
});
}
});
});
}); });

View File

@@ -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();
});
});
}); });

View File

@@ -280,4 +280,37 @@ describe('server', () => {
}) ).toThrow("publicServerURL should be a valid HTTPS URL starting with https://"); }) ).toThrow("publicServerURL should be a valid HTTPS URL starting with https://");
done(); 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();
})
}); });

View File

@@ -62,6 +62,13 @@ var getAuthForSessionToken = function({ config, sessionToken, installationId } =
if (results.length !== 1 || !results[0]['user']) { if (results.length !== 1 || !results[0]['user']) {
return nobody(config); 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']; var obj = results[0]['user'];
delete obj.password; delete obj.password;
obj['className'] = '_User'; obj['className'] = '_User';

View File

@@ -47,17 +47,21 @@ export class Config {
this.customPages = cacheInfo.customPages || {}; this.customPages = cacheInfo.customPages || {};
this.mount = removeTrailingSlash(mount); this.mount = removeTrailingSlash(mount);
this.liveQueryController = cacheInfo.liveQueryController; this.liveQueryController = cacheInfo.liveQueryController;
this.sessionLength = cacheInfo.sessionLength;
this.generateSessionExpiresAt = this.generateSessionExpiresAt.bind(this);
} }
static validate(options) { static validate(options) {
this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails, this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails,
appName: options.appName, appName: options.appName,
publicServerURL: options.publicServerURL}) publicServerURL: options.publicServerURL})
if (options.publicServerURL) { if (options.publicServerURL) {
if (!options.publicServerURL.startsWith("http://") && !options.publicServerURL.startsWith("https://")) { if (!options.publicServerURL.startsWith("http://") && !options.publicServerURL.startsWith("https://")) {
throw "publicServerURL should be a valid HTTPS URL starting with https://" throw "publicServerURL should be a valid HTTPS URL starting with https://"
} }
} }
this.validateSessionLength(options.sessionLength);
} }
static validateEmailConfiguration({verifyUserEmails, appName, publicServerURL}) { static validateEmailConfiguration({verifyUserEmails, appName, publicServerURL}) {
@@ -83,6 +87,20 @@ export class Config {
this._mount = newValue; 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() { get invalidLinkURL() {
return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`; return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`;
} }

View File

@@ -75,6 +75,7 @@ addParseCloud();
// "restAPIKey": optional key from Parse dashboard // "restAPIKey": optional key from Parse dashboard
// "javascriptKey": optional key from Parse dashboard // "javascriptKey": optional key from Parse dashboard
// "push": optional key from configure push // "push": optional key from configure push
// "sessionLength": optional length in seconds for how long Sessions should be valid for
class ParseServer { class ParseServer {
@@ -111,7 +112,8 @@ class ParseServer {
choosePassword: undefined, choosePassword: undefined,
passwordResetSuccess: undefined passwordResetSuccess: undefined
}, },
liveQuery = {} liveQuery = {},
sessionLength = 31536000, // 1 Year in seconds
}) { }) {
// Initialize the node client SDK automatically // Initialize the node client SDK automatically
Parse.initialize(appId, javascriptKey || 'unused', masterKey); Parse.initialize(appId, javascriptKey || 'unused', masterKey);
@@ -185,7 +187,8 @@ class ParseServer {
publicServerURL: publicServerURL, publicServerURL: publicServerURL,
customPages: customPages, customPages: customPages,
maxUploadSize: maxUploadSize, maxUploadSize: maxUploadSize,
liveQueryController: liveQueryController liveQueryController: liveQueryController,
sessionLength : Number(sessionLength),
}); });
// To maintain compatibility. TODO: Remove in some version that breaks backwards compatability // To maintain compatibility. TODO: Remove in some version that breaks backwards compatability

View File

@@ -319,8 +319,7 @@ RestWrite.prototype.transformUser = function() {
var token = 'r:' + cryptoUtils.newToken(); var token = 'r:' + cryptoUtils.newToken();
this.storage['token'] = token; this.storage['token'] = token;
promise = promise.then(() => { promise = promise.then(() => {
var expiresAt = new Date(); var expiresAt = this.config.generateSessionExpiresAt();
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
var sessionData = { var sessionData = {
sessionToken: token, sessionToken: token,
user: { user: {
@@ -474,8 +473,7 @@ RestWrite.prototype.handleSession = function() {
if (!this.query && !this.auth.isMaster) { if (!this.query && !this.auth.isMaster) {
var token = 'r:' + cryptoUtils.newToken(); var token = 'r:' + cryptoUtils.newToken();
var expiresAt = new Date(); var expiresAt = this.config.generateSessionExpiresAt();
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
var sessionData = { var sessionData = {
sessionToken: token, sessionToken: token,
user: { user: {
@@ -739,6 +737,7 @@ RestWrite.prototype.runDatabaseOperation = function() {
ACL['*'] = { read: true, write: false }; ACL['*'] = { read: true, write: false };
this.data.ACL = ACL; this.data.ACL = ACL;
} }
// Run a create // Run a create
return this.config.database.create(this.className, this.data, this.runOptions) return this.config.database.create(this.className, this.data, this.runOptions)
.then((resp) => { .then((resp) => {

View File

@@ -108,9 +108,7 @@ export class UsersRouter extends ClassesRouter {
req.config.filesController.expandFilesInObject(req.config, user); req.config.filesController.expandFilesInObject(req.config, user);
let expiresAt = new Date(); let expiresAt = req.config.generateSessionExpiresAt();
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
let sessionData = { let sessionData = {
sessionToken: token, sessionToken: token,
user: { user: {

View File

@@ -128,9 +128,15 @@ function handleParseHeaders(req, res, next) {
} }
}) })
.catch((error) => { .catch((error) => {
// TODO: Determine the correct error scenario. if(error instanceof Parse.Error) {
log.error('error getting auth for sessionToken', error); next(error);
throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, 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);
}
}); });
} }