Skip to content

Commit 6f885d3

Browse files
authored
feat: extendSessionOnUse to automatically renew Parse Sessions (#8505)
1 parent 559b1de commit 6f885d3

File tree

7 files changed

+108
-2
lines changed

7 files changed

+108
-2
lines changed

spec/Auth.spec.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,35 @@ describe('Auth', () => {
9494
});
9595
});
9696

97+
it('can use extendSessionOnUse', async () => {
98+
await reconfigureServer({
99+
extendSessionOnUse: true,
100+
});
101+
102+
const user = new Parse.User();
103+
await user.signUp({
104+
username: 'hello',
105+
password: 'password',
106+
});
107+
const session = await new Parse.Query(Parse.Session).first();
108+
const updatedAt = new Date('2010');
109+
const expiry = new Date();
110+
expiry.setHours(expiry.getHours() + 1);
111+
112+
await Parse.Server.database.update(
113+
'_Session',
114+
{ objectId: session.id },
115+
{
116+
expiresAt: { __type: 'Date', iso: expiry.toISOString() },
117+
updatedAt: updatedAt.toISOString(),
118+
}
119+
);
120+
await session.fetch();
121+
await new Promise(resolve => setTimeout(resolve, 1000));
122+
await session.fetch();
123+
expect(session.get('expiresAt') > expiry).toBeTrue();
124+
});
125+
97126
it('should load auth without a config', async () => {
98127
const user = new Parse.User();
99128
await user.signUp({

spec/index.spec.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,22 @@ describe('server', () => {
367367
});
368368
});
369369

370+
it('should throw when extendSessionOnUse is invalid', async () => {
371+
await expectAsync(
372+
reconfigureServer({
373+
extendSessionOnUse: 'yolo',
374+
})
375+
).toBeRejectedWith('extendSessionOnUse must be a boolean value');
376+
});
377+
378+
it('should throw when revokeSessionOnPasswordReset is invalid', async () => {
379+
await expectAsync(
380+
reconfigureServer({
381+
revokeSessionOnPasswordReset: 'yolo',
382+
})
383+
).toBeRejectedWith('revokeSessionOnPasswordReset must be a boolean value');
384+
});
385+
370386
it('fails if the session length is not a number', done => {
371387
reconfigureServer({ sessionLength: 'test' })
372388
.then(done.fail)

src/Auth.js

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { isDeepStrictEqual } from 'util';
33
import { getRequestObject, resolveError } from './triggers';
44
import Deprecator from './Deprecator/Deprecator';
55
import { logger } from './logger';
6+
import RestQuery from './RestQuery';
7+
import RestWrite from './RestWrite';
68

79
// An Auth object tells you who is requesting something and whether
810
// the master key was used.
@@ -66,6 +68,47 @@ function nobody(config) {
6668
return new Auth({ config, isMaster: false });
6769
}
6870

71+
const throttle = {};
72+
const renewSessionIfNeeded = async ({ config, session, sessionToken }) => {
73+
if (!config?.extendSessionOnUse) {
74+
return;
75+
}
76+
clearTimeout(throttle[sessionToken]);
77+
throttle[sessionToken] = setTimeout(async () => {
78+
try {
79+
if (!session) {
80+
const { results } = await new RestQuery(
81+
config,
82+
master(config),
83+
'_Session',
84+
{ sessionToken },
85+
{ limit: 1 }
86+
).execute();
87+
console.log({ results });
88+
session = results[0];
89+
}
90+
const lastUpdated = new Date(session?.updatedAt);
91+
const yesterday = new Date();
92+
yesterday.setDate(yesterday.getDate() - 1);
93+
if (lastUpdated > yesterday || !session) {
94+
return;
95+
}
96+
const expiresAt = config.generateSessionExpiresAt();
97+
await new RestWrite(
98+
config,
99+
master(config),
100+
'_Session',
101+
{ objectId: session.objectId },
102+
{ expiresAt: Parse._encode(expiresAt) }
103+
).execute();
104+
} catch (e) {
105+
if (e?.code !== Parse.Error.OBJECT_NOT_FOUND) {
106+
logger.error('Could not update session expiry: ', e);
107+
}
108+
}
109+
}, 500);
110+
};
111+
69112
// Returns a promise that resolves to an Auth object
70113
const getAuthForSessionToken = async function ({
71114
config,
@@ -78,6 +121,7 @@ const getAuthForSessionToken = async function ({
78121
const userJSON = await cacheController.user.get(sessionToken);
79122
if (userJSON) {
80123
const cachedUser = Parse.Object.fromJSON(userJSON);
124+
renewSessionIfNeeded({ config, sessionToken });
81125
return Promise.resolve(
82126
new Auth({
83127
config,
@@ -112,18 +156,20 @@ const getAuthForSessionToken = async function ({
112156
if (results.length !== 1 || !results[0]['user']) {
113157
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
114158
}
159+
const session = results[0];
115160
const now = new Date(),
116-
expiresAt = results[0].expiresAt ? new Date(results[0].expiresAt.iso) : undefined;
161+
expiresAt = session.expiresAt ? new Date(session.expiresAt.iso) : undefined;
117162
if (expiresAt < now) {
118163
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token is expired.');
119164
}
120-
const obj = results[0]['user'];
165+
const obj = session.user;
121166
delete obj.password;
122167
obj['className'] = '_User';
123168
obj['sessionToken'] = sessionToken;
124169
if (cacheController) {
125170
cacheController.user.put(sessionToken, obj);
126171
}
172+
renewSessionIfNeeded({ config, session, sessionToken });
127173
const userObject = Parse.Object.fromJSON(obj);
128174
return new Auth({
129175
config,

src/Config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export class Config {
8686
logLevels,
8787
rateLimit,
8888
databaseOptions,
89+
extendSessionOnUse,
8990
}) {
9091
if (masterKey === readOnlyMasterKey) {
9192
throw new Error('masterKey and readOnlyMasterKey should be different');
@@ -103,6 +104,10 @@ export class Config {
103104
throw 'revokeSessionOnPasswordReset must be a boolean value';
104105
}
105106

107+
if (typeof extendSessionOnUse !== 'boolean') {
108+
throw 'extendSessionOnUse must be a boolean value';
109+
}
110+
106111
if (publicServerURL) {
107112
if (!publicServerURL.startsWith('http://') && !publicServerURL.startsWith('https://')) {
108113
throw 'publicServerURL should be a valid HTTPS URL starting with https://';

src/Options/Definitions.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,12 @@ module.exports.ParseServerOptions = {
227227
action: parsers.booleanParser,
228228
default: true,
229229
},
230+
extendSessionOnUse: {
231+
env: 'PARSE_SERVER_EXTEND_SESSION_ON_USE',
232+
help: 'Whether Parse Server should automatically extend a valid session by the sessionLength',
233+
action: parsers.booleanParser,
234+
default: false,
235+
},
230236
fileKey: {
231237
env: 'PARSE_SERVER_FILE_KEY',
232238
help: 'Key for your files',

src/Options/docs.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Options/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,9 @@ export interface ParseServerOptions {
203203
/* Session duration, in seconds, defaults to 1 year
204204
:DEFAULT: 31536000 */
205205
sessionLength: ?number;
206+
/* Whether Parse Server should automatically extend a valid session by the sessionLength
207+
:DEFAULT: false */
208+
extendSessionOnUse: ?boolean;
206209
/* Default value for limit option on queries, defaults to `100`.
207210
:DEFAULT: 100 */
208211
defaultLimit: ?number;

0 commit comments

Comments
 (0)