diff --git a/README.md b/README.md index 36481fb..1889c7d 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ This middleware guarantees the following: - If a session has not been updated within `maxAge`, its data will be expired. - Minimum public interfaces and configuration options. - Cookie options: `maxAge`, `path`, `domain`, `secure`, `httpOnly` - - Session interfaces: `session`, `sessionHandler { regenerateId(), setMaxAge() }` + - Session interfaces: `session`, `sessionHandler { regenerateId() }` - Store interfaces: `get()`, `set()`, `destroy()` @@ -66,14 +66,13 @@ app.listen(3000) - session data via `ctx.session` (the same way as `koa-generic-session`) - session methods via `ctx.sessionHandler` - `regenerateId()`: regenerate session id - - `setMaxAge(ms)`: update session's `maxAge` to `ms` milliseconds, only take effect when session data has been changed ## Options - `key`: session cookie name and store key prefix - `store`: session store -- `cookie`: cookie options, only supports `maxAge`, `path`, `domain`, `secure`, `httpOnly` (see option details in [`cookies`](https://github.com/pillarjs/cookies) module) +- `cookie`: cookie options, can be an object (static cookie options) or a function that returns an object (dynamic cookie options). Only `maxAge`, `path`, `domain`, `secure`, `httpOnly` are supported as option keys (see option details in [`cookies`](https://github.com/pillarjs/cookies) module). ## Session expiration @@ -84,7 +83,17 @@ Default session has settings `cookie.maxAge = 0` for cookie and `ttl = ONE_DAY` With settings that `cookie.maxAge > 0`, the `ttl` for store data will be always the same as `maxAge`. -When a middleware updates session data, it can also update the `maxAge` by calling `sessionHandler.setMaxAge(ms)`. Then the session's expiration time will be updated to `now() + maxAge` milliseconds. +When setting `cookie` option to a plain object, all sessions will use the same cookie options. If a function is assigned to `cookie`, cookie options will be dynamically calculated at each (non-empty) session's saving stage. +You may use different `maxAge` for user and guest sessions, by initializing the session middleware as below: +```javascript +session({ + cookie: ctx => { + return { + maxAge: ctx.session.user ? ONE_MONTH : ONE_DAY, + } + } +}) +``` ## Session security diff --git a/package.json b/package.json index 75757da..0d69497 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "koa-session-minimal", - "version": "2.0.1", + "version": "3.0.0", "description": "Minimal implementation of session middleware for Koa 2. Inspired by and compatible with koa-generic-session", "main": "dist/session.js", "scripts": { diff --git a/src/session.js b/src/session.js index da67915..34d4c39 100644 --- a/src/session.js +++ b/src/session.js @@ -5,6 +5,20 @@ const MemoryStore = require('./memory_store') const ONE_DAY = 24 * 3600 * 1000 // one day in milliseconds +const cookieOpt = cookie => { + const options = Object.assign({ + maxAge: 0, // default to use session cookie + path: '/', + secure: false, + httpOnly: true, + }, cookie || {}, { + overwrite: true, // overwrite previous session cookie changes + signed: false, // disable signed option + }) + if (!(options.maxAge >= 0)) options.maxAge = 0 + return options +} + const deleteSession = (ctx, key, cookie, store, sid) => { const deleteOption = Object.assign({}, cookie) delete deleteOption.maxAge @@ -22,20 +36,9 @@ module.exports = options => { const opt = options || {} const key = opt.key || 'koa:sess' const store = new Store(opt.store || new MemoryStore()) - const defaultCookie = Object.assign({ - maxAge: 0, // default to use session cookie - path: '/', - secure: false, - httpOnly: true, - }, opt.cookie || {}, { - overwrite: true, // overwrite previous session cookie changes - signed: false, // disable signed option - }) - if (!(defaultCookie.maxAge >= 0)) defaultCookie.maxAge = 0 + const defaultCookie = opt.cookie instanceof Function ? opt.cookie : cookieOpt(opt.cookie) return async (ctx, next) => { - const cookie = Object.assign({}, defaultCookie) - // initialize session id and data const cookieSid = ctx.cookies.get(key) @@ -57,13 +60,11 @@ module.exports = options => { regenerateId: () => { sid = uid.sync(24) }, - setMaxAge: ms => { - cookie.maxAge = ms >= 0 ? ms : 0 - }, } await next() + const cookie = defaultCookie instanceof Function ? cookieOpt(defaultCookie(ctx)) : defaultCookie const sessionHasData = ctx.session && Object.keys(ctx.session).length if (sid !== cookieSid) { // a new session id diff --git a/test/async_store.test.js b/test/async_store.test.js index 4cfc253..93cf3d4 100644 --- a/test/async_store.test.js +++ b/test/async_store.test.js @@ -23,8 +23,7 @@ const updateSession = (ctx, next) => { break default: if (ctx.url.startsWith('/maxage/')) { - const ms = parseInt(ctx.url.replace('/maxage/', ''), 10) - ctx.sessionHandler.setMaxAge(ms) + ctx.ms = parseInt(ctx.url.replace('/maxage/', ''), 10) ctx.session.time = Date.now() } } @@ -399,6 +398,33 @@ describe('session with async store', () => { }) }) }) +}) + +describe('async store with dynamic cookie options', () => { + const app = new Koa() + const key = 'koa:sess' + const store = new SpyStore() + + app.use(session({ + store, + cookie: ctx => { // eslint-disable-line arrow-body-style + return { + maxAge: ctx.ms, + } + }, + })) + app.use(updateSession) + app.use(sessionToBody) + + const client = request(app.listen()) + let startTime + let ttl + + beforeEach(() => { + store.clear() + ttl = 24 * 3600 * 1000 // one day in milliseconds + startTime = Date.now() + }) it('setMaxAge(ms) should set maxAge and ttl', done => { client.get('/maxage/0').expect(200).end((err1, res1) => { @@ -413,7 +439,7 @@ describe('session with async store', () => { destroy: [], }) - const maxAge = 1000 + const maxAge = 123 client.get(`/maxage/${maxAge}`).expect(200).end((err2, res2) => { if (err2) done(err2) validateCookie(res2, key) diff --git a/test/generator_store.test.js b/test/generator_store.test.js index a369fc7..ee15b82 100644 --- a/test/generator_store.test.js +++ b/test/generator_store.test.js @@ -23,8 +23,7 @@ const updateSession = (ctx, next) => { break default: if (ctx.url.startsWith('/maxage/')) { - const ms = parseInt(ctx.url.replace('/maxage/', ''), 10) - ctx.sessionHandler.setMaxAge(ms) + ctx.ms = parseInt(ctx.url.replace('/maxage/', ''), 10) ctx.session.time = Date.now() } } @@ -399,6 +398,33 @@ describe('session with generator store', () => { }) }) }) +}) + +describe('generator store with dynamic cookie options', () => { + const app = new Koa() + const key = 'koa:sess' + const store = new SpyStore() + + app.use(session({ + store, + cookie: ctx => { // eslint-disable-line arrow-body-style + return { + maxAge: ctx.ms, + } + }, + })) + app.use(updateSession) + app.use(sessionToBody) + + const client = request(app.listen()) + let startTime + let ttl + + beforeEach(() => { + store.clear() + ttl = 24 * 3600 * 1000 // one day in milliseconds + startTime = Date.now() + }) it('setMaxAge(ms) should set maxAge and ttl', done => { client.get('/maxage/0').expect(200).end((err1, res1) => { @@ -413,7 +439,7 @@ describe('session with generator store', () => { destroy: [], }) - const maxAge = 1000 + const maxAge = 123 client.get(`/maxage/${maxAge}`).expect(200).end((err2, res2) => { if (err2) done(err2) validateCookie(res2, key)