Skip to content

Commit f99b558

Browse files
Kenishiflovilmart
authored andcommitted
Added session length option for session tokens to server configuration
1 parent 51664c8 commit f99b558

10 files changed

+188
-15
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
185185
* `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)).
186186
* `maxUploadSize` - Max file size for uploads. Defaults to 20 MB.
187187
* `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)).
188-
* `databaseAdapter` - The backing store can be changed by creating an adapter class (see `DatabaseAdapter.js`). Defaults to `MongoStorageAdapter`.
188+
* `sessionLength` - The length of time in seconds that a session should be valid for. Defaults to 31536000 seconds (1 year).
189189

190190
##### Email verification and password reset
191191

spec/ParseUser.spec.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ function verifyACL(user) {
2626
}
2727

2828
describe('Parse.User testing', () => {
29+
2930
it("user sign up class method", (done) => {
3031
Parse.User.signUp("asdf", "zxcv", null, {
3132
success: function(user) {
@@ -2160,4 +2161,44 @@ describe('Parse.User testing', () => {
21602161
});
21612162

21622163
});
2164+
2165+
it('should fail to become user with expired token', (done) => {
2166+
Parse.User.signUp("auser", "somepass", null, {
2167+
success: function(user) {
2168+
request.get({
2169+
url: 'http://localhost:8378/1/classes/_Session',
2170+
json: true,
2171+
headers: {
2172+
'X-Parse-Application-Id': 'test',
2173+
'X-Parse-Master-Key': 'test',
2174+
},
2175+
}, (error, response, body) => {
2176+
var id = body.results[0].objectId;
2177+
var expiresAt = new Date((new Date()).setYear(2015));
2178+
var token = body.results[0].sessionToken;
2179+
request.put({
2180+
url: "http://localhost:8378/1/classes/_Session/" + id,
2181+
json: true,
2182+
headers: {
2183+
'X-Parse-Application-Id': 'test',
2184+
'X-Parse-Master-Key': 'test',
2185+
},
2186+
body: {
2187+
expiresAt: { __type: "Date", iso: expiresAt.toISOString() },
2188+
},
2189+
}, (error, response, body) => {
2190+
Parse.User.become(token)
2191+
.then(() => { fail("Should not have succeded"); })
2192+
.fail((err) => {
2193+
expect(err.code).toEqual(209);
2194+
expect(err.message).toEqual("Session token is expired.");
2195+
Parse.User.logOut() // Logout to prevent polluting CLI with messages
2196+
.then(done());
2197+
});
2198+
});
2199+
});
2200+
}
2201+
});
2202+
});
2203+
21632204
});

spec/RestCreate.spec.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,4 +284,72 @@ describe('rest create', () => {
284284
});
285285
});
286286

287+
it("test default session length", (done) => {
288+
var user = {
289+
username: 'asdf',
290+
password: 'zxcv',
291+
foo: 'bar',
292+
};
293+
var now = new Date();
294+
295+
rest.create(config, auth.nobody(config), '_User', user)
296+
.then((r) => {
297+
expect(Object.keys(r.response).length).toEqual(3);
298+
expect(typeof r.response.objectId).toEqual('string');
299+
expect(typeof r.response.createdAt).toEqual('string');
300+
expect(typeof r.response.sessionToken).toEqual('string');
301+
return rest.find(config, auth.master(config),
302+
'_Session', {sessionToken: r.response.sessionToken});
303+
})
304+
.then((r) => {
305+
expect(r.results.length).toEqual(1);
306+
307+
var session = r.results[0];
308+
var actual = new Date(session.expiresAt.iso);
309+
var expected = new Date(now.getTime() + (1000 * 3600 * 24 * 365));
310+
311+
expect(actual.getFullYear()).toEqual(expected.getFullYear());
312+
expect(actual.getMonth()).toEqual(expected.getMonth());
313+
expect(actual.getDate()).toEqual(expected.getDate());
314+
expect(actual.getMinutes()).toEqual(expected.getMinutes());
315+
316+
done();
317+
});
318+
});
319+
320+
it("test specified session length", (done) => {
321+
var user = {
322+
username: 'asdf',
323+
password: 'zxcv',
324+
foo: 'bar',
325+
};
326+
var sessionLength = 3600, // 1 Hour ahead
327+
now = new Date(); // For reference later
328+
config.sessionLength = sessionLength;
329+
330+
rest.create(config, auth.nobody(config), '_User', user)
331+
.then((r) => {
332+
expect(Object.keys(r.response).length).toEqual(3);
333+
expect(typeof r.response.objectId).toEqual('string');
334+
expect(typeof r.response.createdAt).toEqual('string');
335+
expect(typeof r.response.sessionToken).toEqual('string');
336+
return rest.find(config, auth.master(config),
337+
'_Session', {sessionToken: r.response.sessionToken});
338+
})
339+
.then((r) => {
340+
expect(r.results.length).toEqual(1);
341+
342+
var session = r.results[0];
343+
var actual = new Date(session.expiresAt.iso);
344+
var expected = new Date(now.getTime() + (sessionLength*1000));
345+
346+
expect(actual.getFullYear()).toEqual(expected.getFullYear());
347+
expect(actual.getMonth()).toEqual(expected.getMonth());
348+
expect(actual.getDate()).toEqual(expected.getDate());
349+
expect(actual.getHours()).toEqual(expected.getHours());
350+
expect(actual.getMinutes()).toEqual(expected.getMinutes());
351+
352+
done();
353+
});
354+
});
287355
});

spec/index.spec.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,4 +280,37 @@ describe('server', () => {
280280
}) ).toThrow("publicServerURL should be a valid HTTPS URL starting with https://");
281281
done();
282282
});
283+
284+
it('fails if the session length is not a number', (done) => {
285+
expect(() => setServerConfiguration({
286+
serverURL: 'http://localhost:8378/1',
287+
appId: 'test',
288+
appName: 'unused',
289+
javascriptKey: 'test',
290+
masterKey: 'test',
291+
sessionLength: 'test'
292+
})).toThrow('Session length must be a valid number.');
293+
done();
294+
});
295+
296+
it('fails if the session length is less than or equal to 0', (done) => {
297+
expect(() => setServerConfiguration({
298+
serverURL: 'http://localhost:8378/1',
299+
appId: 'test',
300+
appName: 'unused',
301+
javascriptKey: 'test',
302+
masterKey: 'test',
303+
sessionLength: '-33'
304+
})).toThrow('Session length must be a value greater than 0.');
305+
306+
expect(() => setServerConfiguration({
307+
serverURL: 'http://localhost:8378/1',
308+
appId: 'test',
309+
appName: 'unused',
310+
javascriptKey: 'test',
311+
masterKey: 'test',
312+
sessionLength: '0'
313+
})).toThrow('Session length must be a value greater than 0.');
314+
done();
315+
})
283316
});

src/Auth.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ var getAuthForSessionToken = function({ config, sessionToken, installationId } =
6262
if (results.length !== 1 || !results[0]['user']) {
6363
return nobody(config);
6464
}
65+
66+
var now = new Date(),
67+
expiresAt = new Date(results[0].expiresAt.iso);
68+
if(expiresAt < now) {
69+
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
70+
'Session token is expired.');
71+
}
6572
var obj = results[0]['user'];
6673
delete obj.password;
6774
obj['className'] = '_User';

src/Config.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,21 @@ export class Config {
4747
this.customPages = cacheInfo.customPages || {};
4848
this.mount = removeTrailingSlash(mount);
4949
this.liveQueryController = cacheInfo.liveQueryController;
50+
this.sessionLength = cacheInfo.sessionLength;
51+
this.generateSessionExpiresAt = this.generateSessionExpiresAt.bind(this);
5052
}
5153

5254
static validate(options) {
53-
this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails,
54-
appName: options.appName,
55+
this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails,
56+
appName: options.appName,
5557
publicServerURL: options.publicServerURL})
5658
if (options.publicServerURL) {
5759
if (!options.publicServerURL.startsWith("http://") && !options.publicServerURL.startsWith("https://")) {
5860
throw "publicServerURL should be a valid HTTPS URL starting with https://"
5961
}
6062
}
63+
64+
this.validateSessionLength(options.sessionLength);
6165
}
6266

6367
static validateEmailConfiguration({verifyUserEmails, appName, publicServerURL}) {
@@ -83,6 +87,20 @@ export class Config {
8387
this._mount = newValue;
8488
}
8589

90+
static validateSessionLength(sessionLength) {
91+
if(isNaN(sessionLength)) {
92+
throw 'Session length must be a valid number.';
93+
}
94+
else if(sessionLength <= 0) {
95+
throw 'Session length must be a value greater than 0.'
96+
}
97+
}
98+
99+
generateSessionExpiresAt() {
100+
var now = new Date();
101+
return new Date(now.getTime() + (this.sessionLength*1000));
102+
}
103+
86104
get invalidLinkURL() {
87105
return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`;
88106
}

src/ParseServer.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ addParseCloud();
7575
// "restAPIKey": optional key from Parse dashboard
7676
// "javascriptKey": optional key from Parse dashboard
7777
// "push": optional key from configure push
78+
// "sessionLength": optional length in seconds for how long Sessions should be valid for
7879

7980
class ParseServer {
8081

@@ -111,7 +112,8 @@ class ParseServer {
111112
choosePassword: undefined,
112113
passwordResetSuccess: undefined
113114
},
114-
liveQuery = {}
115+
liveQuery = {},
116+
sessionLength = 31536000, // 1 Year in seconds
115117
}) {
116118
// Initialize the node client SDK automatically
117119
Parse.initialize(appId, javascriptKey || 'unused', masterKey);
@@ -185,7 +187,8 @@ class ParseServer {
185187
publicServerURL: publicServerURL,
186188
customPages: customPages,
187189
maxUploadSize: maxUploadSize,
188-
liveQueryController: liveQueryController
190+
liveQueryController: liveQueryController,
191+
sessionLength : Number(sessionLength),
189192
});
190193

191194
// To maintain compatibility. TODO: Remove in some version that breaks backwards compatability

src/RestWrite.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,7 @@ RestWrite.prototype.transformUser = function() {
319319
var token = 'r:' + cryptoUtils.newToken();
320320
this.storage['token'] = token;
321321
promise = promise.then(() => {
322-
var expiresAt = new Date();
323-
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
322+
var expiresAt = this.config.generateSessionExpiresAt();
324323
var sessionData = {
325324
sessionToken: token,
326325
user: {
@@ -474,8 +473,7 @@ RestWrite.prototype.handleSession = function() {
474473

475474
if (!this.query && !this.auth.isMaster) {
476475
var token = 'r:' + cryptoUtils.newToken();
477-
var expiresAt = new Date();
478-
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
476+
var expiresAt = this.config.generateSessionExpiresAt();
479477
var sessionData = {
480478
sessionToken: token,
481479
user: {
@@ -739,6 +737,7 @@ RestWrite.prototype.runDatabaseOperation = function() {
739737
ACL['*'] = { read: true, write: false };
740738
this.data.ACL = ACL;
741739
}
740+
742741
// Run a create
743742
return this.config.database.create(this.className, this.data, this.runOptions)
744743
.then((resp) => {

src/Routers/UsersRouter.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,7 @@ export class UsersRouter extends ClassesRouter {
108108

109109
req.config.filesController.expandFilesInObject(req.config, user);
110110

111-
let expiresAt = new Date();
112-
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
113-
111+
let expiresAt = req.config.generateSessionExpiresAt();
114112
let sessionData = {
115113
sessionToken: token,
116114
user: {

src/middlewares.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,15 @@ function handleParseHeaders(req, res, next) {
128128
}
129129
})
130130
.catch((error) => {
131-
// TODO: Determine the correct error scenario.
132-
log.error('error getting auth for sessionToken', error);
133-
throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error);
131+
if(error instanceof Parse.Error) {
132+
next(error);
133+
return;
134+
}
135+
else {
136+
// TODO: Determine the correct error scenario.
137+
log.error('error getting auth for sessionToken', error);
138+
throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error);
139+
}
134140
});
135141
}
136142

0 commit comments

Comments
 (0)