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)).
|
* `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
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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://");
|
}) ).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();
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user