Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added session length option for session tokens to server configuration #997

Merged
merged 1 commit into from
Apr 2, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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