Skip to content

Commit

Permalink
feat: extendSessionOnUse to automatically renew Parse Sessions (#8505)
Browse files Browse the repository at this point in the history
  • Loading branch information
dblythy committed May 17, 2023
1 parent 559b1de commit 6f885d3
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 2 deletions.
29 changes: 29 additions & 0 deletions spec/Auth.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,35 @@ describe('Auth', () => {
});
});

it('can use extendSessionOnUse', async () => {
await reconfigureServer({
extendSessionOnUse: true,
});

const user = new Parse.User();
await user.signUp({
username: 'hello',
password: 'password',
});
const session = await new Parse.Query(Parse.Session).first();
const updatedAt = new Date('2010');
const expiry = new Date();
expiry.setHours(expiry.getHours() + 1);

await Parse.Server.database.update(
'_Session',
{ objectId: session.id },
{
expiresAt: { __type: 'Date', iso: expiry.toISOString() },
updatedAt: updatedAt.toISOString(),
}
);
await session.fetch();
await new Promise(resolve => setTimeout(resolve, 1000));
await session.fetch();
expect(session.get('expiresAt') > expiry).toBeTrue();
});

it('should load auth without a config', async () => {
const user = new Parse.User();
await user.signUp({
Expand Down
16 changes: 16 additions & 0 deletions spec/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,22 @@ describe('server', () => {
});
});

it('should throw when extendSessionOnUse is invalid', async () => {
await expectAsync(
reconfigureServer({
extendSessionOnUse: 'yolo',
})
).toBeRejectedWith('extendSessionOnUse must be a boolean value');
});

it('should throw when revokeSessionOnPasswordReset is invalid', async () => {
await expectAsync(
reconfigureServer({
revokeSessionOnPasswordReset: 'yolo',
})
).toBeRejectedWith('revokeSessionOnPasswordReset must be a boolean value');
});

it('fails if the session length is not a number', done => {
reconfigureServer({ sessionLength: 'test' })
.then(done.fail)
Expand Down
50 changes: 48 additions & 2 deletions src/Auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { isDeepStrictEqual } from 'util';
import { getRequestObject, resolveError } from './triggers';
import Deprecator from './Deprecator/Deprecator';
import { logger } from './logger';
import RestQuery from './RestQuery';
import RestWrite from './RestWrite';

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

const throttle = {};
const renewSessionIfNeeded = async ({ config, session, sessionToken }) => {
if (!config?.extendSessionOnUse) {
return;
}
clearTimeout(throttle[sessionToken]);
throttle[sessionToken] = setTimeout(async () => {
try {
if (!session) {
const { results } = await new RestQuery(
config,
master(config),
'_Session',
{ sessionToken },
{ limit: 1 }
).execute();
console.log({ results });
session = results[0];
}
const lastUpdated = new Date(session?.updatedAt);
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
if (lastUpdated > yesterday || !session) {
return;
}
const expiresAt = config.generateSessionExpiresAt();
await new RestWrite(
config,
master(config),
'_Session',
{ objectId: session.objectId },
{ expiresAt: Parse._encode(expiresAt) }
).execute();
} catch (e) {
if (e?.code !== Parse.Error.OBJECT_NOT_FOUND) {
logger.error('Could not update session expiry: ', e);
}
}
}, 500);
};

// Returns a promise that resolves to an Auth object
const getAuthForSessionToken = async function ({
config,
Expand All @@ -78,6 +121,7 @@ const getAuthForSessionToken = async function ({
const userJSON = await cacheController.user.get(sessionToken);
if (userJSON) {
const cachedUser = Parse.Object.fromJSON(userJSON);
renewSessionIfNeeded({ config, sessionToken });
return Promise.resolve(
new Auth({
config,
Expand Down Expand Up @@ -112,18 +156,20 @@ const getAuthForSessionToken = async function ({
if (results.length !== 1 || !results[0]['user']) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
}
const session = results[0];
const now = new Date(),
expiresAt = results[0].expiresAt ? new Date(results[0].expiresAt.iso) : undefined;
expiresAt = session.expiresAt ? new Date(session.expiresAt.iso) : undefined;
if (expiresAt < now) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token is expired.');
}
const obj = results[0]['user'];
const obj = session.user;
delete obj.password;
obj['className'] = '_User';
obj['sessionToken'] = sessionToken;
if (cacheController) {
cacheController.user.put(sessionToken, obj);
}
renewSessionIfNeeded({ config, session, sessionToken });
const userObject = Parse.Object.fromJSON(obj);
return new Auth({
config,
Expand Down
5 changes: 5 additions & 0 deletions src/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export class Config {
logLevels,
rateLimit,
databaseOptions,
extendSessionOnUse,
}) {
if (masterKey === readOnlyMasterKey) {
throw new Error('masterKey and readOnlyMasterKey should be different');
Expand All @@ -103,6 +104,10 @@ export class Config {
throw 'revokeSessionOnPasswordReset must be a boolean value';
}

if (typeof extendSessionOnUse !== 'boolean') {
throw 'extendSessionOnUse must be a boolean value';
}

if (publicServerURL) {
if (!publicServerURL.startsWith('http://') && !publicServerURL.startsWith('https://')) {
throw 'publicServerURL should be a valid HTTPS URL starting with https://';
Expand Down
6 changes: 6 additions & 0 deletions src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,12 @@ module.exports.ParseServerOptions = {
action: parsers.booleanParser,
default: true,
},
extendSessionOnUse: {
env: 'PARSE_SERVER_EXTEND_SESSION_ON_USE',
help: 'Whether Parse Server should automatically extend a valid session by the sessionLength',
action: parsers.booleanParser,
default: false,
},
fileKey: {
env: 'PARSE_SERVER_FILE_KEY',
help: 'Key for your files',
Expand Down
1 change: 1 addition & 0 deletions src/Options/docs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ export interface ParseServerOptions {
/* Session duration, in seconds, defaults to 1 year
:DEFAULT: 31536000 */
sessionLength: ?number;
/* Whether Parse Server should automatically extend a valid session by the sessionLength
:DEFAULT: false */
extendSessionOnUse: ?boolean;
/* Default value for limit option on queries, defaults to `100`.
:DEFAULT: 100 */
defaultLimit: ?number;
Expand Down

0 comments on commit 6f885d3

Please sign in to comment.