Added session length option for session tokens to server configuration
This commit is contained in:
committed by
Florent Vilmart
parent
51664c8f33
commit
f99b5588ab
@@ -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
|
||||
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
})
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user