diff --git a/README.md b/README.md index a42b0b8..07e09b8 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ Allows to destroy the session in the store. If you do not pass a callback, a Pro #### Session#touch() -Updates the `expires` property of the session. +Updates the `expires` property of the session's cookie. #### Session#regenerate(callback) diff --git a/lib/fastifySession.js b/lib/fastifySession.js index b158eb8..308736a 100644 --- a/lib/fastifySession.js +++ b/lib/fastifySession.js @@ -63,13 +63,14 @@ function decryptSession (sessionId, options, request, done) { newSession(secret, request, cookieOpts, idGenerator, done) return } - if (session && session.expires && session.expires <= Date.now()) { + if (session && session.cookie && session.cookie.expires && session.cookie.expires <= Date.now()) { const restoredSession = Session.restore( request, idGenerator, cookieOpts, secret, - session + session, + decryptedSessionId ) restoredSession.destroy(err => { @@ -88,7 +89,8 @@ function decryptSession (sessionId, options, request, done) { idGenerator, cookieOpts, secret, - session + session, + decryptedSessionId ) } else { request.session = Session.restore( @@ -96,7 +98,8 @@ function decryptSession (sessionId, options, request, done) { idGenerator, cookieOpts, secret, - session + session, + decryptedSessionId ) } done() diff --git a/lib/session.js b/lib/session.js index 59eca9c..dd18f43 100644 --- a/lib/session.js +++ b/lib/session.js @@ -10,18 +10,18 @@ const stringify = configureStringifier({ bigint: false }) const maxAge = Symbol('maxAge') const secretKey = Symbol('secretKey') -const sign = Symbol('sign') const addDataToSession = Symbol('addDataToSession') const generateId = Symbol('generateId') const requestKey = Symbol('request') const cookieOptsKey = Symbol('cookieOpts') const originalHash = Symbol('originalHash') const hash = Symbol('hash') +const sessionIdKey = Symbol('sessionId') +const encryptedSessionIdKey = Symbol('encryptedSessionId') module.exports = class Session { - constructor (request, idGenerator, cookieOpts, secret, prevSession = {}) { + constructor (request, idGenerator, cookieOpts, secret, prevSession = {}, sessionId = idGenerator(request)) { this[generateId] = idGenerator - this.expires = null this.cookie = new Cookie(cookieOpts) this[cookieOptsKey] = cookieOpts this[maxAge] = cookieOpts.maxAge @@ -29,17 +29,14 @@ module.exports = class Session { this[addDataToSession](prevSession) this[requestKey] = request this.touch() - if (!this.sessionId) { - this.sessionId = this[generateId](this[requestKey]) - this.encryptedSessionId = this[sign]() - } + this[sessionIdKey] = sessionId + this[encryptedSessionIdKey] = cookieSignature.sign(sessionId, secret) this[originalHash] = this[hash]() } touch () { if (this[maxAge]) { - this.expires = new Date(Date.now() + this[maxAge]) - this.cookie.expires = this.expires + this.cookie.expires = new Date(Date.now() + this[maxAge]) } } @@ -71,7 +68,7 @@ module.exports = class Session { [addDataToSession] (prevSession) { for (const key in prevSession) { - if (!['expires', 'cookie'].includes(key)) { + if (!['cookie', 'sessionId', 'encryptedSessionId'].includes(key)) { this[key] = prevSession[key] } } @@ -87,14 +84,14 @@ module.exports = class Session { destroy (callback) { if (callback) { - this[requestKey].sessionStore.destroy(this.sessionId, error => { + this[requestKey].sessionStore.destroy(this[sessionIdKey], error => { this[requestKey].session = null callback(error) }) } else { return new Promise((resolve, reject) => { - this[requestKey].sessionStore.destroy(this.sessionId, error => { + this[requestKey].sessionStore.destroy(this[sessionIdKey], error => { this[requestKey].session = null if (error) { @@ -109,15 +106,15 @@ module.exports = class Session { reload (callback) { if (callback) { - this[requestKey].sessionStore.get(this.sessionId, (error, session) => { - this[requestKey].session = new Session(this[requestKey], this[generateId], this[cookieOptsKey], this[secretKey], session) + this[requestKey].sessionStore.get(this[sessionIdKey], (error, session) => { + this[requestKey].session = new Session(this[requestKey], this[generateId], this[cookieOptsKey], this[secretKey], session, this[sessionIdKey]) callback(error) }) } else { return new Promise((resolve, reject) => { - this[requestKey].sessionStore.get(this.sessionId, (error, session) => { - this[requestKey].session = new Session(this[requestKey], this[generateId], this[cookieOptsKey], this[secretKey], session) + this[requestKey].sessionStore.get(this[sessionIdKey], (error, session) => { + this[requestKey].session = new Session(this[requestKey], this[generateId], this[cookieOptsKey], this[secretKey], session, this[sessionIdKey]) if (error) { reject(error) @@ -131,12 +128,12 @@ module.exports = class Session { save (callback) { if (callback) { - this[requestKey].sessionStore.set(this.sessionId, this, error => { + this[requestKey].sessionStore.set(this[sessionIdKey], this, error => { callback(error) }) } else { return new Promise((resolve, reject) => { - this[requestKey].sessionStore.set(this.sessionId, this, error => { + this[requestKey].sessionStore.set(this[sessionIdKey], this, error => { if (error) { reject(error) } else { @@ -147,16 +144,13 @@ module.exports = class Session { } } - [sign] () { - return cookieSignature.sign(this.sessionId, this[secretKey]) - } - [hash] () { const sess = this const str = stringify(sess, function (key, val) { // ignore sess.cookie property if (this === sess && key === 'cookie') { - return + // we want `touch` to affect the hash of the session + return sess.cookie.expires } return val @@ -172,12 +166,19 @@ module.exports = class Session { return this[originalHash] !== this[hash]() } - static restore (request, idGenerator, cookieOpts, secret, prevSession) { - const restoredSession = new Session(request, idGenerator, cookieOpts, secret, prevSession) + get sessionId () { + return this[sessionIdKey] + } + + get encryptedSessionId () { + return this[encryptedSessionIdKey] + } + + static restore (request, idGenerator, cookieOpts, secret, prevSession, sessionId) { + const restoredSession = new Session(request, idGenerator, cookieOpts, secret, prevSession, sessionId) const restoredCookie = new Cookie(cookieOpts) restoredCookie.expires = new Date(prevSession.cookie.expires) restoredSession.cookie = restoredCookie - restoredSession.expires = restoredCookie.expires restoredSession[originalHash] = restoredSession[hash]() return restoredSession } diff --git a/test/base.test.js b/test/base.test.js index 2dfad4d..7a8b10b 100644 --- a/test/base.test.js +++ b/test/base.test.js @@ -51,7 +51,7 @@ test('should support multiple secrets', async (t) => { const plugin = fastifyPlugin(async (fastify, opts) => { fastify.addHook('onRequest', (request, reply, done) => { request.sessionStore.set('aYb4uTIhdBXCfk_ylik4QN6-u26K0u0e', { - expires: Date.now() + 1000 + cookie: { expires: Date.now() - 1000 } }, done) }) }) @@ -98,9 +98,8 @@ test('should set session cookie using the default cookie name', async (t) => { const plugin = fastifyPlugin(async (fastify, opts) => { fastify.addHook('onRequest', (request, reply, done) => { request.sessionStore.set('Qk_XT2K7-clT-x1tVvoY6tIQ83iP72KN', { - expires: Date.now() + 1000, sessionId: 'Qk_XT2K7-clT-x1tVvoY6tIQ83iP72KN', - cookie: { secure: true, httpOnly: true, path: '/' } + cookie: { expires: Date.now() + 1000, secure: true, httpOnly: true, path: '/' } }, done) }) }) @@ -119,7 +118,7 @@ test('should set session cookie using the default cookie name', async (t) => { }) t.is(statusCode, 200) - t.regex(cookie, /sessionId=undefined; Path=\/; HttpOnly; Secure/) + t.regex(cookie, /sessionId=.*\..*; Path=\/; HttpOnly; Secure/) }) test('should create new session on expired session', async (t) => { @@ -127,9 +126,8 @@ test('should create new session on expired session', async (t) => { const plugin = fastifyPlugin(async (fastify, opts) => { fastify.addHook('onRequest', (request, reply, done) => { request.sessionStore.set('Qk_XT2K7-clT-x1tVvoY6tIQ83iP72KN', { - expires: Date.now() - 1000, sessionId: 'Qk_XT2K7-clT-x1tVvoY6tIQ83iP72KN', - cookie: { secure: true, httpOnly: true, path: '/' } + cookie: { expires: Date.now() - 1000, secure: true, httpOnly: true, path: '/' } }, done) }) }) @@ -163,12 +161,12 @@ test('should set session.expires if maxAge', async (t) => { const plugin = fastifyPlugin(async (fastify, opts) => { fastify.addHook('onRequest', (request, reply, done) => { request.sessionStore.set('Qk_XT2K7-clT-x1tVvoY6tIQ83iP72KN', { - expires: Date.now() + 1000 + cookie: { expires: Date.now() + 1000 } }, done) }) }) function handler (request, reply) { - t.truthy(request.session.expires) + t.truthy(request.session.cookie.expires) reply.send(200) } const port = await testServer(handler, options, plugin) @@ -187,7 +185,7 @@ test('should set new session cookie if expired', async (t) => { const plugin = fastifyPlugin(async (fastify, opts) => { fastify.addHook('onRequest', (request, reply, done) => { request.sessionStore.set('Qk_XT2K7-clT-x1tVvoY6tIQ83iP72KN', { - expires: Date.now() + 1000 + cookie: { expires: Date.now() - 1000 } }, done) }) }) @@ -258,7 +256,7 @@ test('should create new session if cookie contains invalid session', async (t) = const plugin = fastifyPlugin(async (fastify, opts) => { fastify.addHook('onRequest', (request, reply, done) => { request.sessionStore.set('Qk_XT2K7-clT-x1tVvoY6tIQ83iP72KN', { - expires: Date.now() + 1000 + cookie: { expires: Date.now() + 1000 } }, done) }) }) diff --git a/test/session.test.js b/test/session.test.js index 5ea66f8..90a9308 100644 --- a/test/session.test.js +++ b/test/session.test.js @@ -36,9 +36,11 @@ test('should destroy the session', async (t) => { }) test('should add session.encryptedSessionId object to request', async (t) => { - t.plan(2) + t.plan(3) const port = await testServer((request, reply) => { t.truthy(request.session.encryptedSessionId) + // serialize, then deserialize to make sure it's gone + t.falsy(JSON.parse(JSON.stringify(request.session)).encryptedSessionId) reply.send(200) }, DEFAULT_OPTIONS) @@ -59,22 +61,6 @@ test('should add session.cookie object to request', async (t) => { t.is(statusCode, 200) }) -test('should add session.expires object to request', async (t) => { - t.plan(2) - const options = { - secret: 'cNaoPYAwF60HZJzkcNaoPYAwF60HZJzk', - cookie: { maxAge: 42 } - } - const port = await testServer((request, reply) => { - t.truthy(request.session.expires) - reply.send(200) - }, options) - - const { statusCode } = await request(`http://localhost:${port}`) - - t.is(statusCode, 200) -}) - test('should add session.sessionId object to request', async (t) => { t.plan(2) const port = await testServer((request, reply) => { @@ -223,9 +209,8 @@ test('should decryptSession with custom request object', async (t) => { fastify.addHook('onRequest', (request, reply, done) => { request.sessionStore.set('Qk_XT2K7-clT-x1tVvoY6tIQ83iP72KN', { testData: 'this is a test', - expires: Date.now() + 1000, sessionId: 'Qk_XT2K7-clT-x1tVvoY6tIQ83iP72KN', - cookie: { secure: true, httpOnly: true, path: '/' } + cookie: { expires: Date.now() + 1000, secure: true, httpOnly: true, path: '/' } }, done) }) @@ -284,7 +269,7 @@ test('should bubble up errors with destroy call if session expired', async (t) = const store = { set (id, data, cb) { cb(null) }, get (id, cb) { - cb(null, { expires: Date.now() - 1000, cookie: { expires: Date.now() - 1000 } }) + cb(null, { cookie: { expires: Date.now() - 1000 } }) }, destroy (id, cb) { cb(new Error('No can do')) } } @@ -325,7 +310,7 @@ test('should not reset session cookie expiration if rolling is false', async (t) fastify.register(fastifyCookie) fastify.register(fastifySession, options) fastify.addHook('onRequest', (request, reply, done) => { - reply.send(request.session.expires) + reply.send(request.session.cookie.expires) done() }) @@ -362,7 +347,7 @@ test('should update the expires property of the session using Session#touch() ev fastify.register(fastifySession, options) fastify.addHook('onRequest', (request, reply, done) => { request.session.touch() - reply.send(request.session.expires) + reply.send(request.session.cookie.expires) done() }) diff --git a/test/store.test.js b/test/store.test.js index cc73e24..30969d4 100644 --- a/test/store.test.js +++ b/test/store.test.js @@ -87,7 +87,7 @@ test('should set new session cookie if expired', async (t) => { const plugin = fastifyPlugin(async (fastify, opts) => { fastify.addHook('onRequest', (request, reply, done) => { request.sessionStore.set('Qk_XT2K7-clT-x1tVvoY6tIQ83iP72KN', { - expires: Date.now() - 1000 + cookie: {expires: Date.now() - 1000} }, done) }) }) diff --git a/types/types.d.ts b/types/types.d.ts index c5c2ebf..66aca95 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -20,7 +20,7 @@ interface SessionData extends ExpressSessionData { encryptedSessionId: string; - /** Updates the `expires` property of the session. */ + /** Updates the `expires` property of the session's cookie. */ touch(): void; /**