diff --git a/README.md b/README.md index c1376c0..5c8c99c 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ Note that the rest of the application may manipulate the array during its life c This can be used to rotate the signing secret at regular intervals. A secret should remain somewhere in the array as long as there are active sessions with cookies signed by it. Secrets management is left up to the rest of the application. ##### cookieName (optional) The name of the session cookie. Defaults to `sessionId`. +##### cookiePrefix (optional) +Prefix for the value of the cookie. This is useful for compatibility with `express-session`, which prefixes all cookies with `"s:"`. Defaults to `""`. ##### cookie The options object is used to generate the `Set-Cookie` header of the session cookie. May have the following properties: * `path` - The `Path` attribute. Defaults to `/` (the root path). diff --git a/lib/fastifySession.js b/lib/fastifySession.js index cf93989..21361d0 100644 --- a/lib/fastifySession.js +++ b/lib/fastifySession.js @@ -106,7 +106,12 @@ function decryptSession (sessionId, options, request, done) { function onRequest (options) { const unsignSignedCookie = options.unsignSignedCookie const cookieOpts = options.cookie + const cookieName = options.cookieName const idGenerator = options.idGenerator + const cookiePrefix = options.cookiePrefix + const hasCookiePrefix = typeof cookiePrefix === 'string' && cookiePrefix.length !== 0 + const cookiePrefixLength = hasCookiePrefix && cookiePrefix.length + return function handleSession (request, reply, done) { request.session = {} @@ -115,7 +120,10 @@ function onRequest (options) { done() return } - const sessionId = request.cookies[options.cookieName] + let sessionId = request.cookies[cookieName] + if (sessionId && hasCookiePrefix && sessionId.startsWith(cookiePrefix)) { + sessionId = sessionId.slice(cookiePrefixLength) + } const secret = options.secret[0] if (!sessionId) { newSession(secret, request, cookieOpts, idGenerator, done) @@ -135,6 +143,12 @@ function onRequest (options) { } function onSend (options) { + const cookieOpts = options.cookie + const cookieName = options.cookieName + const cookiePrefix = options.cookiePrefix + const saveUninitialized = options.saveUninitialized + const hasCookiePrefix = typeof cookiePrefix === 'string' && cookiePrefix.length !== 0 + return function saveSession (request, reply, payload, done) { const session = request.session if (!session || !session.sessionId) { @@ -142,10 +156,15 @@ function onSend (options) { return } - if (!shouldSaveSession(request, options.cookie, options.saveUninitialized)) { + let encryptedSessionId = session.encryptedSessionId + if (encryptedSessionId && hasCookiePrefix) { + encryptedSessionId = `${cookiePrefix}${encryptedSessionId}` + } + + if (!shouldSaveSession(request, cookieOpts, saveUninitialized)) { // if a session cookie is set, but has a different ID, clear it - if (request.cookies[options.cookieName] && request.cookies[options.cookieName] !== session.encryptedSessionId) { - reply.clearCookie(options.cookieName) + if (request.cookies[cookieName] && request.cookies[cookieName] !== encryptedSessionId) { + reply.clearCookie(cookieName) } done() return @@ -156,8 +175,8 @@ function onSend (options) { return } reply.setCookie( - options.cookieName, - session.encryptedSessionId, + cookieName, + encryptedSessionId, session.cookie.options(isConnectionSecure(request)) ) done() @@ -196,6 +215,7 @@ function ensureDefaults (options) { options.rolling = option(options, 'rolling', true) options.saveUninitialized = option(options, 'saveUninitialized', true) options.secret = Array.isArray(options.secret) ? options.secret : [options.secret] + options.cookiePrefix = option(options, 'cookiePrefix', '') return options } diff --git a/test/base.test.js b/test/base.test.js index aba1323..d0803b0 100644 --- a/test/base.test.js +++ b/test/base.test.js @@ -127,6 +127,40 @@ test('should set session cookie using the default cookie name', async (t) => { t.match(response.headers['set-cookie'], /sessionId=undefined; Path=\/; HttpOnly; Secure/) }) +test('should set express sessions using the specified cookiePrefix', async (t) => { + t.plan(2) + const options = { + secret: 'cNaoPYAwF60HZJzkcNaoPYAwF60HZJzk', + cookieName: 'connect.sid', + cookiePrefix: 's:' + } + + const plugin = fastifyPlugin(async (fastify, opts) => { + fastify.addHook('onRequest', (request, reply, done) => { + request.sessionStore.set('Qk_XT2K7-clT-x1tVvoY6tIQ83iP72KN', { + expires: Date.now() + 1000 + }, done) + }) + }) + function handler (request, reply) { + request.session.test = {} + reply.send(200) + } + const fastify = await buildFastify(handler, options, plugin) + t.teardown(() => fastify.close()) + + const response = await fastify.inject({ + url: '/', + headers: { + cookie: 'connect.sid=s%3AQk_XT2K7-clT-x1tVvoY6tIQ83iP72KN.B7fUDYXU9fXF9pNuL3qm4NVmSduLJ6kzCOPh5JhHGoE; Path=/; HttpOnly; Secure', + 'x-forwarded-proto': 'https' + } + }) + + t.equal(response.statusCode, 200) + t.match(response.headers['set-cookie'], /connect.sid=s%3A[\w-]{32}.[\w-%]{43,57}; Path=\/; HttpOnly; Secure/) +}) + test('should create new session on expired session', async (t) => { t.plan(2) const plugin = fastifyPlugin(async (fastify, opts) => { diff --git a/types/types.d.ts b/types/types.d.ts index 3e7ee51..51b96f4 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -125,6 +125,12 @@ declare namespace FastifySessionPlugin { /** Function used to generate new session IDs. Defaults to uid(24). */ idGenerator?(request?: Fastify.FastifyRequest): string; + + /** + * Prefixes all cookie values. Run with "s:" to be be compatible with express-session. + * Defaults to "" + */ + cookiePrefix?: string; } interface CookieOptions {