From a593bc23aa9a66d1f442da2e565b8ddf17a92dec Mon Sep 17 00:00:00 2001 From: Samith Reddy Chinni Date: Thu, 23 Oct 2025 14:05:09 +0530 Subject: [PATCH] enhancement: add encode/decode overrides and README example; relates to expressjs#169, #174 --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ index.js | 23 +++++++++++++++++------ test/test.js | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c066746..d00a47d 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,54 @@ app.use(cookieSession({ // ... your logic here ... ``` +### Custom encode/decode (encryption example) + +If you want to encrypt (or compress) the session payload before it is +stored in the cookie you can provide `encode` and `decode` functions to the +middleware. Below is a minimal example using Node's `crypto` and AES-256-GCM. + +Note: this is a compact example for demonstration. In production you should +securely manage keys, use unique nonces/IVs per-encryption, and consider +authenticity/rotation policies. + +```js +var cookieSession = require('cookie-session') +var crypto = require('crypto') + +// secretKey should be 32 bytes for AES-256 +var secretKey = Buffer.from(process.env.SESSION_ENC_KEY, 'hex') + +function encodeEncrypted (obj) { + var plaintext = JSON.stringify(obj) + var iv = crypto.randomBytes(12) // recommended nonce size for GCM + var cipher = crypto.createCipheriv('aes-256-gcm', secretKey, iv) + var encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]) + var tag = cipher.getAuthTag() + + // store iv + tag + cipher in base64 for cookie transport + return Buffer.concat([iv, tag, encrypted]).toString('base64') +} + +function decodeEncrypted (str) { + var buf = Buffer.from(str, 'base64') + var iv = buf.slice(0, 12) + var tag = buf.slice(12, 28) + var encrypted = buf.slice(28) + + var decipher = crypto.createDecipheriv('aes-256-gcm', secretKey, iv) + decipher.setAuthTag(tag) + var decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]) + return JSON.parse(decrypted.toString('utf8')) +} + +app.use(cookieSession({ + name: 'session', + keys: ['signing-key'], // still use signing for tamper protection + encode: encodeEncrypted, + decode: decodeEncrypted +})) +``` + ## Usage Limitations ### Max Cookie Size diff --git a/index.js b/index.js index ea2b04e..803f343 100644 --- a/index.js +++ b/index.js @@ -57,14 +57,18 @@ function cookieSession (options) { debug('session options %j', opts) + // encode/decode overrides (allow encryption/compression/custom serialization) + var encodeFn = typeof opts.encode === 'function' ? opts.encode : encode + var decodeFn = typeof opts.decode === 'function' ? opts.decode : decode + return function _cookieSession (req, res, next) { var cookies = new Cookies(req, res, { keys: keys }) var sess - // for overriding - req.sessionOptions = Object.create(opts) + // for overriding + req.sessionOptions = Object.create(opts) // define req.session getter / setter Object.defineProperty(req, 'session', { @@ -86,7 +90,7 @@ function cookieSession (options) { } // get session - if ((sess = tryGetSession(cookies, name, req.sessionOptions))) { + if ((sess = tryGetSession(cookies, name, req.sessionOptions, decodeFn))) { return sess } @@ -125,7 +129,7 @@ function cookieSession (options) { } else if ((!sess.isNew || sess.isPopulated) && sess.isChanged) { // save populated or non-new changed session debug('save %s', name) - cookies.set(name, Session.serialize(sess), req.sessionOptions) + cookies.set(name, encodeFn(sess), req.sessionOptions) } } catch (e) { debug('error saving session %s', e.message) @@ -273,7 +277,7 @@ function encode (body) { * @private */ -function tryGetSession (cookies, name, opts) { +function tryGetSession (cookies, name, opts, decodeFn) { var str = cookies.get(name, opts) if (!str) { @@ -283,7 +287,14 @@ function tryGetSession (cookies, name, opts) { debug('parse %s', str) try { - return Session.deserialize(str) + var obj = decodeFn(str) + + // build session with context like Session.deserialize would do + var ctx = new SessionContext() + ctx._new = false + ctx._val = str + + return new Session(ctx, obj) } catch (err) { return undefined } diff --git a/test/test.js b/test/test.js index cd9e5d5..3c56f0e 100644 --- a/test/test.js +++ b/test/test.js @@ -603,6 +603,46 @@ describe('Cookie Session', function () { }) }) }) + + describe('custom encode/decode', function () { + it('should allow overriding encode and decode functions', function (done) { + // simple encode/decode that prefixes with 'X:' and base64 encodes JSON + function myEncode (obj) { + var str = JSON.stringify(obj) + return 'X:' + Buffer.from(str).toString('base64') + } + + function myDecode (str) { + if (str.indexOf('X:') !== 0) throw new Error('bad prefix') + var body = Buffer.from(str.slice(2), 'base64').toString('utf8') + return JSON.parse(body) + } + + var app = connect() + app.use(session({ keys: ['a'], encode: myEncode, decode: myDecode })) + app.use(function (req, res) { + if (req.url === '/set') { + req.session.message = 'hello-enc' + return res.end() + } + + res.setHeader('Content-Type', 'application/json') + res.end(JSON.stringify(req.session)) + }) + + request(app) + .get('/set') + .expect(shouldHaveCookie('session')) + .expect(200, '', function (err, res) { + if (err) return done(err) + + request(app) + .get('/') + .set('Cookie', cookieHeader(cookies(res))) + .expect(200, { message: 'hello-enc' }, done) + }) + }) + }) }) function App (options) {