express-session: Extend session lifetime if user is active

pull/5348/head
Richard Hansen 2021-12-23 01:45:38 -05:00
parent 9c1f52f1b0
commit 692749d1cf
9 changed files with 68 additions and 10 deletions

View File

@ -6,7 +6,8 @@
* `express_sid` cookies and `sessionstorage:*` database records are no longer * `express_sid` cookies and `sessionstorage:*` database records are no longer
created unless `requireAuthentication` is `true` (or a plugin causes them to created unless `requireAuthentication` is `true` (or a plugin causes them to
be created). be created).
* Login sessions now have a finite lifetime by default (10 days). * Login sessions now have a finite lifetime by default (10 days after
leaving).
* `sessionstorage:*` database records are automatically deleted when the login * `sessionstorage:*` database records are automatically deleted when the login
session expires (with some exceptions that will be fixed in the future). session expires (with some exceptions that will be fixed in the future).
* Requests for static content (e.g., `/robots.txt`) and special pages (e.g., * Requests for static content (e.g., `/robots.txt`) and special pages (e.g.,
@ -47,7 +48,7 @@
### Compatibility changes ### Compatibility changes
* The default login session expiration (applicable if `requireAuthentication` is * The default login session expiration (applicable if `requireAuthentication` is
`true`) changed from never to 10 days. `true`) changed from never to 10 days after the user leaves.
#### For plugin authors #### For plugin authors

View File

@ -378,24 +378,46 @@
"sameSite": "Lax", "sameSite": "Lax",
/* /*
* How long (in milliseconds) a session lasts before the user is required to * How long (in milliseconds) after navigating away from Etherpad before the
* log in again. (The express_sid cookie is set to expire at time now + * user is required to log in again. (The express_sid cookie is set to
* sessionLifetime when first created.) If requireAuthentication is false * expire at time now + sessionLifetime when first created, and its
* then this value does not really matter. * expiration time is periodically refreshed to a new now + sessionLifetime
* value.) If requireAuthentication is false then this value does not really
* matter.
* *
* The "best" value depends on your users' usage patterns and the amount of * The "best" value depends on your users' usage patterns and the amount of
* convenience you desire. A long lifetime is more convenient (users won't * convenience you desire. A long lifetime is more convenient (users won't
* have to log back in as often) but has some drawbacks: * have to log back in as often) but has some drawbacks:
* - It increases the amount of state kept in the database. * - It increases the amount of state kept in the database.
* - It might weaken security somewhat: Once a user has accessed a pad, * - It might weaken security somewhat: The cookie expiration is refreshed
* the user can continue to use the pad until the session expires. * indefinitely without consulting authentication or authorization
* hooks, so once a user has accessed a pad, the user can continue to
* use the pad until the user leaves for longer than sessionLifetime.
* *
* Session lifetime can be set to infinity (not recommended) by setting this * Session lifetime can be set to infinity (not recommended) by setting this
* to null or 0. Note that if the session does not expire, most browsers * to null or 0. Note that if the session does not expire, most browsers
* will delete the cookie when the browser exits, but a session record is * will delete the cookie when the browser exits, but a session record is
* kept in the database forever. * kept in the database forever.
*/ */
"sessionLifetime": 864000000 // = 10d * 24h/d * 60m/h * 60s/m * 1000ms/s "sessionLifetime": 864000000, // = 10d * 24h/d * 60m/h * 60s/m * 1000ms/s
/*
* How long (in milliseconds) before the expiration time of an active user's
* session is refreshed (to now + sessionLifetime). This setting affects the
* following:
* - How often a new session expiration time will be written to the
* database.
* - How often each user's browser will ping the Etherpad server to
* refresh the expiration time of the session cookie.
*
* High values reduce the load on the database and the load from browsers,
* but can shorten the effective session lifetime if Etherpad is restarted
* or the user navigates away.
*
* Automatic session refreshes can be disabled (not recommended) by setting
* this to null.
*/
"sessionRefreshInterval": 86400000 // = 1d * 24h/d * 60m/h * 60s/m * 1000ms/s
}, },
/* /*

View File

@ -998,6 +998,7 @@ const handleClientReady = async (socket, message) => {
readOnlyId: sessionInfo.readOnlyPadId, readOnlyId: sessionInfo.readOnlyPadId,
readonly: sessionInfo.readonly, readonly: sessionInfo.readonly,
serverTimestamp: Date.now(), serverTimestamp: Date.now(),
sessionRefreshInterval: settings.cookie.sessionRefreshInterval,
userId: sessionInfo.author, userId: sessionInfo.author,
abiwordAvailable: settings.abiwordAvailable(), abiwordAvailable: settings.abiwordAvailable(),
sofficeAvailable: settings.sofficeAvailable(), sofficeAvailable: settings.sofficeAvailable(),

View File

@ -176,8 +176,10 @@ exports.restartServer = async () => {
app.use(cookieParser(settings.sessionKey, {})); app.use(cookieParser(settings.sessionKey, {}));
sessionStore = new SessionStore(); sessionStore = new SessionStore(settings.cookie.sessionRefreshInterval);
exports.sessionMiddleware = expressSession({ exports.sessionMiddleware = expressSession({
propagateTouch: true,
rolling: true,
secret: settings.sessionKey, secret: settings.sessionKey,
store: sessionStore, store: sessionStore,
resave: false, resave: false,

View File

@ -105,6 +105,19 @@ exports.expressCreateServer = (hookName, args, cb) => {
express.sessionMiddleware(req, {}, next); express.sessionMiddleware(req, {}, next);
}); });
io.use((socket, next) => {
socket.conn.on('packet', (packet) => {
// Tell express-session that the session is still active. The session store can use these
// touch events to defer automatic session cleanup, and if express-session is configured with
// rolling=true the cookie's expiration time will be renewed. (Note that WebSockets does not
// have a standard mechanism for periodically updating the browser's cookies, so the browser
// will not see the new cookie expiration time unless it makes a new HTTP request or the new
// cookie value is sent to the client in a custom socket.io message.)
if (socket.request.session != null) socket.request.session.touch();
});
next();
});
// var socketIOLogger = log4js.getLogger("socket.io"); // var socketIOLogger = log4js.getLogger("socket.io");
// Debug logging now has to be set at an environment level, this is stupid. // Debug logging now has to be set at an environment level, this is stupid.
// https://github.com/Automattic/socket.io/wiki/Migrating-to-1.0 // https://github.com/Automattic/socket.io/wiki/Migrating-to-1.0

View File

@ -106,5 +106,12 @@ exports.expressCreateServer = (hookName, args, cb) => {
})); }));
}); });
// The client occasionally polls this endpoint to get an updated expiration for the express_sid
// cookie. This handler must be installed after the express-session middleware.
args.app.put('/_extendExpressSessionLifetime', (req, res) => {
// express-session automatically calls req.session.touch() so we don't need to do it here.
res.json({status: 'ok'});
});
return cb(); return cb();
}; };

View File

@ -323,6 +323,7 @@ exports.cookie = {
*/ */
sameSite: 'Lax', sameSite: 'Lax',
sessionLifetime: 10 * 24 * 60 * 60 * 1000, sessionLifetime: 10 * 24 * 60 * 60 * 1000,
sessionRefreshInterval: 1 * 24 * 60 * 60 * 1000,
}; };
/* /*

View File

@ -293,6 +293,11 @@ const handshake = async () => {
} else if (!receivedClientVars && obj.type === 'CLIENT_VARS') { } else if (!receivedClientVars && obj.type === 'CLIENT_VARS') {
receivedClientVars = true; receivedClientVars = true;
window.clientVars = obj.data; window.clientVars = obj.data;
if (window.clientVars.sessionRefreshInterval) {
const ping =
() => $.ajax('../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {});
setInterval(ping, window.clientVars.sessionRefreshInterval);
}
} else if (obj.disconnect) { } else if (obj.disconnect) {
padconnectionstatus.disconnected(obj.disconnect); padconnectionstatus.disconnected(obj.disconnect);
socket.disconnect(); socket.disconnect();

View File

@ -111,6 +111,12 @@ const handleClientVars = (message) => {
// save the client Vars // save the client Vars
window.clientVars = message.data; window.clientVars = message.data;
if (window.clientVars.sessionRefreshInterval) {
const ping =
() => $.ajax('../../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {});
setInterval(ping, window.clientVars.sessionRefreshInterval);
}
// load all script that doesn't work without the clientVars // load all script that doesn't work without the clientVars
BroadcastSlider = require('./broadcast_slider') BroadcastSlider = require('./broadcast_slider')
.loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded); .loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded);