├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .taprc ├── Readme.md ├── docker-compose.yml ├── lib └── session.js ├── package.json ├── plugin.js └── test ├── lib └── session.test.js ├── mongo.test.js └── session.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | 12 | # [*.md] 13 | # trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - "*.md" 7 | pull_request: 8 | paths-ignore: 9 | - "*.md" 10 | 11 | jobs: 12 | tests: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [12, 14, 16] 17 | steps: 18 | - uses: actions/checkout@v2.3.4 19 | - name: Use Node.js 20 | uses: actions/setup-node@v2.1.5 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: Install 24 | run: | 25 | npm install --ignore-scripts 26 | - name: Run tests 27 | run: | 28 | docker-compose up -d mongodb && npm run test-ci 29 | docker-compose down 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | node_modules 30 | 31 | # Optional npm cache directory 32 | .npm 33 | 34 | # Optional REPL history 35 | .node_repl_history 36 | 37 | # 0x 38 | .__browserify_string_empty.js 39 | profile-* 40 | 41 | # tap --cov 42 | .nyc_output/ 43 | 44 | # JetBrains IntelliJ IDEA 45 | .idea/ 46 | *.iml 47 | 48 | # VS Code 49 | .vscode/ 50 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.taprc: -------------------------------------------------------------------------------- 1 | no-check-coverage: true 2 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # fastify-server-session 2 | 3 | This is a plugin for [Fastify](https://fastify.io/) that facilitates keep state 4 | for clients between requests via server-side storage. 5 | 6 | **Requirements:** 7 | 8 | + [fastify-cookie](https://www.npmjs.com/package/fastify-cookie): used to set 9 | a cookie for tracking sessions. 10 | + [fastify-caching](https://www.npmjs.com/package/fastify-caching): used to 11 | store the session data via the `fastify.cache` decorator. 12 | 13 | 14 | **Installation:** 15 | 16 | ``` 17 | npm install fastify-server-session fastify-cookie fastify-caching 18 | ``` 19 | 20 | ## Example 21 | 22 | ### Server Side Storage 23 | 24 | Using this implementation the sessions will be stored in memory on the Fastify instance 25 | making the server stateful. This is not recommended for production. It will not share 26 | state among multiple instances of the same application. 27 | 28 | ```js 29 | const fastify = require('fastify')() 30 | fastify 31 | .register(require('fastify-cookie')) 32 | .register(require('fastify-caching')) 33 | .register(require('fastify-server-session'), { 34 | secretKey: 'some-secret-password-at-least-32-characters-long', 35 | sessionMaxAge: 900000, // 15 minutes in milliseconds 36 | cookie: { 37 | domain: '.example.com', 38 | path: '/' 39 | } 40 | }) 41 | 42 | fastify.get('/one', (req, reply) => { 43 | req.session.foo = 'foo' 44 | reply.send() 45 | }) 46 | 47 | fastify.get('/two', (req, reply) => { 48 | reply.send({foo: req.session.foo}) 49 | }) 50 | ``` 51 | 52 | ### Remote Cache Store 53 | 54 | `fastify-caching` offers the connectivity to a remote store as shown below with `ioredis` and `abstract-cache`. 55 | See `fastify-caching` [documentation](https://github.com/fastify/fastify-caching) for other 56 | storage capabilities. 57 | 58 | ```js 59 | // This example requires the following packages to be installed 60 | // - ioredis 61 | // - abstract-cache 62 | 63 | const IORedis = require('ioredis') 64 | const redis = new IORedis({host: '127.0.0.1'}) 65 | const abcache = require('abstract-cache')({ 66 | useAwait: true, 67 | driver: { 68 | name: 'abstract-cache-redis', 69 | options: {client: redis} 70 | } 71 | }) 72 | const fastify = require('fastify')() 73 | fastify 74 | .register(require('fastify-cookie')) 75 | .register(require('fastify-caching'), {cache: abcache}) 76 | .register(require('fastify-server-session'), { 77 | secretKey: 'some-secret-password-at-least-32-characters-long', 78 | sessionMaxAge: 900000, // 15 minutes in milliseconds 79 | cookie: { 80 | domain: '.example.com', 81 | path: '/' 82 | } 83 | }) 84 | 85 | fastify.get('/one', (req, reply) => { 86 | req.session.foo = 'foo' 87 | reply.send() 88 | }) 89 | 90 | fastify.get('/two', (req, reply) => { 91 | reply.send({foo: req.session.foo}) 92 | }) 93 | ``` 94 | 95 | **Note:** In the previous example the `sessionMaxAge` value will set the Redis TTL of the session key. 96 | 97 | ## Options 98 | 99 | The plugin accepts an options object with the following properties: 100 | 101 | + `secretKey` (Default: `undefined`): this is a **required** property that must 102 | be a string with a minimum of 32 characters. 103 | + `sessionCookieName` (Default: `sessionid`): a string to name the cookie sent 104 | to the client to track the session. 105 | + `sessionMaxAge` (Default: `1800000`): a duration in milliseconds for which 106 | the sessions will be valid. 107 | + `cookie`: an options as described in the [cookie module's documentation][cookiedoc]. 108 | The default value is: 109 | * `domain`: `undefined` 110 | * `expires`: same as `sessionMaxAge` 111 | * `httpOnly`: `true` 112 | * `path`: `undefined` 113 | * `sameSite`: `true` 114 | 115 | [cookiedoc]: https://www.npmjs.com/package/cookie#options-1 116 | 117 | ## TypeScript 118 | 119 | To use type checking on session object you can use the declaration: 120 | 121 | ```typescript 122 | declare module 'fastify' { 123 | interface FastifyRequest { 124 | session: { 125 | foo: string; 126 | bar: number; 127 | }; 128 | } 129 | } 130 | ``` 131 | 132 | ## License 133 | 134 | [MIT License](http://jsumners.mit-license.org/) 135 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | mongodb: 5 | image: mongo 6 | ports: 7 | - 27017:27017 8 | -------------------------------------------------------------------------------- /lib/session.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const syms = { 4 | kSessionModified: Symbol('fastify-servier-session.sessionModified') 5 | } 6 | 7 | module.exports = function getSession (fromObject) { 8 | let session 9 | if (fromObject) { 10 | session = fromObject 11 | session[syms.kSessionModified] = false 12 | } else { 13 | session = { 14 | get [Symbol.toStringTag] () { return 'fastify-server-session.session-object' }, 15 | [syms.kSessionModified]: false 16 | } 17 | } 18 | const proxy = new Proxy(session, { 19 | set (target, prop, value, receiver) { 20 | if (target[syms.kSessionModified] === false) { 21 | target[syms.kSessionModified] = true 22 | } 23 | target[prop] = value 24 | return receiver 25 | } 26 | }) 27 | return proxy 28 | } 29 | 30 | module.exports.symbols = syms 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-server-session", 3 | "version": "5.0.1", 4 | "description": "A Fastify plugin to maintain state on the server", 5 | "main": "plugin.js", 6 | "scripts": { 7 | "pretest": "docker-compose up -d mongodb", 8 | "posttest": "docker-compose down", 9 | "test": "tap 'test/**/*.test.js'", 10 | "test-ci": "tap --cov 'test/**/*.test.js'", 11 | "lint": "standard | snazzy", 12 | "lint-ci": "standard" 13 | }, 14 | "precommit": [ 15 | "lint", 16 | "test" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+ssh://git@github.com/jsumners/fastify-server-session.git" 21 | }, 22 | "keywords": [ 23 | "fastify", 24 | "session" 25 | ], 26 | "author": "James Sumners ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/jsumners/fastify-server-session/issues" 30 | }, 31 | "homepage": "https://github.com/jsumners/fastify-server-session#readme", 32 | "devDependencies": { 33 | "abstract-cache": "^1.0.1", 34 | "abstract-cache-mongo": "^2.0.2", 35 | "abstract-logging": "^2.0.0", 36 | "cookie": "^0.4.1", 37 | "fastify": "^3.0.0", 38 | "fastify-caching": "^6.0.1", 39 | "fastify-cookie": "^5.3.1", 40 | "pre-commit": "^1.2.2", 41 | "request": "^2.88.0", 42 | "snazzy": "^9.0.0", 43 | "standard": "^16.0.3", 44 | "tap": "^15.0.9" 45 | }, 46 | "dependencies": { 47 | "cookie-signature": "^1.1.0", 48 | "fastify-plugin": "^3.0.0", 49 | "merge-options": "^3.0.4", 50 | "uid-safe": "^2.1.5" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fp = require('fastify-plugin') 4 | const { sign, unsign } = require('cookie-signature') 5 | const uidgen = require('uid-safe') 6 | const merge = require('merge-options') 7 | const MAX_AGE = 1800000 // 30 minutes 8 | const defaultOptions = { 9 | cookie: { 10 | domain: undefined, 11 | expires: MAX_AGE, 12 | httpOnly: true, 13 | path: undefined, 14 | sameSite: true 15 | }, 16 | secretKey: undefined, 17 | sessionCookieName: 'sessionid', 18 | sessionMaxAge: MAX_AGE 19 | } 20 | const getSession = require('./lib/session') 21 | const { symbols: syms } = getSession 22 | 23 | function plugin (fastify, options, pluginRegistrationDone) { 24 | // eslint-disable-next-line 25 | const _options = Function.prototype.isPrototypeOf(options) ? {} : options 26 | const opts = merge({}, defaultOptions, _options) 27 | if (!opts.secretKey) { 28 | return pluginRegistrationDone(Error('must supply secretKey')) 29 | } 30 | // https://security.stackexchange.com/a/96176/38214 31 | if (opts.secretKey.length < 32) { 32 | return pluginRegistrationDone( 33 | Error('secretKey must be at least 32 characters') 34 | ) 35 | } 36 | if (opts.cookie.expires && !Number.isInteger(opts.cookie.expires)) { 37 | return pluginRegistrationDone( 38 | Error('cookie expires time must be a value in milliseconds') 39 | ) 40 | } 41 | 42 | fastify.decorateRequest('session', null) 43 | fastify.addHook('onRequest', function (req, reply, hookFinished) { 44 | function getHandler (cached) { 45 | if (!cached) { 46 | req.log.trace('session data missing (new/expired)') 47 | return hookFinished() 48 | } 49 | req.session = getSession(cached.item) 50 | req.log.trace('session restored: %j', req.session) 51 | hookFinished() 52 | } 53 | 54 | function getErrorHandler (err) { 55 | req.log.trace('could not retrieve session data') 56 | hookFinished(err) 57 | } 58 | 59 | req.session = getSession() 60 | if (!req.cookies[opts.sessionCookieName]) { 61 | return hookFinished() 62 | } 63 | 64 | const sessionId = unsign( 65 | req.cookies[opts.sessionCookieName], 66 | opts.secretKey 67 | ) 68 | req.log.trace('sessionId: %s', sessionId) 69 | if (sessionId === false) { 70 | req.log.warn('session id signature mismatch, starting new session') 71 | return hookFinished() 72 | } 73 | 74 | if (this.cache.await) { 75 | this.cache.get(sessionId) 76 | .then(getHandler) 77 | .catch(getErrorHandler) 78 | } else { 79 | this.cache.get(sessionId, (err, cached) => { 80 | if (err) { 81 | getErrorHandler(err) 82 | } else { 83 | getHandler(cached) 84 | } 85 | }) 86 | } 87 | }) 88 | 89 | fastify.addHook('onSend', function (req, reply, payload, hookFinished) { 90 | if (req.session[syms.kSessionModified] === false) { 91 | return hookFinished() 92 | } 93 | 94 | if (req.cookies[opts.sessionCookieName]) { 95 | const id = unsign(req.cookies[opts.sessionCookieName], opts.secretKey) 96 | return storeSession.call(this, null, id) 97 | } 98 | 99 | uidgen(18, storeSession.bind(this)) 100 | 101 | function storeSession (err, sessionId) { 102 | function cacheSetHandler () { 103 | const cookieExiresMs = opts.cookie && opts.cookie.expires 104 | const cookieOpts = merge({}, opts.cookie, { 105 | expires: !cookieExiresMs 106 | ? undefined 107 | : new Date(Date.now() + cookieExiresMs) 108 | }) 109 | const signedId = sign(sessionId, opts.secretKey) 110 | reply.setCookie(opts.sessionCookieName, signedId, cookieOpts) 111 | hookFinished() 112 | } 113 | 114 | function cacheErrorHandler () { 115 | req.log.trace('error saving session: %s', err.message) 116 | return hookFinished(err) 117 | } 118 | 119 | if (err) { 120 | req.log.trace('could not store session with invalid id') 121 | return hookFinished(err) 122 | } 123 | 124 | if (!sessionId) { 125 | req.log.trace('could not store session with missing id') 126 | return hookFinished(Error('missing session id')) 127 | } 128 | if (this.cache.await) { 129 | this.cache.set(sessionId, req.session, opts.sessionMaxAge) 130 | .then(cacheSetHandler) 131 | .catch(cacheErrorHandler) 132 | } else { 133 | this.cache.set(sessionId, req.session, opts.sessionMaxAge, err => { 134 | if (err) { 135 | cacheErrorHandler(err) 136 | } else { 137 | cacheSetHandler() 138 | } 139 | }) 140 | } 141 | } 142 | }) 143 | 144 | pluginRegistrationDone() 145 | } 146 | 147 | module.exports = fp(plugin, { 148 | fastify: '^3.0.0', 149 | dependencies: ['fastify-cookie'], 150 | decorators: { 151 | fastify: ['cache'] 152 | } 153 | }) 154 | -------------------------------------------------------------------------------- /test/lib/session.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const getSession = require('../../lib/session') 5 | const { symbols: syms } = getSession 6 | 7 | test('returns a session object that is unmodified', t => { 8 | t.plan(1) 9 | const session = getSession() 10 | t.equal(session[syms.kSessionModified], false) 11 | }) 12 | 13 | test('detects when session is modified', t => { 14 | t.plan(2) 15 | const session = getSession() 16 | session.foo = 'foo' 17 | t.equal(session.foo, 'foo') 18 | t.equal(session[syms.kSessionModified], true) 19 | }) 20 | 21 | test('returns existing unmodified session from existing object', t => { 22 | t.plan(2) 23 | const session = getSession({ foo: 'foo' }) 24 | t.equal(session.foo, 'foo') 25 | t.equal(session[syms.kSessionModified], false) 26 | }) 27 | 28 | test('detects modification of session from existing object', t => { 29 | t.plan(2) 30 | const session = getSession({ foo: 'foo' }) 31 | t.equal(session.foo, 'foo') 32 | 33 | session.bar = 'bar' 34 | t.equal(session[syms.kSessionModified], true) 35 | }) 36 | -------------------------------------------------------------------------------- /test/mongo.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const request = require('request') 5 | const fastify = require('fastify') 6 | const fastifyCaching = require('fastify-caching') 7 | const fastifyCookie = require('fastify-cookie') 8 | const plugin = require('../') 9 | const secretKey = '12345678901234567890123456789012' 10 | 11 | test('can store sessions in a mongo database', (t) => { 12 | t.plan(3) 13 | 14 | const server = fastify() 15 | const cache = require('abstract-cache')({ 16 | useAwait: false, 17 | driver: { 18 | name: 'abstract-cache-mongo', 19 | options: { 20 | dbName: 'testing', 21 | mongodb: { 22 | url: 'mongodb://localhost:27017/testing' 23 | } 24 | } 25 | } 26 | }) 27 | 28 | t.teardown(() => { 29 | server.close() 30 | cache.stop(() => {}) 31 | }) 32 | 33 | cache.start((err) => { 34 | if (err) t.threw(err) 35 | runTests() 36 | }) 37 | 38 | function runTests () { 39 | server 40 | .register(fastifyCookie) 41 | .register(fastifyCaching, { cache }) 42 | .register(plugin, { secretKey }) 43 | .after((err) => { 44 | if (err) t.threw(err) 45 | }) 46 | 47 | server.get('/one', (req, reply) => { 48 | t.ok(req.session) 49 | t.strictSame(req.session, {}) 50 | req.session.one = true 51 | reply.send() 52 | }) 53 | 54 | server.get('/two', (req, reply) => { 55 | t.strictSame(req.session, { 56 | one: true 57 | }) 58 | reply.send() 59 | }) 60 | 61 | server.listen(0, (err) => { 62 | server.server.unref() 63 | if (err) t.threw(err) 64 | const port = server.server.address().port 65 | const address = `http://127.0.0.1:${port}` 66 | const jar = request.jar() 67 | request.get(`${address}/one`, { jar }, (err, res, body) => { 68 | if (err) t.threw(err) 69 | request.get(`${address}/two`, { jar }, (err, res, body) => { 70 | if (err) t.threw(err) 71 | }) 72 | }) 73 | }) 74 | } 75 | }) 76 | 77 | test('can store sessions in a mongo database useAwait:true', (t) => { 78 | t.plan(3) 79 | 80 | const server = fastify() 81 | const cache = require('abstract-cache')({ 82 | useAwait: true, 83 | driver: { 84 | name: 'abstract-cache-mongo', 85 | options: { 86 | dbName: 'testing-await', 87 | mongodb: { 88 | url: 'mongodb://localhost:27017/testing-await' 89 | } 90 | } 91 | } 92 | }) 93 | 94 | t.teardown(() => { 95 | server.close() 96 | cache.stop() 97 | }) 98 | 99 | cache.start() 100 | .then(runTests) 101 | .catch(t.threw) 102 | 103 | function runTests () { 104 | server 105 | .register(fastifyCookie) 106 | .register(fastifyCaching, { cache }) 107 | .register(plugin, { secretKey }) 108 | .after((err) => { 109 | if (err) t.threw(err) 110 | }) 111 | 112 | server.get('/one', (req, reply) => { 113 | t.ok(req.session) 114 | t.strictSame(req.session, {}) 115 | req.session.one = true 116 | reply.send() 117 | }) 118 | 119 | server.get('/two', (req, reply) => { 120 | t.strictSame(req.session, { 121 | one: true 122 | }) 123 | reply.send() 124 | }) 125 | 126 | server.listen(0, (err) => { 127 | server.server.unref() 128 | if (err) t.threw(err) 129 | const port = server.server.address().port 130 | const address = `http://127.0.0.1:${port}` 131 | const jar = request.jar() 132 | request.get(`${address}/one`, { jar }, (err, res, body) => { 133 | if (err) t.threw(err) 134 | request.get(`${address}/two`, { jar }, (err, res, body) => { 135 | if (err) t.threw(err) 136 | }) 137 | }) 138 | }) 139 | } 140 | }) 141 | -------------------------------------------------------------------------------- /test/session.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const request = require('request') 5 | const cookie = require('cookie') 6 | const fastify = require('fastify') 7 | const fastifyCaching = require('fastify-caching') 8 | const fastifyCookie = require('fastify-cookie') 9 | const plugin = require('../') 10 | const secretKey = '12345678901234567890123456789012' 11 | 12 | test('rejects if no secretKey supplied', t => { 13 | t.plan(1) 14 | const server = fastify() 15 | t.throws(server.register.bind(plugin)) 16 | }) 17 | 18 | test('rejects if secretKey is too short', t => { 19 | t.plan(1) 20 | const server = fastify() 21 | t.throws(server.register.bind(plugin, { secretKey: '123456' })) 22 | }) 23 | 24 | test('rejects if cookie expiration is not an integer', t => { 25 | t.plan(1) 26 | const server = fastify() 27 | server.register(fastifyCookie).register(fastifyCaching) 28 | t.throws( 29 | server.register.bind(plugin, { secretKey, cookie: { expires: 'foo' } }) 30 | ) 31 | }) 32 | 33 | test('registers with all dependencies met', t => { 34 | t.plan(1) 35 | const server = fastify() 36 | server 37 | .register(fastifyCookie) 38 | .register(fastifyCaching) 39 | .register(plugin, { secretKey }) 40 | .after(err => { 41 | if (err) t.threw(err) 42 | t.pass() 43 | }) 44 | 45 | server.listen(0, err => { 46 | server.server.unref() 47 | if (err) t.threw(err) 48 | }) 49 | 50 | t.teardown(() => server.close().catch(() => {})) 51 | }) 52 | 53 | test('decorates server with session object', t => { 54 | t.plan(2) 55 | const server = fastify() 56 | server 57 | .register(fastifyCookie) 58 | .register(fastifyCaching) 59 | .register(plugin, { secretKey }) 60 | 61 | server.get('/', (req, reply) => { 62 | t.ok(req.session) 63 | t.strictSame(req.session, {}) 64 | reply.send() 65 | }) 66 | 67 | server.listen(0, err => { 68 | server.server.unref() 69 | if (err) t.threw(err) 70 | request.get( 71 | `http://127.0.0.1:${server.server.address().port}/`, 72 | (err, res, body) => { 73 | if (err) t.threw(err) 74 | } 75 | ) 76 | }) 77 | }) 78 | 79 | test('sets cookie name', t => { 80 | t.plan(2) 81 | 82 | const server = fastify() 83 | server 84 | .register(fastifyCookie) 85 | .register(fastifyCaching) 86 | .register(plugin, { secretKey, sessionCookieName: 'foo-session' }) 87 | 88 | server.get('/', (req, reply) => { 89 | req.session.touched = true 90 | reply.send() 91 | }) 92 | 93 | server.listen(0, err => { 94 | server.server.unref() 95 | if (err) t.threw(err) 96 | request.get( 97 | `http://127.0.0.1:${server.server.address().port}/`, 98 | (err, res, body) => { 99 | if (err) t.threw(err) 100 | t.ok(res.headers['set-cookie']) 101 | t.match(res.headers['set-cookie'], /foo-session/) 102 | } 103 | ) 104 | }) 105 | }) 106 | 107 | test('sets cookie expiration', t => { 108 | t.plan(1) 109 | 110 | const server = fastify() 111 | server 112 | .register(fastifyCookie) 113 | .register(fastifyCaching) 114 | .register(plugin, { secretKey, cookie: { expires: 60000 } }) 115 | 116 | server.get('/', (req, reply) => { 117 | req.session.touched = true 118 | reply.send() 119 | }) 120 | 121 | server.listen(0, err => { 122 | server.server.unref() 123 | if (err) t.threw(err) 124 | request.get( 125 | `http://127.0.0.1:${server.server.address().port}/`, 126 | (err, res, body) => { 127 | if (err) t.threw(err) 128 | const setCookie = cookie.parse(res.headers['set-cookie'][0]) 129 | const future = new Date(Date.now() + 60000) 130 | const expiration = new Date(setCookie.Expires) 131 | t.ok(future > expiration) 132 | } 133 | ) 134 | }) 135 | }) 136 | 137 | test('set session data', t => { 138 | t.plan(4) 139 | 140 | const server = fastify() 141 | server 142 | .register(fastifyCookie) 143 | .register(fastifyCaching) 144 | .register(plugin, { secretKey }) 145 | 146 | server.get('/one', (req, reply) => { 147 | req.session.one = true 148 | reply.send() 149 | }) 150 | 151 | server.get('/two', (req, reply) => { 152 | t.ok(req.session) 153 | t.ok(req.session.one) 154 | reply.send() 155 | }) 156 | 157 | server.listen(0, err => { 158 | server.server.unref() 159 | if (err) t.threw(err) 160 | 161 | const port = server.server.address().port 162 | const r = request.defaults({ 163 | baseUrl: `http://127.0.0.1:${port}`, 164 | jar: request.jar() 165 | }) 166 | 167 | r.get('/one', (err, res, body) => { 168 | if (err) t.threw(err) 169 | t.ok(res.headers['set-cookie']) 170 | t.match(res.headers['set-cookie'], /sessionid/) 171 | 172 | r.get('/two', (err, res, body) => { 173 | if (err) t.threw(err) 174 | }) 175 | }) 176 | }) 177 | }) 178 | 179 | test('separate clients do not share a session', { only: true }, t => { 180 | t.plan(8) 181 | const server = fastify() 182 | server 183 | .register(fastifyCookie) 184 | .register(fastifyCaching) 185 | .register(plugin, { 186 | secretKey, 187 | cookie: { 188 | domain: '127.0.0.1', 189 | path: '/' 190 | } 191 | }) 192 | 193 | server.get('/one/:clientid', (req, reply) => { 194 | t.notOk(req.session.client) 195 | req.session.client = req.params.clientid 196 | reply.send() 197 | }) 198 | 199 | server.get('/two/:clientid', (req, reply) => { 200 | t.ok(req.session.client) 201 | t.equal(req.session.client, req.params.clientid) 202 | reply.send() 203 | }) 204 | 205 | server.listen(0, err => { 206 | server.server.unref() 207 | if (err) t.threw(err) 208 | 209 | const port = server.server.address().port 210 | const jar1 = request.jar() 211 | const jar2 = request.jar() 212 | const r = request.defaults({ baseUrl: `http://127.0.0.1:${port}` }) 213 | 214 | r.get({ url: '/one/foo', jar: jar1 }, (err, res, body) => { 215 | if (err) t.threw(err) 216 | 217 | r.get({ url: '/one/bar', jar: jar2 }, (err, res, body) => { 218 | if (err) t.threw(err) 219 | 220 | r.get({ url: '/two/foo', jar: jar1 }, (err, res, body) => { 221 | t.error(err) 222 | }) 223 | r.get({ url: '/two/bar', jar: jar2 }, (err, res, body) => { 224 | t.error(err) 225 | }) 226 | }) 227 | }) 228 | }) 229 | }) 230 | 231 | test('no cookie is sent when new session is not changed', t => { 232 | t.plan(1) 233 | 234 | const server = fastify() 235 | server 236 | .register(fastifyCookie) 237 | .register(fastifyCaching) 238 | .register(plugin, { secretKey }) 239 | 240 | server.get('/notcreate', (req, reply) => { 241 | reply.send() 242 | }) 243 | 244 | server.listen(0, err => { 245 | server.server.unref() 246 | if (err) t.threw(err) 247 | 248 | const port = server.server.address().port 249 | const r = request.defaults({ 250 | baseUrl: `http://127.0.0.1:${port}`, 251 | jar: false 252 | }) 253 | r.get('/notcreate', (err, res, body) => { 254 | if (err) t.threw(err) 255 | t.notOk(res.headers['set-cookie']) 256 | }) 257 | }) 258 | }) 259 | 260 | test('issue #7: modify existing session', t => { 261 | t.plan(2) 262 | 263 | const server = fastify() 264 | server 265 | .register(fastifyCookie) 266 | .register(fastifyCaching) 267 | .register(plugin, { 268 | secretKey, 269 | cookie: { 270 | domain: '127.0.0.1', 271 | path: '/' 272 | } 273 | }) 274 | 275 | server.get('/one', (req, reply) => { 276 | req.session.foo = 'foo' 277 | reply.send({ hello: 'world' }) 278 | }) 279 | 280 | server.get('/two', (req, reply) => { 281 | req.session.bar = 'bar' 282 | reply.send({ session: req.session }) 283 | }) 284 | 285 | server.listen(0, err => { 286 | server.server.unref() 287 | if (err) t.threw(err) 288 | 289 | const port = server.server.address().port 290 | const r = request.defaults({ 291 | baseUrl: `http://127.0.0.1:${port}`, 292 | jar: request.jar() 293 | }) 294 | 295 | r.get('/one', (err, res, body) => { 296 | if (err) t.threw(err) 297 | t.strictSame(JSON.parse(body), { hello: 'world' }) 298 | r.get('/two', (err, res, body) => { 299 | if (err) t.threw(err) 300 | t.strictSame(JSON.parse(body), { session: { foo: 'foo', bar: 'bar' } }) 301 | }) 302 | }) 303 | }) 304 | }) 305 | --------------------------------------------------------------------------------