From f69da5d06d87cd14d5581738e49875802a7d6f5b Mon Sep 17 00:00:00 2001 From: Harish Karumuthil Date: Sat, 19 Aug 2023 18:31:58 +0530 Subject: [PATCH 1/5] Add auto-refresh option --- lib/cookie.js | 5 ++++- lib/fastifySession.js | 20 +++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) 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 ea5f346..cad8398 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,13 @@ function fastifySession (fastify, options, next) { return opts } - function shouldSaveSession (request, cookieId, saveUninitializedSession, rollingSessions) { - return cookieId !== request.session.encryptedSessionId - ? saveUninitializedSession || request.session.isModified() - : rollingSessions || (Boolean(request.session.cookie.expires) && request.session.isModified()) + function shouldSaveSession (request, cookieId, saveUninitializedSession, rollingSessions, refresh) { + if (cookieId !== request.session.encryptedSessionId) { + return saveUninitializedSession || request.session.isModified() + } else { + const shouldRefresh = refresh ? request.session.cookie.expires.getTime() < (Date.now() + refresh) : false + return rollingSessions || shouldRefresh || (Boolean(request.session.cookie.expires) && request.session.isModified()) + } } function option (options, key, def) { From bde997c84e59b9ab0966843de3a182ebee8d0d57 Mon Sep 17 00:00:00 2001 From: Harish Karumuthil Date: Sat, 19 Aug 2023 20:08:15 +0530 Subject: [PATCH 2/5] Added unit tests. Fixed dependency version. Later versions of connect-redis exports a {default: ..} object --- package.json | 2 +- test/session.test.js | 59 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 21c3a6b..ce1f663 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,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 a3807e7..230c9f9 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) From 02034acfce8475c7bdc9a367037f9d5faa4c25da Mon Sep 17 00:00:00 2001 From: Harish Karumuthil Date: Sat, 19 Aug 2023 21:10:31 +0530 Subject: [PATCH 3/5] lint:fix --- test/session.test.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/session.test.js b/test/session.test.js index 230c9f9..b1d0a62 100644 --- a/test/session.test.js +++ b/test/session.test.js @@ -5,9 +5,9 @@ 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'); +const Cookie = require('cookie') -function sleep(ms) { +function sleep (ms) { return new Promise(resolve => setTimeout(resolve, ms)) } @@ -422,7 +422,7 @@ test('should refresh session cookie expiration if refresh is set to nonzero', as fastify.register(fastifySession, options) fastify.get('/check', (request, reply) => { - request.session.testSessionId = request.session.sessionId; + request.session.testSessionId = request.session.sessionId return reply.send(request.session.testSessionId) }) await fastify.listen({ port: 0 }) @@ -432,32 +432,32 @@ test('should refresh session cookie expiration if refresh is set to nonzero', as url: '/check' }) t.equal(response1.statusCode, 200) - t.ok(response1.headers['set-cookie']); + 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); + 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']); + t.notOk(response2.headers['set-cookie']) response1 = await fastify.inject({ url: '/check' }) t.equal(response1.statusCode, 200) - t.ok(response1.headers['set-cookie']); + 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); + 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); + 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) => { From 8e5722812950b89f0b192def5e915312db0502ef Mon Sep 17 00:00:00 2001 From: Harish Karumuthil Date: Sat, 19 Aug 2023 21:20:35 +0530 Subject: [PATCH 4/5] Added documentation --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 545c360..eadd097 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. From 0ae8b065412b3b070da383930069f170d93abde1 Mon Sep 17 00:00:00 2001 From: Harish Karumuthil Date: Mon, 21 Aug 2023 04:14:08 +0530 Subject: [PATCH 5/5] Update README.md Co-authored-by: Manuel Spigolon --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eadd097..cd20ebd 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ Forces the session identifier cookie to be set on every response. The expiration 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 +It is recommended to disable `rolling` and `saveUninitialized` options if we set this option. ##### idGenerator(request) (optional)