From a3b19b88fd23130493f214945dd04b78c5fe9bbd Mon Sep 17 00:00:00 2001 From: Jimmy li Date: Mon, 6 Jul 2026 21:49:17 +0800 Subject: [PATCH 01/10] fix: preserve session store when reloading with callback Signed-off-by: Jimmy li --- lib/session.js | 3 ++- test/session.test.js | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/session.js b/lib/session.js index 9cbace1..953d402 100644 --- a/lib/session.js +++ b/lib/session.js @@ -165,8 +165,9 @@ module.exports = class Session { reload (callback) { if (callback) { this[sessionStoreKey].get(this[sessionIdKey], (error, session) => { - this[requestKey].session = new Session(this[requestKey], + this[requestKey].session = new Session( this[sessionStoreKey], + this[requestKey], this[generateId], this[cookieOptsKey], this[cookieSignerKey], diff --git a/test/session.test.js b/test/session.test.js index f5acfa8..1358fd1 100644 --- a/test/session.test.js +++ b/test/session.test.js @@ -613,6 +613,32 @@ test('should reload the session', async (t) => { t.assert.strictEqual(response.statusCode, 200) }) +test('should save the reloaded session with callback', async (t) => { + t.plan(3) + const fastify = await buildFastify((request, reply) => { + request.session.someData = 'some-data' + request.session.save((err) => { + t.assert.ifError(err) + + request.session.reload((err) => { + t.assert.ifError(err) + + reply.send(200) + }) + }) + }, { + ...DEFAULT_OPTIONS, + cookie: { secure: false } + }) + t.after(() => fastify.close()) + + const response = await fastify.inject({ + url: '/' + }) + + t.assert.strictEqual(response.statusCode, 200) +}) + test('should save the session', async (t) => { t.plan(6) const fastify = await buildFastify((request, reply) => { From bda832208e09b24d9e55fc228e8b35a9f8a23dce Mon Sep 17 00:00:00 2001 From: Jimmy Date: Sun, 12 Jul 2026 23:34:49 +0800 Subject: [PATCH 02/10] fix session secure cookie override (#342) --- index.js | 2 +- test/base.test.js | 27 +++++++-------- test/cookie.test.js | 81 ++++++++++++++++++++++++++++++++++----------- test/store.test.js | 4 +-- test/util.js | 4 ++- 5 files changed, 82 insertions(+), 36 deletions(-) diff --git a/index.js b/index.js index f4f3c78..25f60bc 100644 --- a/index.js +++ b/index.js @@ -166,7 +166,7 @@ function fastifySession (fastify, options, next) { const cookieSessionId = getCookieSessionId(request) const saveSession = shouldSaveSession(request, cookieSessionId, saveUninitializedSession, rollingSessions) - const isInsecureConnection = cookieOpts.secure === true && request.protocol !== 'https' + const isInsecureConnection = session.cookie.secure === true && request.protocol !== 'https' const sessionIdWithPrefix = hasCookiePrefix ? `${cookiePrefix}${session.encryptedSessionId}` : session.encryptedSessionId if (!saveSession || isInsecureConnection) { // if a session cookie is set, but has a different ID, clear it diff --git a/test/base.test.js b/test/base.test.js index 637fa7e..2b72e69 100644 --- a/test/base.test.js +++ b/test/base.test.js @@ -3,7 +3,7 @@ const test = require('node:test') const Signer = require('@fastify/cookie').Signer const fastifyPlugin = require('fastify-plugin') -const { DEFAULT_OPTIONS, DEFAULT_COOKIE, DEFAULT_SESSION_ID, DEFAULT_SECRET, DEFAULT_ENCRYPTED_SESSION_ID, buildFastify } = require('./util') +const { DEFAULT_OPTIONS, DEFAULT_COOKIE, DEFAULT_SESSION_ID, DEFAULT_SECRET, DEFAULT_ENCRYPTED_SESSION_ID, SIGNED_COOKIE_VALUE_PATTERN, buildFastify } = require('./util') const TestStore = require('./TestStore') const { setTimeout: sleep } = require('timers/promises') @@ -72,7 +72,7 @@ test('should set session cookie', async (t) => { }) t.assert.strictEqual(response1.statusCode, 200) - const pattern1 = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly; Secure` + const pattern1 = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; Secure` t.assert.strictEqual(new RegExp(pattern1).test(response1.headers['set-cookie']), true) const response2 = await fastify.inject({ @@ -81,7 +81,7 @@ test('should set session cookie', async (t) => { }) t.assert.strictEqual(response2.statusCode, 200) - const pattern2 = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly; Secure` + const pattern2 = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; Secure` t.assert.strictEqual(new RegExp(pattern2).test(response2.headers['set-cookie']), true) }) @@ -129,7 +129,8 @@ test('should support multiple secrets', async (t) => { } }) t.assert.strictEqual(response1.statusCode, 200) - t.assert.ok(response1.headers['set-cookie'].includes(encodeURIComponent(sessionIdSignedWithNewSecret))) + const setCookieValue = response1.headers['set-cookie'].split(';', 1)[0].slice('sessionId='.length) + t.assert.strictEqual(decodeURIComponent(setCookieValue), sessionIdSignedWithNewSecret) const response2 = await fastify.inject({ url: '/', @@ -162,7 +163,7 @@ test('should set session cookie using the specified cookie name', async (t) => { }) t.assert.strictEqual(response.statusCode, 200) - const pattern = String.raw`anothername=[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly; Secure` + const pattern = String.raw`anothername=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; Secure` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -184,7 +185,7 @@ test('should set session cookie using the default cookie name', async (t) => { }) t.assert.strictEqual(response.statusCode, 200) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly; Secure` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; Secure` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -219,7 +220,7 @@ test('should set express sessions using the specified cookiePrefix', async (t) = }) t.assert.strictEqual(response.statusCode, 200) - const pattern = String.raw`connect.sid=s%3A[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly; Secure` + const pattern = String.raw`connect.sid=s(?::|%3A)${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; Secure` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -315,7 +316,7 @@ test('should set new session cookie if expired', async (t) => { t.assert.strictEqual(response.headers['set-cookie'].includes(DEFAULT_ENCRYPTED_SESSION_ID), false) t.assert.strictEqual(response.statusCode, 200) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly; Secure` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; Secure` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -338,7 +339,7 @@ test('should return new session cookie if does not exist in store', async (t) => t.assert.strictEqual(response.statusCode, 200) t.assert.strictEqual(response.headers['set-cookie'].includes(DEFAULT_ENCRYPTED_SESSION_ID), false) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly; Secure` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; Secure` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -387,7 +388,7 @@ test('should create new session if cookie contains invalid session', async (t) = t.assert.strictEqual(response.statusCode, 200) t.assert.strictEqual(response.headers['set-cookie'].includes('badinvalidsignaturenoooo'), false) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly; Secure` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; Secure` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -427,7 +428,7 @@ test('should handle algorithm sha256', async (t) => { t.assert.strictEqual(response.statusCode, 200) t.assert.strictEqual(response.headers['set-cookie'].includes(DEFAULT_ENCRYPTED_SESSION_ID), false) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly; Secure` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; Secure` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -449,7 +450,7 @@ test('should handle algorithm sha512', async (t) => { t.assert.strictEqual(response.statusCode, 200) t.assert.strictEqual(response.headers['set-cookie'].includes(DEFAULT_ENCRYPTED_SESSION_ID), false) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,135}; Path=\/; HttpOnly; Secure` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; Secure` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -472,6 +473,6 @@ test('should handle custom signer', async (t) => { t.assert.strictEqual(response.statusCode, 200) t.assert.strictEqual(response.headers['set-cookie'].includes(DEFAULT_ENCRYPTED_SESSION_ID), false) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,135}; Path=\/; HttpOnly; Secure` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; Secure` t.assert.strictEqual(RegExp(pattern).test(response.headers['set-cookie']), true) }) diff --git a/test/cookie.test.js b/test/cookie.test.js index 442ed06..e0ad1bf 100644 --- a/test/cookie.test.js +++ b/test/cookie.test.js @@ -6,7 +6,7 @@ const fastifyCookie = require('@fastify/cookie') const fastifySession = require('..') const fastifyPlugin = require('fastify-plugin') const Cookie = require('../lib/cookie') -const { DEFAULT_OPTIONS, DEFAULT_COOKIE, DEFAULT_SECRET, buildFastify, DEFAULT_SESSION_ID } = require('./util') +const { DEFAULT_OPTIONS, DEFAULT_COOKIE, DEFAULT_SECRET, SIGNED_COOKIE_VALUE_PATTERN, buildFastify, DEFAULT_SESSION_ID } = require('./util') test('should set session cookie', async (t) => { t.plan(2) @@ -29,7 +29,7 @@ test('should set session cookie', async (t) => { }) t.assert.strictEqual(response.statusCode, 200) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly; Secure` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; Secure` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -136,7 +136,7 @@ test('should set session cookie with expires if maxAge', async (t) => { }) t.assert.strictEqual(response.statusCode, 200) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; Expires=[\w, :]{29}; HttpOnly; Secure` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; Expires=[\w, :]{29}; HttpOnly; Secure` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -158,7 +158,7 @@ test('should set session cookie with maxAge', async (t) => { }) t.assert.strictEqual(response.statusCode, 200) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Domain=localhost; Path=\/; HttpOnly; Secure` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Domain=localhost; Path=\/; HttpOnly; Secure` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -180,7 +180,7 @@ test('should set session cookie with sameSite', async (t) => { }) t.assert.strictEqual(response.statusCode, 200) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly; Secure; SameSite=Strict` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; Secure; SameSite=Strict` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -207,7 +207,7 @@ test('should set session another path in cookie', async (t) => { }) t.assert.strictEqual(response.statusCode, 200) - const pattern = String.raw`sessionId=[\w-]{32}.[[\w-%]{43,57}; Path=\/a\/test\/path; HttpOnly; Secure` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/a\/test\/path; HttpOnly; Secure` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -231,7 +231,7 @@ test('should set session cookie with expires', async (t) => { }) t.assert.strictEqual(response.statusCode, 200) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; Expires=Mon, 01 Feb 1971 00:01:01 GMT; HttpOnly; Secure` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; Expires=Mon, 01 Feb 1971 00:01:01 GMT; HttpOnly; Secure` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -253,7 +253,7 @@ test('should set session non HttpOnly cookie', async (t) => { }) t.assert.strictEqual(response.statusCode, 200) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; Secure` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; Secure` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -274,10 +274,53 @@ test('should set session non secure cookie', async (t) => { }) t.assert.strictEqual(response.statusCode, 200) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) +test('should use session cookie secure override when saving non secure cookie', async (t) => { + t.plan(3) + const options = { + secret: DEFAULT_SECRET, + cookie: { secure: true } + } + const fastify = await buildFastify((request, reply) => { + request.session.options({ secure: false }) + request.session.test = {} + reply.send(200) + }, options) + t.after(() => { fastify.close() }) + + const response = await fastify.inject({ + url: '/' + }) + + t.assert.strictEqual(response.statusCode, 200) + t.assert.strictEqual(typeof response.headers['set-cookie'], 'string') + t.assert.strictEqual(response.headers['set-cookie'].includes('Secure'), false) +}) + +test('should use session cookie secure override when saving secure cookie', async (t) => { + t.plan(2) + const options = { + secret: DEFAULT_SECRET, + cookie: { secure: false } + } + const fastify = await buildFastify((request, reply) => { + request.session.options({ secure: true }) + request.session.test = {} + reply.send(200) + }, options) + t.after(() => { fastify.close() }) + + const response = await fastify.inject({ + url: '/' + }) + + t.assert.strictEqual(response.statusCode, 200) + t.assert.strictEqual(response.headers['set-cookie'], undefined) +}) + test('should set session non secure cookie secureAuto', async (t) => { t.plan(2) const options = { @@ -295,7 +338,7 @@ test('should set session non secure cookie secureAuto', async (t) => { }) t.assert.strictEqual(response.statusCode, 200) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -322,7 +365,7 @@ test('should set session cookie secureAuto', async (t) => { }) t.assert.strictEqual(response.statusCode, 200) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly; SameSite=Lax` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; SameSite=Lax` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -349,7 +392,7 @@ test('should set session cookie secureAuto change SameSite', async (t) => { }) t.assert.strictEqual(response.statusCode, 200) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly; SameSite=Lax` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; SameSite=Lax` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -376,7 +419,7 @@ test('should set session cookie secureAuto keep SameSite when secured', async (t }) t.assert.strictEqual(response.statusCode, 200) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly; Secure; SameSite=None` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; Secure; SameSite=None` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -403,7 +446,7 @@ test('should set session secure cookie secureAuto http encrypted', async (t) => }) t.assert.strictEqual(response.statusCode, 200) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly; Secure` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; Secure` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -425,7 +468,7 @@ test('should set session secure cookie secureAuto x-forwarded-proto header', asy }) t.assert.strictEqual(response.statusCode, 200) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly; Secure` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; Secure` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -452,7 +495,7 @@ test('should set session partitioned cookie secure http encrypted', async (t) => }) t.assert.strictEqual(response.statusCode, 200) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly; Secure; Partitioned` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; Secure; Partitioned` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) @@ -478,9 +521,9 @@ test('should use maxAge instead of expires in session if both are set in options t.assert.strictEqual(response.statusCode, 200) // Expires attribute should be determined by options.maxAge -> Date.now() + 1000 and should have the same year from response.body, // and not determined by options.expires and should not have the year of 1971 - const pattern1 = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; Expires=\w+, \d+ \w+ 1971 \d{2}:\d{2}:\d{2} GMT; HttpOnly; Secure/` + const pattern1 = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; Expires=\w+, \d+ \w+ 1971 \d{2}:\d{2}:\d{2} GMT; HttpOnly; Secure/` t.assert.strictEqual(new RegExp(pattern1).exec(response.headers['set-cookie']), null) - const pattern2 = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; Expires=\w+, \d+ \w+ ${dateFromBody.getFullYear()} \d{2}:\d{2}:\d{2} GMT; HttpOnly; Secure` + const pattern2 = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; Expires=\w+, \d+ \w+ ${dateFromBody.getFullYear()} \d{2}:\d{2}:\d{2} GMT; HttpOnly; Secure` t.assert.strictEqual(new RegExp(pattern2).test(response.headers['set-cookie']), true) }) @@ -545,7 +588,7 @@ test('when cookie secure is set to false then store secure as false', async t => t.assert.strictEqual(response.statusCode, 200) t.assert.strictEqual(typeof response.headers['set-cookie'], 'string') - const pattern = String.raw`^sessionId=[\w-]{32}.[\w-%]{43,135}; Path=\/; HttpOnly$` + const pattern = String.raw`^sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly$` t.assert.strictEqual(new RegExp(pattern).test(response.headers['set-cookie']), true) }) diff --git a/test/store.test.js b/test/store.test.js index 6386c23..27737be 100644 --- a/test/store.test.js +++ b/test/store.test.js @@ -2,7 +2,7 @@ const test = require('node:test') const fastifyPlugin = require('fastify-plugin') -const { buildFastify, DEFAULT_OPTIONS, DEFAULT_COOKIE, DEFAULT_SECRET, DEFAULT_SESSION_ID } = require('./util') +const { buildFastify, DEFAULT_OPTIONS, DEFAULT_COOKIE, DEFAULT_SECRET, DEFAULT_SESSION_ID, SIGNED_COOKIE_VALUE_PATTERN } = require('./util') test('should decorate request with sessionStore', async (t) => { t.plan(2) @@ -64,7 +64,7 @@ test('should create new session if ENOENT error on store.get', async (t) => { }) t.assert.strictEqual(response.headers['set-cookie'].includes('AAzZgRQddT1TKLkT3OZcnPsDiLKgV1uM1XHy2bIyqIg'), false) - const pattern = String.raw`sessionId=[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly; Secure` + const pattern = String.raw`sessionId=${SIGNED_COOKIE_VALUE_PATTERN}; Path=\/; HttpOnly; Secure` t.assert.strictEqual(RegExp(pattern).test(response.headers['set-cookie']), true) t.assert.strictEqual(response.statusCode, 200) t.assert.strictEqual(response.cookies[0].name, 'sessionId') diff --git a/test/util.js b/test/util.js index 6b7fda2..f11c033 100644 --- a/test/util.js +++ b/test/util.js @@ -11,6 +11,7 @@ const DEFAULT_SESSION_ID = 'Qk_XT2K7-clT-x1tVvoY6tIQ83iP72KN' const DEFAULT_ENCRYPTED_SESSION_ID = `${DEFAULT_SESSION_ID}.B7fUDYXU9fXF9pNuL3qm4NVmSduLJ6kzCOPh5JhHGoE` const DEFAULT_COOKIE_VALUE = `sessionId=${DEFAULT_ENCRYPTED_SESSION_ID};` const DEFAULT_COOKIE = `${DEFAULT_COOKIE_VALUE}; Path=/; HttpOnly; Secure` +const SIGNED_COOKIE_VALUE_PATTERN = String.raw`[\w-]{32}\.[^;]+` async function buildFastify (handler, sessionOptions, plugin) { const fastify = Fastify({ trustProxy: true }) @@ -32,5 +33,6 @@ module.exports = { DEFAULT_SESSION_ID, DEFAULT_ENCRYPTED_SESSION_ID, DEFAULT_COOKIE_VALUE, - DEFAULT_COOKIE + DEFAULT_COOKIE, + SIGNED_COOKIE_VALUE_PATTERN } From cad6ecf4d11437a8ba89c936ed77867d3f0c670e Mon Sep 17 00:00:00 2001 From: Tony133 Date: Sun, 12 Jul 2026 22:34:42 +0200 Subject: [PATCH 03/10] fix: use named import for connect-redis v8.x --- benchmark/bench.js | 2 +- examples/redis.js | 2 +- package.json | 2 +- types/index.tst.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/benchmark/bench.js b/benchmark/bench.js index 364a603..af169f3 100644 --- a/benchmark/bench.js +++ b/benchmark/bench.js @@ -1,6 +1,6 @@ 'use strict' -const RedisStore = require('connect-redis').default +const { RedisStore } = require('connect-redis') const Fastify = require('fastify') const Redis = require('ioredis') const fileStoreFactory = require('session-file-store') diff --git a/examples/redis.js b/examples/redis.js index e3355e2..a0fde54 100644 --- a/examples/redis.js +++ b/examples/redis.js @@ -4,7 +4,7 @@ const Fastify = require('fastify') const fastifySession = require('..') const fastifyCookie = require('@fastify/cookie') const Redis = require('ioredis') -const RedisStore = require('connect-redis').default +const { RedisStore } = require('connect-redis') const fastify = Fastify() diff --git a/package.json b/package.json index a7bbd3f..7c21ff2 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@types/node": "^26.0.1", "c8": "^11.0.0", "connect-mongo": "^6.0.0", - "connect-redis": "^7.1.1", + "connect-redis": "^8.0.0", "cronometro": "^6.0.3", "eslint": "^9.17.0", "fastify": "^5.0.0", diff --git a/types/index.tst.ts b/types/index.tst.ts index 593601b..f082276 100644 --- a/types/index.tst.ts +++ b/types/index.tst.ts @@ -1,5 +1,5 @@ import MongoStore from 'connect-mongo' -import RedisStore from 'connect-redis' +import { RedisStore } from 'connect-redis' import fastify, { type FastifyInstance, type FastifyReply, From c6ff3adbc5d92d1c587533f1306ba4356460e985 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Tue, 14 Jul 2026 03:18:44 +0800 Subject: [PATCH 04/10] fix: clear session cookie with configured path (#344) --- index.js | 2 +- test/session.test.js | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 25f60bc..1ec75f8 100644 --- a/index.js +++ b/index.js @@ -171,7 +171,7 @@ function fastifySession (fastify, options, next) { if (!saveSession || isInsecureConnection) { // if a session cookie is set, but has a different ID, clear it if (cookieSessionId && cookieSessionId !== session.encryptedSessionId) { - reply.clearCookie(cookieName, { domain: cookieOpts.domain }) + reply.clearCookie(cookieName, { domain: cookieOpts.domain, path: cookieOpts.path || '/' }) } if (session.isSaved()) { diff --git a/test/session.test.js b/test/session.test.js index 1358fd1..db8e7ba 100644 --- a/test/session.test.js +++ b/test/session.test.js @@ -882,6 +882,29 @@ test("clearing cookie sets the domain if it's specified in the cookie options", t.assert.strictEqual(response.headers['set-cookie'], 'sessionId=; Max-Age=0; Domain=domain.test; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax') }) +test("clearing cookie sets the path if it's specified in the cookie options", async t => { + t.plan(2) + const fastify = Fastify({ trustProxy: true }) + await fastify.register(fastifyCookie) + await fastify.register(fastifySession, { + ...DEFAULT_OPTIONS, + cookie: { path: '/admin' } + }) + fastify.get('/admin', (_request, reply) => { + reply.send(200) + }) + await fastify.listen({ port: 0 }) + t.after(() => fastify.close()) + + const response = await fastify.inject({ + url: '/admin', + headers: { cookie: DEFAULT_COOKIE_VALUE } + }) + + t.assert.strictEqual(response.statusCode, 200) + t.assert.strictEqual(response.headers['set-cookie'], 'sessionId=; Max-Age=0; Path=/admin; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax') +}) + test('does not clear cookie if no session cookie in request', async t => { t.plan(2) const fastify = await buildFastify((_request, reply) => { From e8f0d9357364cf2c5db5fa3a186225fce9110c0a Mon Sep 17 00:00:00 2001 From: Tony133 Date: Wed, 15 Jul 2026 23:38:05 +0200 Subject: [PATCH 05/10] Bumped v11.1.2 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7c21ff2..4941420 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fastify/session", - "version": "11.1.1", + "version": "11.1.2", "description": "a session plugin for fastify", "main": "index.js", "type": "commonjs", @@ -80,4 +80,4 @@ "index.js", "types/index.d.ts" ] -} \ No newline at end of file +} From d5cdcf0090f67ea7473fc4a5d5af72e3d1eca2fe Mon Sep 17 00:00:00 2001 From: Frazer Smith Date: Mon, 20 Jul 2026 06:56:20 +0100 Subject: [PATCH 06/10] ci: pin actions to commit-hash --- .github/workflows/ci.yml | 2 +- .github/workflows/lock-threads.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a68582..998d68f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: permissions: contents: write pull-requests: write - uses: fastify/workflows/.github/workflows/plugins-ci.yml@v6 + uses: fastify/workflows/.github/workflows/plugins-ci.yml@2073dc8e1f9e172bf42daa3843c9dbd31af1e8cb # v6.0.0 with: license-check: true lint: true diff --git a/.github/workflows/lock-threads.yml b/.github/workflows/lock-threads.yml index bd263f2..3c10b10 100644 --- a/.github/workflows/lock-threads.yml +++ b/.github/workflows/lock-threads.yml @@ -16,4 +16,4 @@ jobs: permissions: issues: write pull-requests: write - uses: fastify/workflows/.github/workflows/lock-threads.yml@v6 + uses: fastify/workflows/.github/workflows/lock-threads.yml@2073dc8e1f9e172bf42daa3843c9dbd31af1e8cb # v6.0.0 From 9abea94dd2bda8048e9b759a68c97cbae5b6bf5e Mon Sep 17 00:00:00 2001 From: Mark Xian Date: Wed, 22 Jul 2026 16:32:49 +0800 Subject: [PATCH 07/10] fix: destroy the previous session on regenerate (#348) --- index.js | 10 ++-------- lib/session.js | 29 ++++++++++++++++++----------- test/session.test.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 19 deletions(-) diff --git a/index.js b/index.js index 1ec75f8..0879818 100644 --- a/index.js +++ b/index.js @@ -95,14 +95,8 @@ function fastifySession (fastify, options, next) { const expiration = restoredSession.cookie.originalExpires || restoredSession.cookie.expires if (expiration && expiration.getTime() <= Date.now()) { - restoredSession.destroy(err => { - if (err) { - done(err) - return - } - - restoredSession.regenerate(done) - }) + // `regenerate` destroys the expired session before issuing a new one. + restoredSession.regenerate(done) return } diff --git a/lib/session.js b/lib/session.js index 953d402..77e3a6e 100644 --- a/lib/session.js +++ b/lib/session.js @@ -111,22 +111,29 @@ module.exports = class Session { } } + const oldSessionId = this[sessionIdKey] + if (callback) { - this[sessionStoreKey].set(session.sessionId, session, error => { - this[requestKey].session = session + this[sessionStoreKey].destroy(oldSessionId, destroyError => { + this[sessionStoreKey].set(session.sessionId, session, setError => { + this[requestKey].session = session - callback(error) + callback(destroyError || setError) + }) }) } else { return new Promise((resolve, reject) => { - this[sessionStoreKey].set(session.sessionId, session, error => { - this[requestKey].session = session - - if (error) { - reject(error) - } else { - resolve() - } + this[sessionStoreKey].destroy(oldSessionId, destroyError => { + this[sessionStoreKey].set(session.sessionId, session, setError => { + this[requestKey].session = session + + const error = destroyError || setError + if (error) { + reject(error) + } else { + resolve() + } + }) }) }) } diff --git a/test/session.test.js b/test/session.test.js index db8e7ba..f93cdfa 100644 --- a/test/session.test.js +++ b/test/session.test.js @@ -761,6 +761,49 @@ test('regenerate supports rejecting promises', async t => { t.assert.strictEqual(response.statusCode, 200) }) +test('regenerate destroys the previous session in the store', async (t) => { + t.plan(6) + const { MemoryStore } = require('../lib/store') + const store = new MemoryStore() + + let oldSessionId + let newSessionId + + const fastify = await buildFastify((request, reply) => { + if (request.session.get('userId')) { + // Second request: a session already exists in the store, regenerate it. + oldSessionId = request.session.sessionId + request.session.regenerate(error => { + if (error) { + reply.status(500).send('Error ' + error) + return + } + newSessionId = request.session.sessionId + reply.send(200) + }) + } else { + // First request: create and persist a session. + request.session.set('userId', 42) + reply.send(200) + } + }, { ...DEFAULT_OPTIONS, cookie: { secure: false }, store }) + t.after(() => fastify.close()) + + const response1 = await fastify.inject({ url: '/' }) + t.assert.strictEqual(response1.statusCode, 200) + t.assert.strictEqual(store.store.size, 1) + + const response2 = await fastify.inject({ + url: '/', + headers: { Cookie: response1.headers['set-cookie'] } + }) + t.assert.strictEqual(response2.statusCode, 200) + + t.assert.notStrictEqual(newSessionId, oldSessionId) + t.assert.strictEqual(store.store.has(oldSessionId), false) + t.assert.strictEqual(store.store.has(newSessionId), true) +}) + test('reload supports promises', async t => { t.plan(2) const fastify = await buildFastify(async (request, reply) => { From 4f7c3f28f267ad6e1f1a2ace5df89101b5ddb08c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 04:54:48 +0000 Subject: [PATCH 08/10] chore: bump c8 from 11.0.0 to 12.0.0 (#352) Bumps [c8](https://github.com/bcoe/c8) from 11.0.0 to 12.0.0. - [Release notes](https://github.com/bcoe/c8/releases) - [Changelog](https://github.com/bcoe/c8/blob/main/CHANGELOG.md) - [Commits](https://github.com/bcoe/c8/compare/v11.0.0...v12.0.0) --- updated-dependencies: - dependency-name: c8 dependency-version: 12.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4941420..11b2c1b 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "devDependencies": { "@fastify/cookie": "^11.0.0", "@types/node": "^26.0.1", - "c8": "^11.0.0", + "c8": "^12.0.0", "connect-mongo": "^6.0.0", "connect-redis": "^8.0.0", "cronometro": "^6.0.3", From a550470e6552b1ac1e85c7a4599e0ca8ce97c719 Mon Sep 17 00:00:00 2001 From: Antonio Tripodi Date: Sun, 26 Jul 2026 10:36:06 +0200 Subject: [PATCH 09/10] chore: upgrade connect-redis v9.x (#347) --- benchmark/bench.js | 11 ++++++-- examples/redis.js | 13 ++++----- package.json | 5 ++-- types/index.tst.ts | 70 +++++++++++++++++++++++++++++++--------------- 4 files changed, 65 insertions(+), 34 deletions(-) diff --git a/benchmark/bench.js b/benchmark/bench.js index af169f3..404392c 100644 --- a/benchmark/bench.js +++ b/benchmark/bench.js @@ -2,7 +2,7 @@ const { RedisStore } = require('connect-redis') const Fastify = require('fastify') -const Redis = require('ioredis') +const { createClient } = require('redis') const fileStoreFactory = require('session-file-store') const { isMainThread } = require('node:worker_threads') @@ -10,6 +10,7 @@ const fastifySession = require('..') const fastifyCookie = require('@fastify/cookie') let redisClient +let redisClientConnecting function createServer (sessionPlugin, cookiePlugin, storeType) { let requestCounter = 0 @@ -17,7 +18,8 @@ function createServer (sessionPlugin, cookiePlugin, storeType) { if (storeType === 'redis') { if (!redisClient) { - redisClient = new Redis() + redisClient = createClient() + redisClientConnecting = redisClient.connect() } store = new RedisStore({ client: redisClient }) } else if (storeType === 'file') { @@ -55,6 +57,11 @@ function testFunction (sessionPlugin, cookiePlugin, storeType) { const server = createServer(sessionPlugin, cookiePlugin, storeType) return async function () { + if (redisClientConnecting) { + await redisClientConnecting + redisClientConnecting = null + } + const { headers } = await server.inject('/') const setCookieHeader = headers['set-cookie'] diff --git a/examples/redis.js b/examples/redis.js index a0fde54..e7d5045 100644 --- a/examples/redis.js +++ b/examples/redis.js @@ -3,15 +3,16 @@ const Fastify = require('fastify') const fastifySession = require('..') const fastifyCookie = require('@fastify/cookie') -const Redis = require('ioredis') +const { createClient } = require('redis') const { RedisStore } = require('connect-redis') const fastify = Fastify() +const redisClient = createClient() +redisClient.connect().catch(console.error) + const store = new RedisStore({ - client: new Redis({ - enableAutoPipelining: true - }) + client: redisClient }) fastify.register(fastifyCookie, {}) @@ -27,8 +28,6 @@ fastify.get('/', (request, reply) => { }) const response = fastify.inject('/') -response.then(v => console.log(` - -autocannon -p 10 -H "Cookie=${decodeURIComponent(v.headers['set-cookie'])}" http://127.0.0.1:3000`)) +response.then(v => console.log(`\n\nautocannon -p 10 -H "Cookie=${decodeURIComponent(v.headers['set-cookie'])}" http://127.0.0.1:3000`)) fastify.listen({ port: 3000 }) diff --git a/package.json b/package.json index 11b2c1b..63c72bf 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "test": "npm run test:unit && npm run test:typescript", "test:unit": "c8 --100 node --test", "test:typescript": "tstyche", + "redis": "docker run -p 6379:6379 --rm redis", "benchmark": "node benchmark/bench.js", "lint": "eslint", "lint:fix": "eslint --fix" @@ -65,12 +66,12 @@ "@types/node": "^26.0.1", "c8": "^12.0.0", "connect-mongo": "^6.0.0", - "connect-redis": "^8.0.0", + "connect-redis": "^9.0.0", "cronometro": "^6.0.3", "eslint": "^9.17.0", "fastify": "^5.0.0", - "ioredis": "^5.3.2", "neostandard": "^0.13.0", + "redis": "^6.0.0", "session-file-store": "^1.5.0", "tstyche": "^7.0.0" }, diff --git a/types/index.tst.ts b/types/index.tst.ts index f082276..8312765 100644 --- a/types/index.tst.ts +++ b/types/index.tst.ts @@ -6,9 +6,13 @@ import fastify, { type FastifyRequest, type Session } from 'fastify' -import Redis from 'ioredis' +import { createClient } from 'redis' import { expect } from 'tstyche' -import fastifySession, { type CookieOptions, MemoryStore, type SessionStore } from '..' +import fastifySession, { + type CookieOptions, + MemoryStore, + type SessionStore +} from '..' const plugin = fastifySession @@ -25,7 +29,7 @@ declare module 'fastify' { user?: { id: number; }; - foo: string + foo: string; } } @@ -62,7 +66,7 @@ app.register(plugin, { }) app.register(plugin, { secret, - store: new RedisStore({ client: new Redis() }) + store: new RedisStore({ client: createClient() }) }) app.register(plugin, { secret, @@ -77,19 +81,29 @@ app.register(plugin, { idGenerator: () => Date.now() + '' }) app.register(plugin, { - secret, + secret }) app.register(plugin, { secret, - idGenerator: (request) => `${request === undefined ? 'null' : request.ip}-${Date.now()}` + idGenerator: (request) => + `${request === undefined ? 'null' : request.ip}-${Date.now()}` }) expect(app.register).type.not.toBeCallableWith(plugin) expect(app.register).type.not.toBeCallableWith(plugin, {}) expect(app.decryptSession).type.not.toBeInstantiableWith<[string]>() -app.decryptSession<{ hello: 'world' }>('sessionId', { hello: 'world' }, () => ({})) -app.decryptSession<{ hello: 'world' }>('sessionId', { hello: 'world' }, { domain: '/' }, () => ({})) +app.decryptSession<{ hello: 'world' }>( + 'sessionId', + { hello: 'world' }, + () => ({}) +) +app.decryptSession<{ hello: 'world' }>( + 'sessionId', + { hello: 'world' }, + { domain: '/' }, + () => ({}) +) app.decryptSession('sessionId', {}, () => ({})) app.decryptSession('sessionId', {}, { domain: '/' }, () => ({})) @@ -136,21 +150,25 @@ app.route({ expect(request.session.regenerate(['foo'])).type.toBe>() expect(request.session.save()).type.toBe>() - expect(request.session.options).type.not.toBeCallableWith({ keyNotInCookieOptions: true }) + expect(request.session.options).type.not.toBeCallableWith({ + keyNotInCookieOptions: true + }) expect(request.session.options).type.not.toBeCallableWith({ signed: true }) expect(request.session.options({})).type.toBe() - expect(request.session.options({ - domain: 'example.com', - expires: new Date(), - httpOnly: true, - maxAge: 1000, - partitioned: true, - path: '/', - sameSite: 'lax', - priority: 'low', - secure: 'auto' - })).type.toBe() + expect( + request.session.options({ + domain: 'example.com', + expires: new Date(), + httpOnly: true, + maxAge: 1000, + partitioned: true, + path: '/', + sameSite: 'lax', + priority: 'low', + secure: 'auto' + }) + ).type.toBe() } }) @@ -169,8 +187,12 @@ const app2 = fastify() app2.register(fastifySession, { secret: 'DizIzSecret' }) app2.get('/', async function (request) { - expect(request.session.get('foo')).type.toBeAssignableTo() - expect(request.session.get('foo')).type.not.toBeAssignableTo() + expect(request.session.get('foo')).type.toBeAssignableTo< + string | undefined + >() + expect(request.session.get('foo')).type.not.toBeAssignableTo< + number | undefined + >() expect(request.session.set('foo', 'bar')).type.toBe() @@ -183,5 +205,7 @@ app2.get('/', async function (request) { expect(request.session.set).type.not.toBeCallableWith('not exist', 'abc') expect(request.session.get('not exist')).type.toBe() - expect(request.session.set('not exist', 'abc')).type.toBeAssignableTo() + expect( + request.session.set('not exist', 'abc') + ).type.toBeAssignableTo() }) From ee47908a1d2b107eab1cc0f5147544eff3dd2dda Mon Sep 17 00:00:00 2001 From: Hashim Khan <64767361+Hashim1999164@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:41:49 +0500 Subject: [PATCH 10/10] fix(types): document cookie secure and httpOnly defaults as true (#353) --- types/index.d.ts | 13 ++++++++++++- types/index.tst.ts | 6 ++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 35729f0..043909e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -180,9 +180,20 @@ declare namespace fastifySession { cookiePrefix?: string; } - export interface CookieOptions extends Omit { + export interface CookieOptions extends Omit { /** A `number` in milliseconds that specifies the `Expires` attribute by adding the specified milliseconds to the current date. If both `expires` and `maxAge` are set, then `expires` is used. */ maxAge?: number; + /** + * The `boolean` value of the `HttpOnly` attribute. + * @default true + */ + httpOnly?: boolean; + /** + * The `boolean` value of the `Secure` attribute. Set this option to false when communicating over an unencrypted (HTTP) connection. + * Value can be set to `auto`; in that case the `Secure` attribute is false for HTTP and true for HTTPS. + * @default true + */ + secure?: boolean | 'auto'; } export class MemoryStore implements fastifySession.SessionStore { diff --git a/types/index.tst.ts b/types/index.tst.ts index 8312765..920d42a 100644 --- a/types/index.tst.ts +++ b/types/index.tst.ts @@ -56,9 +56,11 @@ app.register(plugin, { } }) -const cookieMaxAge: CookieOptions = {} +const cookieOptions: CookieOptions = {} -expect(cookieMaxAge.maxAge).type.toBe() +expect(cookieOptions.maxAge).type.toBe() +expect(cookieOptions.httpOnly).type.toBe() +expect(cookieOptions.secure).type.toBe() app.register(plugin, { secret,