diff --git a/README.md b/README.md index 30b78a2..b4562d5 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,12 @@ Setting this to `false` can save storage space and comply with the EU cookie law ##### rolling (optional) Forces the session identifier cookie to be set on every response. The expiration is reset to the original maxAge - effectively resetting the cookie lifetime. This is typically used in conjuction with short, non-session-length maxAge values to provide a quick expiration of the session data with reduced potential of session expiration occurring during ongoing server interactions. Defaults to true. +##### refresh (optional) +Automatically refresh ( extend the expiry of ) session before `` milliseconds before `expiry`. This is more efficient way than setting `rolling` option. +The default value is `0 ms` meaning, this feature is disabled. +Consider `cookie.maxAge` is `60 seconds`. If we set `refresh` = `20 seconds`, then it will auto refresh the session if sent any request after 40 second. +It is recommended to disable `rolling` and `saveUninitialized` options if we set this option. + ##### idGenerator(request) (optional) Function used to generate new session IDs. diff --git a/lib/cookie.js b/lib/cookie.js index d157bd6..6c93a90 100644 --- a/lib/cookie.js +++ b/lib/cookie.js @@ -13,6 +13,9 @@ module.exports = class Cookie { this._expires = null if (originalMaxAge) { + if (cookie.expires) { + this.expires = new Date(cookie.expires) + } this.maxAge = originalMaxAge } else if (cookie.expires) { this.expires = new Date(cookie.expires) @@ -40,7 +43,7 @@ module.exports = class Cookie { } set maxAge (ms) { - this.expires = new Date(Date.now() + ms) + if (!this.expires) { this.expires = new Date(Date.now() + ms) } // we force the same originalMaxAge to match old behavior this.originalMaxAge = ms } diff --git a/lib/fastifySession.js b/lib/fastifySession.js index fb45ab9..b42c27f 100644 --- a/lib/fastifySession.js +++ b/lib/fastifySession.js @@ -62,7 +62,8 @@ function fastifySession (fastify, options, next) { request, idGenerator, cookieOpts, - cookieSigner + cookieSigner, + session ) done() } else { @@ -92,7 +93,6 @@ function fastifySession (fastify, options, next) { session, decryptedSessionId ) - if (restoredSession.cookie.expires && restoredSession.cookie.expires.getTime() <= Date.now()) { restoredSession.destroy(err => { if (err) { @@ -156,6 +156,7 @@ function fastifySession (fastify, options, next) { const cookieOpts = options.cookie const saveUninitializedSession = options.saveUninitialized const rollingSessions = options.rolling + const refresh = options.refresh return function saveSession (request, reply, payload, done) { const session = request.session @@ -165,7 +166,7 @@ function fastifySession (fastify, options, next) { } const cookieSessionId = getCookieSessionId(request) - const saveSession = shouldSaveSession(request, cookieSessionId, saveUninitializedSession, rollingSessions) + const saveSession = shouldSaveSession(request, cookieSessionId, saveUninitializedSession, rollingSessions, refresh) const isInsecureConnection = cookieOpts.secure === true && isConnectionSecure(request) === false const sessionIdWithPrefix = hasCookiePrefix ? `${cookiePrefix}${session.encryptedSessionId}` : session.encryptedSessionId if (!saveSession || isInsecureConnection) { @@ -187,6 +188,7 @@ function fastifySession (fastify, options, next) { return } + session.touch() session.save((err) => { if (err) { done(err) @@ -223,6 +225,7 @@ function fastifySession (fastify, options, next) { opts.cookie = options.cookie || {} opts.cookie.secure = option(opts.cookie, 'secure', true) opts.rolling = option(options, 'rolling', true) + opts.refresh = option(options, 'refresh', 0) // refreshing is disabled opts.saveUninitialized = option(options, 'saveUninitialized', true) opts.algorithm = options.algorithm || 'sha256' opts.signer = typeof options.secret === 'string' || Array.isArray(options.secret) @@ -232,10 +235,10 @@ function fastifySession (fastify, options, next) { return opts } - function shouldSaveSession (request, cookieId, saveUninitializedSession, rollingSessions) { + function shouldSaveSession (request, cookieId, saveUninitializedSession, rollingSessions, refresh) { return cookieId !== request.session.encryptedSessionId ? saveUninitializedSession || request.session.isModified() - : rollingSessions || request.session.isModified() + : rollingSessions || shouldRefresh || (Boolean(request.session.cookie.expires) && request.session.isModified()) } function option (options, key, def) { diff --git a/package.json b/package.json index 11c928e..452b2a2 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@fastify/pre-commit": "^2.0.2", "@types/node": "^20.1.0", "connect-mongo": "^5.0.0", - "connect-redis": "^7.0.0", + "connect-redis": "7.0.0", "cronometro": "^1.1.0", "fastify": "^4.3.0", "ioredis": "^5.0.5", diff --git a/test/session.test.js b/test/session.test.js index b26061f..bded243 100644 --- a/test/session.test.js +++ b/test/session.test.js @@ -5,6 +5,11 @@ const Fastify = require('fastify') const fastifyCookie = require('@fastify/cookie') const fastifySession = require('..') const { buildFastify, DEFAULT_OPTIONS, DEFAULT_COOKIE, DEFAULT_SESSION_ID, DEFAULT_SECRET, DEFAULT_COOKIE_VALUE } = require('./util') +const Cookie = require('cookie') + +function sleep (ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} test('should add session object to request', async (t) => { t.plan(2) @@ -401,6 +406,60 @@ test('should bubble up errors with destroy call if session expired', async (t) = t.equal(JSON.parse(response.body).message, 'No can do') }) +test('should refresh session cookie expiration if refresh is set to nonzero', async (t) => { + t.plan(9) + + const fastify = Fastify() + + const options = { + secret: DEFAULT_SECRET, + rolling: false, + saveUninitialized: false, + refresh: 1000, // should refresh cookie after 1 second + cookie: { secure: false, maxAge: 2000 } + } + fastify.register(fastifyCookie) + fastify.register(fastifySession, options) + + fastify.get('/check', (request, reply) => { + request.session.testSessionId = request.session.sessionId + return reply.send(request.session.testSessionId) + }) + await fastify.listen({ port: 0 }) + t.teardown(() => { fastify.close() }) + + let response1 = await fastify.inject({ + url: '/check' + }) + t.equal(response1.statusCode, 200) + t.ok(response1.headers['set-cookie']) + // we should not get 'set-cookie' header if we sent request + // within interval . Here it is 1000 ms + await sleep(500) + let response2 = await fastify.inject({ + url: '/check', + headers: { Cookie: response1.headers['set-cookie'] } + }) + t.equal(response2.statusCode, 200) + t.notOk(response2.headers['set-cookie']) + + response1 = await fastify.inject({ + url: '/check' + }) + t.equal(response1.statusCode, 200) + t.ok(response1.headers['set-cookie']) + // we should get 'set-cookie' header if we sent request + // after interval . Here it is 1000 ms + await sleep(1100) + response2 = await fastify.inject({ + url: '/check', + headers: { Cookie: response1.headers['set-cookie'] } + }) + t.equal(response2.statusCode, 200) + t.ok(response2.headers['set-cookie']) + t.equal(Cookie.parse(response2.headers['set-cookie']).sessionId, Cookie.parse(response1.headers['set-cookie']).sessionId) +}) + test('should not reset session cookie expiration if rolling is false', async (t) => { t.plan(3)