express-session: Extend session lifetime if user is active

This commit is contained in:
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
created unless `requireAuthentication` is `true` (or a plugin causes them to
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
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.,
@ -47,7 +48,7 @@
### Compatibility changes
* 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

View File

@ -378,24 +378,46 @@
"sameSite": "Lax",
/*
* How long (in milliseconds) a session lasts before the user is required to
* log in again. (The express_sid cookie is set to expire at time now +
* sessionLifetime when first created.) If requireAuthentication is false
* then this value does not really matter.
* How long (in milliseconds) after navigating away from Etherpad before the
* user is required to log in again. (The express_sid cookie is set to
* expire at time now + sessionLifetime when first created, and its
* 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
* convenience you desire. A long lifetime is more convenient (users won't
* have to log back in as often) but has some drawbacks:
* - It increases the amount of state kept in the database.
* - It might weaken security somewhat: Once a user has accessed a pad,
* the user can continue to use the pad until the session expires.
* - It might weaken security somewhat: The cookie expiration is refreshed
* 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
* 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
* 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,
readonly: sessionInfo.readonly,
serverTimestamp: Date.now(),
sessionRefreshInterval: settings.cookie.sessionRefreshInterval,
userId: sessionInfo.author,
abiwordAvailable: settings.abiwordAvailable(),
sofficeAvailable: settings.sofficeAvailable(),

View File

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

View File

@ -105,6 +105,19 @@ exports.expressCreateServer = (hookName, args, cb) => {
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");
// 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

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();
};

View File

@ -323,6 +323,7 @@ exports.cookie = {
*/
sameSite: 'Lax',
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') {
receivedClientVars = true;
window.clientVars = obj.data;
if (window.clientVars.sessionRefreshInterval) {
const ping =
() => $.ajax('../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {});
setInterval(ping, window.clientVars.sessionRefreshInterval);
}
} else if (obj.disconnect) {
padconnectionstatus.disconnected(obj.disconnect);
socket.disconnect();

View File

@ -111,6 +111,12 @@ const handleClientVars = (message) => {
// save the client Vars
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
BroadcastSlider = require('./broadcast_slider')
.loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded);