feat: extendSessionOnUse to automatically renew Parse Sessions (#8505)
This commit is contained in:
@@ -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 () => {
|
it('should load auth without a config', async () => {
|
||||||
const user = new Parse.User();
|
const user = new Parse.User();
|
||||||
await user.signUp({
|
await user.signUp({
|
||||||
|
|||||||
@@ -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 => {
|
it('fails if the session length is not a number', done => {
|
||||||
reconfigureServer({ sessionLength: 'test' })
|
reconfigureServer({ sessionLength: 'test' })
|
||||||
.then(done.fail)
|
.then(done.fail)
|
||||||
|
|||||||
50
src/Auth.js
50
src/Auth.js
@@ -3,6 +3,8 @@ import { isDeepStrictEqual } from 'util';
|
|||||||
import { getRequestObject, resolveError } from './triggers';
|
import { getRequestObject, resolveError } from './triggers';
|
||||||
import Deprecator from './Deprecator/Deprecator';
|
import Deprecator from './Deprecator/Deprecator';
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
|
import RestQuery from './RestQuery';
|
||||||
|
import RestWrite from './RestWrite';
|
||||||
|
|
||||||
// An Auth object tells you who is requesting something and whether
|
// An Auth object tells you who is requesting something and whether
|
||||||
// the master key was used.
|
// the master key was used.
|
||||||
@@ -66,6 +68,47 @@ function nobody(config) {
|
|||||||
return new Auth({ config, isMaster: false });
|
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
|
// Returns a promise that resolves to an Auth object
|
||||||
const getAuthForSessionToken = async function ({
|
const getAuthForSessionToken = async function ({
|
||||||
config,
|
config,
|
||||||
@@ -78,6 +121,7 @@ const getAuthForSessionToken = async function ({
|
|||||||
const userJSON = await cacheController.user.get(sessionToken);
|
const userJSON = await cacheController.user.get(sessionToken);
|
||||||
if (userJSON) {
|
if (userJSON) {
|
||||||
const cachedUser = Parse.Object.fromJSON(userJSON);
|
const cachedUser = Parse.Object.fromJSON(userJSON);
|
||||||
|
renewSessionIfNeeded({ config, sessionToken });
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
new Auth({
|
new Auth({
|
||||||
config,
|
config,
|
||||||
@@ -112,18 +156,20 @@ const getAuthForSessionToken = async function ({
|
|||||||
if (results.length !== 1 || !results[0]['user']) {
|
if (results.length !== 1 || !results[0]['user']) {
|
||||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
|
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
|
||||||
}
|
}
|
||||||
|
const session = results[0];
|
||||||
const now = new Date(),
|
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) {
|
if (expiresAt < now) {
|
||||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token is expired.');
|
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;
|
delete obj.password;
|
||||||
obj['className'] = '_User';
|
obj['className'] = '_User';
|
||||||
obj['sessionToken'] = sessionToken;
|
obj['sessionToken'] = sessionToken;
|
||||||
if (cacheController) {
|
if (cacheController) {
|
||||||
cacheController.user.put(sessionToken, obj);
|
cacheController.user.put(sessionToken, obj);
|
||||||
}
|
}
|
||||||
|
renewSessionIfNeeded({ config, session, sessionToken });
|
||||||
const userObject = Parse.Object.fromJSON(obj);
|
const userObject = Parse.Object.fromJSON(obj);
|
||||||
return new Auth({
|
return new Auth({
|
||||||
config,
|
config,
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export class Config {
|
|||||||
logLevels,
|
logLevels,
|
||||||
rateLimit,
|
rateLimit,
|
||||||
databaseOptions,
|
databaseOptions,
|
||||||
|
extendSessionOnUse,
|
||||||
}) {
|
}) {
|
||||||
if (masterKey === readOnlyMasterKey) {
|
if (masterKey === readOnlyMasterKey) {
|
||||||
throw new Error('masterKey and readOnlyMasterKey should be different');
|
throw new Error('masterKey and readOnlyMasterKey should be different');
|
||||||
@@ -103,6 +104,10 @@ export class Config {
|
|||||||
throw 'revokeSessionOnPasswordReset must be a boolean value';
|
throw 'revokeSessionOnPasswordReset must be a boolean value';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof extendSessionOnUse !== 'boolean') {
|
||||||
|
throw 'extendSessionOnUse must be a boolean value';
|
||||||
|
}
|
||||||
|
|
||||||
if (publicServerURL) {
|
if (publicServerURL) {
|
||||||
if (!publicServerURL.startsWith('http://') && !publicServerURL.startsWith('https://')) {
|
if (!publicServerURL.startsWith('http://') && !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://';
|
||||||
|
|||||||
@@ -227,6 +227,12 @@ module.exports.ParseServerOptions = {
|
|||||||
action: parsers.booleanParser,
|
action: parsers.booleanParser,
|
||||||
default: true,
|
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: {
|
fileKey: {
|
||||||
env: 'PARSE_SERVER_FILE_KEY',
|
env: 'PARSE_SERVER_FILE_KEY',
|
||||||
help: 'Key for your files',
|
help: 'Key for your files',
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
* @property {String} encryptionKey Key for encrypting your files
|
* @property {String} encryptionKey Key for encrypting your files
|
||||||
* @property {Boolean} enforcePrivateUsers Set to true if new users should be created without public read and write access.
|
* @property {Boolean} enforcePrivateUsers Set to true if new users should be created without public read and write access.
|
||||||
* @property {Boolean} expireInactiveSessions Sets whether we should expire the inactive sessions, defaults to true. If false, all new sessions are created with no expiration date.
|
* @property {Boolean} expireInactiveSessions Sets whether we should expire the inactive sessions, defaults to true. If false, all new sessions are created with no expiration date.
|
||||||
|
* @property {Boolean} extendSessionOnUse Whether Parse Server should automatically extend a valid session by the sessionLength
|
||||||
* @property {String} fileKey Key for your files
|
* @property {String} fileKey Key for your files
|
||||||
* @property {Adapter<FilesAdapter>} filesAdapter Adapter module for the files sub-system
|
* @property {Adapter<FilesAdapter>} filesAdapter Adapter module for the files sub-system
|
||||||
* @property {FileUploadOptions} fileUpload Options for file uploads
|
* @property {FileUploadOptions} fileUpload Options for file uploads
|
||||||
|
|||||||
@@ -203,6 +203,9 @@ export interface ParseServerOptions {
|
|||||||
/* Session duration, in seconds, defaults to 1 year
|
/* Session duration, in seconds, defaults to 1 year
|
||||||
:DEFAULT: 31536000 */
|
:DEFAULT: 31536000 */
|
||||||
sessionLength: ?number;
|
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 value for limit option on queries, defaults to `100`.
|
||||||
:DEFAULT: 100 */
|
:DEFAULT: 100 */
|
||||||
defaultLimit: ?number;
|
defaultLimit: ?number;
|
||||||
|
|||||||
Reference in New Issue
Block a user