diff --git a/README.md b/README.md index f8dd1d2..adea017 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,29 @@ idGenerator: (request) => { } ``` +##### idStore +A session id store. Used to store and retrieve session ids. +* get(request, key): string | undefined +* set(reply, key, value, cookieOptions) +* clear(reply, key, cookieOptions) + +Defaults to a cookie store. + +Custom implementation example: +```js +idStore: { + get: (request, key) => request.headers[key.toLowerCase()], + set: (reply, key, value) => { + reply.header(key, value); + }, + clear: (reply, key) => { + reply.removeHeader(key); + }, + }, +} +``` + + #### request.session Allows to access or modify the session data. diff --git a/lib/fastifySession.js b/lib/fastifySession.js index 56aa6f4..f288aab 100644 --- a/lib/fastifySession.js +++ b/lib/fastifySession.js @@ -4,6 +4,7 @@ const fp = require('fastify-plugin') const idGenerator = require('./idGenerator')() const Store = require('./store') const Session = require('./session') +const idStore = require('./idStore') function fastifySession (fastify, options, next) { const error = checkOptions(options) @@ -14,6 +15,7 @@ function fastifySession (fastify, options, next) { options = ensureDefaults(options) const sessionStore = options.store + const sessionIdStore = options.idStore const cookieSigner = options.signer const cookieName = options.cookieName const cookiePrefix = options.cookiePrefix @@ -113,14 +115,14 @@ function fastifySession (fastify, options, next) { const getCookieSessionId = hasCookiePrefix ? function getCookieSessionId (request) { - const cookieValue = request.cookies[cookieName] + const cookieValue = sessionIdStore.get(request, cookieName) return ( cookieValue?.startsWith(cookiePrefix) && cookieValue.slice(cookiePrefixLength) ) } : function getCookieSessionId (request) { - return request.cookies[cookieName] + return sessionIdStore.get(request, cookieName) } function onRequest (options) { @@ -171,11 +173,12 @@ 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 }) + sessionIdStore.clear?.(reply, cookieName, { domain: cookieOpts.domain }) } if (session.isSaved()) { - reply.setCookie( + sessionIdStore.set?.( + reply, cookieName, sessionIdWithPrefix, // we need to remove extra properties to align the same with `express-session` @@ -192,7 +195,8 @@ function fastifySession (fastify, options, next) { done(err) return } - reply.setCookie( + sessionIdStore.set?.( + reply, cookieName, sessionIdWithPrefix, // we need to remove extra properties to align the same with `express-session` @@ -220,6 +224,7 @@ function fastifySession (fastify, options, next) { function ensureDefaults (options) { const opts = {} opts.store = options.store || new Store() + opts.idStore = options.idStore || idStore opts.idGenerator = options.idGenerator || idGenerator opts.cookieName = options.cookieName || 'sessionId' opts.cookie = options.cookie || {} diff --git a/lib/idStore.js b/lib/idStore.js new file mode 100644 index 0000000..f975bcf --- /dev/null +++ b/lib/idStore.js @@ -0,0 +1,5 @@ +module.exports = { + get: (request, key) => request.cookies[key], + set: (reply, key, value, opts) => reply.setCookie(key, value, opts), + clear: (reply, key, opts) => reply.clearCookie(key, opts) +} diff --git a/test/customIdStore.test.js b/test/customIdStore.test.js new file mode 100644 index 0000000..87f43aa --- /dev/null +++ b/test/customIdStore.test.js @@ -0,0 +1,73 @@ +'use strict' + +const test = require('node:test') +const Fastify = require('fastify') +const fastifyCookie = require('@fastify/cookie') +const fastifySession = require('../lib/fastifySession') +const { DEFAULT_OPTIONS, DEFAULT_SESSION_ID, DEFAULT_ENCRYPTED_SESSION_ID } = require('./util') + +const idStore = { + get: (request, key) => request.headers[key.toLowerCase()], + set: (reply, key, value) => reply.header(key, value), + clear: (reply, key) => reply.removeHeader(key) +} + +test('should set sessionid header with custom id store', async (t) => { + t.plan(3) + const fastify = Fastify() + let sessionId = null + + fastify.addHook('onRequest', async (request) => { + request.raw.socket.encrypted = true + }) + fastify.register(fastifyCookie) + fastify.register(fastifySession, { + ...DEFAULT_OPTIONS, + idStore + }) + fastify.get('/', (request, reply) => { + request.session.test = {} + sessionId = request.session.sessionId + reply.send(200) + }) + await fastify.listen({ port: 0 }) + t.after(() => { fastify.close() }) + + const response = await fastify.inject({ + url: '/' + }) + + t.assert.strictEqual(response.statusCode, 200) + t.assert.ok(sessionId) + const pattern = `${sessionId}\\..{43,57}` + t.assert.strictEqual(new RegExp(pattern).test(response.headers['sessionid']), true) +}) + +test('should retrieve sessionid header with custom id store', async (t) => { + t.plan(2) + const fastify = Fastify() + + fastify.register(fastifyCookie) + fastify.register(fastifySession, { + ...DEFAULT_OPTIONS, + idStore, + store: { + get (id, cb) { cb(null, { id }) }, + } + }) + fastify.get('/', (request, reply) => { + t.assert.strictEqual(request.session.sessionId, DEFAULT_SESSION_ID) + reply.send(200) + }) + await fastify.listen({ port: 0 }) + t.after(() => { fastify.close() }) + + const response = await fastify.inject({ + url: '/', + headers: { + sessionid: DEFAULT_ENCRYPTED_SESSION_ID + } + }) + + t.assert.strictEqual(response.statusCode, 200) +}) diff --git a/types/types.d.ts b/types/types.d.ts index e0dcd0d..caecc8c 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -155,6 +155,12 @@ declare namespace fastifySession { */ store?: fastifySession.SessionStore; + /** + * A session ID store. + * Defaults to a cookie based store. + */ + idStore?: SessionIdStore; + /** * Save sessions to the store, even when they are new and not modified. * Defaults to true. Setting this to false can be useful to save storage space and to comply with the EU cookie law. @@ -182,6 +188,12 @@ declare namespace fastifySession { maxAge?: number; } + export interface SessionIdStore { + get: (request: Fastify.FastifyRequest, key: string) => string | undefined; + set?: (reply: Fastify.FastifyReply, key: string, value: string, cookieOptions: CookieOptions) => void; + clear?: (reply: Fastify.FastifyReply, key: string, cookieOptions: CookieOptions) => void; + } + export class MemoryStore implements fastifySession.SessionStore { constructor (map?: Map) set (