Skip to content

Commit

Permalink
Added session length option for session tokens to server configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
Kenishi authored and flovilmart committed Apr 2, 2016
1 parent 51664c8 commit f99b558
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 15 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
41 changes: 41 additions & 0 deletions spec/ParseUser.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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());
});
});
});
}
});
});

});
68 changes: 68 additions & 0 deletions spec/RestCreate.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
33 changes: 33 additions & 0 deletions spec/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
})
});
7 changes: 7 additions & 0 deletions src/Auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
22 changes: 20 additions & 2 deletions src/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}) {
Expand All @@ -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`;
}
Expand Down
7 changes: 5 additions & 2 deletions src/ParseServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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) => {
Expand Down
4 changes: 1 addition & 3 deletions src/Routers/UsersRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
12 changes: 9 additions & 3 deletions src/middlewares.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
}

Expand Down

0 comments on commit f99b558

Please sign in to comment.