From 0362c8e457a8a5fc164e51e9f74916b5d4ce9e26 Mon Sep 17 00:00:00 2001 From: Jonathan Ong Date: Fri, 15 Nov 2013 10:03:40 -0800 Subject: [PATCH] add app.keys support --- docs/api/index.md | 20 ++++++++++ lib/application.js | 36 +++++++++++++++++- package.json | 3 +- test/context/cookies.js | 81 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 test/context/cookies.js diff --git a/docs/api/index.md b/docs/api/index.md index 5d0f4bf..96d071a 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -59,6 +59,26 @@ http.createServer(app.callback()).listen(3001); Add the given middleware function to this application. See [Middleware](#middleware) for more information. +### app.keys= + + Set signed cookie keys. + + These are passed to [KeyGrip](https://github.com/jed/keygrip), + however you may also pass your own `KeyGrip` instance. For + example the following are acceptable: + +```js +app.keys = ['im a newer secret', 'i like turtle']; +app.keys = new KeyGrip(['im a newer secret', 'i like turtle'], 'sha256'); +``` + + These keys may be rotated and are used when signing cookies + with the `{ signed: true }` option: + +```js +this.cookies.set('name', 'tobi', { signed: true }); +``` + ## Handling Requests Koa requests are manipulated using a `Context` object containing both a Koa `Request` and `Response` object. For more information on these view: diff --git a/lib/application.js b/lib/application.js index db7cf6e..1a6c6c2 100644 --- a/lib/application.js +++ b/lib/application.js @@ -10,6 +10,7 @@ var context = require('./context'); var request = require('./request'); var response = require('./response'); var Cookies = require('cookies'); +var Keygrip = require('keygrip'); var Stream = require('stream'); var http = require('http'); var co = require('co'); @@ -96,11 +97,42 @@ app.callback = function(){ return function(req, res, next){ var ctx = self.createContext(req, res); - co.call(ctx, gen)(next || ctx.onerror); } }; +/** + * Set signed cookie keys. + * + * These are passed to [KeyGrip](https://github.com/jed/keygrip), + * however you may also pass your own `KeyGrip` instance. For + * example the following are acceptable: + * + * app.keys = ['im a newer secret', 'i like turtle']; + * app.keys = new KeyGrip(['im a newer secret', 'i like turtle'], 'sha256'); + * + * @param {Array|KeyGrip} keys + * @api public + */ + +app.__defineSetter__('keys', function(keys){ + var ok = Array.isArray(keys) || keys instanceof Keygrip; + if (!ok) throw new TypeError('app.keys must be an array or Keygrip'); + if (!(keys instanceof Keygrip)) keys = new Keygrip(keys); + this._keys = keys; +}); + +/** + * Get `Keygrip` instance. + * + * @return {Keygrip} + * @api public + */ + +app.__defineGetter__('keys', function(){ + return this._keys; +}); + /** * Initialize a new context. * @@ -116,8 +148,8 @@ app.createContext = function(req, res){ context.res = request.res = response.res = res; request.ctx = response.ctx = context; context.onerror = context.onerror.bind(context); - context.cookies = new Cookies(req, res); context.originalUrl = request.originalUrl = req.url; + context.cookies = new Cookies(req, res, this.keys); return context; } diff --git a/package.json b/package.json index 47203d7..193a6a4 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "fresh": "~0.2.0", "negotiator": "~0.3.0", "koa-compose": "~2.0.0", - "cookies": "~0.3.6" + "cookies": "~0.3.6", + "keygrip": "~0.2.4" }, "devDependencies": { "bytes": "~0.2.1", diff --git a/test/context/cookies.js b/test/context/cookies.js new file mode 100644 index 0000000..49e666d --- /dev/null +++ b/test/context/cookies.js @@ -0,0 +1,81 @@ + +var koa = require('../..') +var request = require('supertest'); + +describe('ctx.cookies.set()', function(){ + it('should set an unsigned cookie', function(done){ + var app = koa(); + + app.use(function *(next){ + this.cookies.set('name', 'jon'); + this.status = 204; + }) + + var server = app.listen(); + + request(server) + .get('/') + .expect(204) + .end(function(err, res){ + if (err) return done(err); + + res.headers['set-cookie'].some(function(cookie){ + return /^name=/.test(cookie); + }).should.be.ok; + + done(); + }) + }) + + describe('with .signed', function(){ + describe('when no .keys are set', function(){ + it('should error', function(done){ + var app = koa(); + + app.use(function *(next){ + try { + this.cookies.set('foo', 'bar', { signed: true }); + } catch (err) { + this.body = err.message; + } + }); + + request(app.listen()) + .get('/') + .expect('Cannot call method \'sign\' of undefined', done); + }) + }) + + it('should send a signed cookie', function(done){ + var app = koa(); + + app.keys = ['a', 'b']; + + app.use(function *(next){ + this.cookies.set('name', 'jon', { signed: true }); + this.status = 204; + }) + + var server = app.listen(); + + request(server) + .get('/') + .expect(204) + .end(function(err, res){ + if (err) return done(err); + + var cookies = res.headers['set-cookie']; + + cookies.some(function(cookie){ + return /^name=/.test(cookie); + }).should.be.ok; + + cookies.some(function(cookie){ + return /^name\.sig=/.test(cookie); + }).should.be.ok; + + done(); + }) + }) + }) +}) \ No newline at end of file