From 11d03e0709113bee7861264cbd8ab7115f944606 Mon Sep 17 00:00:00 2001 From: Jonathan Ong Date: Wed, 18 Jun 2014 19:47:09 -0700 Subject: [PATCH 1/2] add and split up raw/encoded/signed/encrypted cookies --- .gitignore | 2 + lib/cookie.js | 38 ++++++++ lib/cookies.js | 219 ++++++++++++++++++++++++---------------------- lib/utils.js | 26 ++++++ package.json | 7 +- test/encoded.js | 41 +++++++++ test/encrypted.js | 101 +++++++++++++++++++++ test/express.js | 86 ------------------ test/http.js | 83 ------------------ test/index.js | 18 ++++ test/raw.js | 149 +++++++++++++++++++++++++++++++ test/restify.js | 86 ------------------ test/signed.js | 139 +++++++++++++++++++++++++++++ 13 files changed, 629 insertions(+), 366 deletions(-) create mode 100644 lib/cookie.js create mode 100644 lib/utils.js create mode 100644 test/encoded.js create mode 100644 test/encrypted.js delete mode 100644 test/express.js delete mode 100644 test/http.js create mode 100644 test/index.js create mode 100644 test/raw.js delete mode 100644 test/restify.js create mode 100644 test/signed.js diff --git a/.gitignore b/.gitignore index 3c3629e..d301fa2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ node_modules +.DS_Store* +npm-debug.log diff --git a/lib/cookie.js b/lib/cookie.js new file mode 100644 index 0000000..4f006d4 --- /dev/null +++ b/lib/cookie.js @@ -0,0 +1,38 @@ + +module.exports = Cookie + +function Cookie(name, value, attrs) { + value || (this.expires = new Date(0)) + + this.name = name + this.value = value || "" + + for (var name in attrs) this[name] = attrs[name] +} + +Cookie.prototype = { + path: "/", + expires: undefined, + domain: undefined, + httpOnly: true, + secure: false, + overwrite: true, + + toString: function() { + return this.name + "=" + this.value + }, + + toHeader: function() { + var header = this.toString() + + if (this.maxage) this.expires = new Date(Date.now() + this.maxage); + + if (this.path ) header += "; path=" + this.path + if (this.expires ) header += "; expires=" + this.expires.toUTCString() + if (this.domain ) header += "; domain=" + this.domain + if (this.secure ) header += "; secure" + if (this.httpOnly ) header += "; httponly" + + return header + } +} diff --git a/lib/cookies.js b/lib/cookies.js index c1b615b..ff4d579 100644 --- a/lib/cookies.js +++ b/lib/cookies.js @@ -1,143 +1,148 @@ + +var base64 = require('base64-url') var Keygrip = require('keygrip') -var http = require('http') -var cache = {} - -function Cookies(request, response, keys) { - if (!(this instanceof Cookies)) return new Cookies(request, response, keys) - - this.request = request - this.response = response - if (keys) { - // array of key strings - if (Array.isArray(keys)) - this.keys = new Keygrip(keys) - // any keygrip constructor to allow different versions - else if (keys.constructor && keys.constructor.name === 'Keygrip') - this.keys = keys - } -} -Cookies.prototype = { - get: function(name, opts) { - var sigName = name + ".sig" - , header, match, value, remote, data, index - , signed = opts && opts.signed !== undefined ? opts.signed : !!this.keys +var Cookie = require('./cookie') +var utils = require('./utils') - header = this.request.headers["cookie"] - if (!header) return +var getPattern = utils.getPattern +var pushCookie = utils.pushCookie +var merge = utils.merge - match = header.match(getPattern(name)) - if (!match) return +module.exports = function (options, keys) { + if (Array.isArray(options) + || (options && options.constructor && options.constructor.name === 'Keygrip')) { + keys = options + options = {} + } - value = match[1] - if (!opts || !signed) return value + options = options || {} + keys = keys || options.keys + if (Array.isArray(keys)) keys = new Keygrip(keys) - remote = this.get(sigName) - if (!remote) return + function Cookies(req, res, next) { + if (!(this instanceof Cookies)) return new Cookies(req, res, next) - data = name + "=" + value - if (!this.keys) throw new Error('.keys required for signed cookies'); - index = this.keys.index(data, remote) + this.req = req + this.res = res - if (index < 0) { - this.set(sigName, null, {path: "/", signed: false }) - } else { - index && this.set(sigName, this.keys.sign(data), { signed: false }) - return value + // middleware support + if (typeof next === 'function') { + req.cookies = res.cookies = this + next() } - }, + } + + Cookies.prototype.get = function (name) { + var header = this.req.headers.cookie + if (!header) return + var match = header.match(getPattern(name)) + if (!match) return + return match[1] + } - set: function(name, value, opts) { - var res = this.response - , req = this.request - , headers = res.getHeader("Set-Cookie") || [] - , secure = req.connection.encrypted - , cookie = new Cookie(name, value, opts) - , signed = opts && opts.signed !== undefined ? opts.signed : !!this.keys + Cookies.prototype.set = function (name, value, opts) { + opts = this.extend(opts) + var res = this.res + var headers = res.getHeader('Set-Cookie') || [] + // node.js header sillyness if (typeof headers == "string") headers = [headers] - if (!secure && opts && opts.secure) throw new Error("Cannot send secure cookie over unencrypted socket") - - cookie.secure = secure - if (opts && "secure" in opts) cookie.secure = opts.secure - if (opts && "secureProxy" in opts) cookie.secure = opts.secureProxy + var cookie = new Cookie(name, value, opts) headers = pushCookie(headers, cookie) - if (opts && signed) { - if (!this.keys) throw new Error('.keys required for signed cookies'); - cookie.value = this.keys.sign(cookie.toString()) - cookie.name += ".sig" - headers = pushCookie(headers, cookie) - } + res.setHeader('Set-Cookie', headers) + return this + } + + Cookies.prototype.decode = function (name, buffer) { + var value = this.get(name) + if (!value) return + + var buf = new Buffer(base64.unescape(value), 'base64') + if (buffer) return buf + return buf.toString('utf8') + } - var setHeader = res.set ? http.OutgoingMessage.prototype.setHeader : res.setHeader - setHeader.call(res, 'Set-Cookie', headers) + Cookies.prototype.encode = function (name, value, opts) { + opts = this.extend(opts) + + var digest = base64.escape(new Buffer(value, 'utf8').toString('base64')) + this.set(name, digest, opts) return this } -} -function Cookie(name, value, attrs) { - value || (this.expires = new Date(0)) + Cookies.prototype.unsign = function (name, opts) { + opts = this.extend(opts) + assertKeys() - this.name = name - this.value = value || "" + var value = opts.encoded + ? this.decode(name + '.b64') + : this.get(name) + if (!value) return - for (var name in attrs) this[name] = attrs[name] -} + var remote = this.decode(name + '.sig', true) + if (!remote) return // no signature -Cookie.prototype = { - path: "/", - expires: undefined, - domain: undefined, - httpOnly: true, - secure: false, - overwrite: false, + var data = name + '=' + value + var index = keys.index(data, remote) - toString: function() { - return this.name + "=" + this.value - }, + // invalid signature, so we clear it + if (index < 0) return this.clear(name + '.sig', opts) + + // update the signature to the latest key + // to do: update the original cookie as well + if (index > 0) this.encode(name + '.sig', data, opts) + return value + } + + Cookies.prototype.sign = function (name, value, opts) { + opts = this.extend(opts) + assertKeys() + + if (opts.encoded) this.encode(name + '.b64', value, opts) + else this.set(name, value, opts) + this.encode(name + '.sig', keys.sign(name + '=' + value), opts) + return this + } - toHeader: function() { - var header = this.toString() + Cookies.prototype.decrypt = function (name, opts) { + opts = this.extend(opts) + assertKeys() - if (this.maxage) this.expires = new Date(Date.now() + this.maxage); + var value = this.decode(name + '.enc', true) + if (!value) return - if (this.path ) header += "; path=" + this.path - if (this.expires ) header += "; expires=" + this.expires.toUTCString() - if (this.domain ) header += "; domain=" + this.domain - if (this.secure ) header += "; secure" - if (this.httpOnly ) header += "; httponly" + var msg = keys.decrypt(value) + if (!msg) return // bad decryption - return header + // re-encrypt if not using the latest key + if (msg[1] > 0) this.encrypt(name, msg[0], opts) + return msg[0].toString('utf8') } -} -function getPattern(name) { - if (cache[name]) return cache[name] + Cookies.prototype.encrypt = function (name, value, opts) { + opts = this.extend(opts) + assertKeys() - return cache[name] = new RegExp( - "(?:^|;) *" + - name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&") + - "=([^;]*)" - ) -} + this.encode(name + '.enc', keys.encrypt(value), opts) + return this + } -function pushCookie(cookies, cookie) { - if (cookie.overwrite) { - cookies = cookies.filter(function(c) { return c.indexOf(cookie.name+'=') !== 0 }) + // clear a cookie + Cookies.prototype.clear = function (name, opts) { + this.set(name, null, opts) } - cookies.push(cookie.toHeader()) - return cookies -} -Cookies.connect = Cookies.express = function(keys) { - return function(req, res, next) { - req.cookies = res.cookies = new Cookies(req, res, keys) - next() + // inherit the options from the main options + Cookies.prototype.extend = function (opts) { + return merge(Object.create(options), opts || {}) } -} -Cookies.Cookie = Cookie + return Cookies -module.exports = Cookies + function assertKeys() { + if (!keys) throw new Error('.keys required for signed cookies') + } +} diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..dc47e06 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,26 @@ + +var cache = {} +exports.getPattern = function getPattern(name) { + if (cache[name]) return cache[name] + + return cache[name] = new RegExp( + "(?:^|;) *" + + name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&") + + "=([^;]*)" + ) +} + +exports.pushCookie = function pushCookie(cookies, cookie) { + if (cookie.overwrite) { + cookies = cookies.filter(function(c) { + return c.indexOf(cookie.name+'=') !== 0 + }) + } + cookies.push(cookie.toHeader()) + return cookies +} + +exports.merge = function merge(dest, src) { + for (var name in src) dest[name] = src[name] + return dest +} diff --git a/package.json b/package.json index 1d14650..9f05779 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,10 @@ "description": "Cookies, optionally signed using Keygrip.", "main": "./lib/cookies", "dependencies": { - "keygrip": "~1.0.0" + "keygrip": "~2.0.0", + "base64-url": "~1.0.0" }, "devDependencies": { - "express": "*", - "restify": "*", "supertest": "0", "mocha": "1" }, @@ -19,6 +18,6 @@ "author": "Jed Schmidt (http://jed.is)", "repository": "expressjs/cookies", "scripts": { - "test": "mocha --reporter spec" + "test": "mocha --reporter spec test/index.js" } } diff --git a/test/encoded.js b/test/encoded.js new file mode 100644 index 0000000..3b1af7e --- /dev/null +++ b/test/encoded.js @@ -0,0 +1,41 @@ + +describe('Base64 Encoded Cookies', function () { + describe('Getting/Setting', function () { + var string = "something \n really \n crazy" + var cookie + + before(function () { + serve(function (req, res) { + var cookies = Cookies(req, res) + + if (req.method === 'POST') { + cookies.encode('unsigned', string) + } else { + res.end(cookies.decode('unsigned')) + } + + res.end() + }) + }) + + it('should set an encoded cookie', function (done) { + request(server) + .post('/') + .expect('Set-Cookie', /^unsigned=[\w-]+; path=\/; httponly$/) + .expect(200, function (err, res) { + if (err) return done(err) + + cookie = res.headers['set-cookie'][0] + done() + }) + }) + + it('should get an encoded cookie', function (done) { + request(server) + .get('/') + .set('Cookie', cookie) + .expect(200) + .expect(string, done) + }) + }) +}) diff --git a/test/encrypted.js b/test/encrypted.js new file mode 100644 index 0000000..39c754d --- /dev/null +++ b/test/encrypted.js @@ -0,0 +1,101 @@ + +describe('Encrypted Cookies', function () { + describe('Getting/Setting', function () { + var string = "something \n really \n crazy" + var cookie + + before(function () { + serve(function (req, res) { + var cookies = Cookies(req, res) + + if (req.method === 'POST') { + cookies.encrypt('unsigned', string) + } else { + res.end(cookies.decrypt('unsigned')) + } + + res.end() + }) + }) + + it('should set an encrypted cookie', function (done) { + request(server) + .post('/') + .expect('Set-Cookie', /^unsigned\.enc=[\w-]+; path=\/; httponly$/) + .expect(200, function (err, res) { + if (err) return done(err) + + cookie = res.headers['set-cookie'][0] + done() + }) + }) + + it('should get an encrypted cookie', function (done) { + request(server) + .get('/') + .set('Cookie', cookie) + .expect(200) + .expect(string, done) + }) + }) + + describe('Rotation', function () { + it('should rotate old keys', function (done) { + var cookie + + serve(function (req, res) { + var cookies = require('..')(['qwer'])(req, res) + cookies.encrypt('foo', 'bar') + res.end() + }) + + request(server) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + + cookie = res.headers['set-cookie'][0] + assert(cookie) + + serve(function (req, res) { + var cookies = Cookies(req, res) + res.end(cookies.decrypt('foo')) + }) + + request(server) + .get('/') + .set('Cookie', cookie) + .expect(200) + .expect('Set-Cookie', /^foo\.enc=[\w-]+; path=\/; httponly$/) + .expect('bar', done) + }) + }) + }) + + describe('Invalid', function () { + it('should return nothing if the message is invalid', function (done) { + serve(function (req, res) { + var cookies = Cookies(req, res) + if (req.method === 'POST') { + cookies.set('foo.enc', 'asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf') + } else { + res.end(cookies.decrypt('foo') || '') + } + + res.end() + }) + + request(server) + .post('/') + .expect(200, function (err, res) { + if (err) return done(err) + + request(server) + .get('/') + .set('Cookie', res.headers['set-cookie'].join(';')) + .expect(200) + .expect('', done) + }) + }) + }) +}) diff --git a/test/express.js b/test/express.js deleted file mode 100644 index eb4d6ca..0000000 --- a/test/express.js +++ /dev/null @@ -1,86 +0,0 @@ - -var assert = require( "assert" ) - , express = require( "express" ) - , http = require( "http" ) - , keys = require( "keygrip" )(['a', 'b']) - , cookies = require( "../" ).express - , request = require('supertest') - -describe('Express', function () { - var server - var header - - before(function setup() { - var app = express() - - app.use( cookies( keys ) ) - - app.get( "/set", function(req, res) { - res.cookies - // set a regular cookie - .set( "unsigned", "foo", { signed:false, httpOnly: false } ) - - // set a signed cookie - .set( "signed", "bar", { signed: true } ) - - // mimic a signed cookie, but with a bogus signature - .set( "tampered", "baz" ) - .set( "tampered.sig", "bogus" ) - - // set a cookie that will be overwritten - .set( "overwrite", "old-value", { signed: true } ) - .set( "overwrite", "new-value", { overwrite: true, signed: true } ) - - res.writeHead(302, {Location: "/"}) - res.end() - }) - - app.get("/", function(req, res) { - var unsigned = req.cookies.get( "unsigned" ) - , signed = req.cookies.get( "signed", { signed: true } ) - , tampered = req.cookies.get( "tampered", { signed: true } ) - , overwrite = req.cookies.get( "overwrite", { signed: true } ) - - assert.equal( unsigned, "foo" ) - assert.equal( req.cookies.get( "unsigned.sig", { signed:false } ), undefined) - assert.equal( signed, "bar" ) - assert.equal( req.cookies.get( "signed.sig", { signed: false } ), keys.sign('signed=bar') ) - assert.notEqual( tampered, "baz" ) - assert.equal( tampered, undefined ) - assert.equal( overwrite, "new-value" ) - assert.equal( req.cookies.get( "overwrite.sig", { signed:false } ), keys.sign('overwrite=new-value') ) - - assert.equal(res.getHeader('Set-Cookie'), 'tampered.sig=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; httponly') - - res.send( - "unsigned expected: foo\n" + - "unsigned actual: " + unsigned + "\n\n" + - "signed expected: bar\n" + - "signed actual: " + signed + "\n\n" + - "tampered expected: undefined\n"+ - "tampered: " + tampered + "\n" - ) - }) - - server = require('http').createServer(app).listen() - }) - - it('should set cookies', function (done) { - request(server) - .get('/set') - .expect(302, function (err, res) { - if (err) return done(err) - - header = res.headers['set-cookie'] - assert.equal(header.length, 7) - done() - }) - }) - - it('should get cookies', function (done) { - request(server) - .get('/') - .set('Cookie', header.join(';')) - .expect(200, done) - }) -}) diff --git a/test/http.js b/test/http.js deleted file mode 100644 index cbb2d50..0000000 --- a/test/http.js +++ /dev/null @@ -1,83 +0,0 @@ - -var assert = require( "assert" ) - , http = require( "http" ) - , keys = require( "keygrip" )(['a', 'b']) - , Cookies = require( "../" ) - , request = require('supertest') - -describe('HTTP', function () { - var server - var header - - before(function setup() { - server = http.createServer( function( req, res ) { - var cookies = new Cookies( req, res, keys ) - , unsigned, signed, tampered, overwrite - - if ( req.url == "/set" ) { - cookies - // set a regular cookie - .set( "unsigned", "foo", { signed:false, httpOnly: false } ) - - // set a signed cookie - .set( "signed", "bar", { signed: true } ) - - // mimic a signed cookie, but with a bogus signature - .set( "tampered", "baz" ) - .set( "tampered.sig", "bogus" ) - - // set a cookie that will be overwritten - .set( "overwrite", "old-value", { signed: true } ) - .set( "overwrite", "new-value", { overwrite: true, signed: true } ) - - res.writeHead( 302, { "Location": "/" } ) - return res.end( "Now let's check." ) - } - - unsigned = cookies.get( "unsigned" ) - signed = cookies.get( "signed", { signed: true } ) - tampered = cookies.get( "tampered", { signed: true } ) - overwrite = cookies.get( "overwrite", { signed: true } ) - - assert.equal( unsigned, "foo" ) - assert.equal( cookies.get( "unsigned.sig", { signed:false } ), undefined) - assert.equal( signed, "bar" ) - assert.equal( cookies.get( "signed.sig", { signed: false } ), keys.sign('signed=bar') ) - assert.notEqual( tampered, "baz" ) - assert.equal( tampered, undefined ) - assert.equal( overwrite, "new-value" ) - assert.equal( cookies.get( "overwrite.sig", { signed:false } ), keys.sign('overwrite=new-value') ) - - assert.equal(res.getHeader('Set-Cookie'), 'tampered.sig=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; httponly') - - res.writeHead( 200, { "Content-Type": "text/plain" } ) - res.end( - "unsigned expected: foo\n" + - "unsigned actual: " + unsigned + "\n\n" + - "signed expected: bar\n" + - "signed actual: " + signed + "\n\n" + - "tampered expected: undefined\n"+ - "tampered: " + tampered + "\n" - ) - }).listen() - }) - - it('should set cookies', function (done) { - request(server) - .get('/set') - .expect(302, function (err, res) { - if (err) return done(err) - - header = res.headers['set-cookie'] - assert.equal(header.length, 7) - done() - }) - }) - - it('should get cookies', function (done) { - request(server) - .get('/') - .set('Cookie', header.join(';')) - .expect(200, done) - }) -}) diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..0a24fed --- /dev/null +++ b/test/index.js @@ -0,0 +1,18 @@ + +http = require('http') +assert = require('assert') +request = require('supertest') + +Cookies = require('..')(['asdf', 'qwer']) + +server = null + +serve = function serve(app) { + if (server) server.close() + server = http.createServer(app).listen() +} + +require('./raw') +require('./encoded') +require('./encrypted') +require('./signed') diff --git a/test/raw.js b/test/raw.js new file mode 100644 index 0000000..48c70c4 --- /dev/null +++ b/test/raw.js @@ -0,0 +1,149 @@ + +describe('Raw Cookies', function () { + describe('Getting/Setting', function () { + before(function () { + serve(function (req, res) { + var cookies = Cookies(req, res) + + if (req.method === 'POST') { + cookies.set('unsigned', 'foo') + } else { + res.end(cookies.get('unsigned')) + } + + res.end() + }) + }) + + it('should set an unsigned cookie', function (done) { + request(server) + .post('/') + .expect('Set-Cookie', 'unsigned=foo; path=/; httponly') + .expect(200, done) + }) + + it('should get an unsigned cookie', function (done) { + request(server) + .get('/') + .set('Cookie', 'unsigned=foo; path=/; httponly') + .expect(200) + .expect('foo', done) + }) + }) + + describe('Options', function () { + it('should set httponly by default', function (done) { + serve(function (req, res) { + var cookies = Cookies(req, res) + cookies.set('unsigned', 'foo') + res.end() + }) + + request(server) + .post('/') + .expect('Set-Cookie', 'unsigned=foo; path=/; httponly') + .expect(200, done) + }) + + it('should set optionally set httponly to false', function (done) { + serve(function (req, res) { + var cookies = Cookies(req, res) + cookies.set('unsigned', 'foo', { + httpOnly: false + }) + res.end() + }) + + request(server) + .post('/') + .expect('Set-Cookie', 'unsigned=foo; path=/') + .expect(200, done) + }) + + it('should set optionally set path', function (done) { + serve(function (req, res) { + var cookies = Cookies(req, res) + cookies.set('unsigned', 'foo', { + path: '/whatever' + }) + res.end() + }) + + request(server) + .post('/') + .expect('Set-Cookie', 'unsigned=foo; path=/whatever; httponly') + .expect(200, done) + }) + }) + + describe('Overwriting', function () { + it('should by default overwrite cookies of the same name', function (done) { + serve(function (req, res) { + var cookies = Cookies(req, res) + cookies.set('unsigned', '1') + cookies.set('unsigned', '2') + cookies.set('unsigned', '3') + res.end() + }) + + request(server) + .get('/') + .expect('Set-Cookie', 'unsigned=3; path=/; httponly') + .expect(200, function (err, res) { + if (err) return done(err) + assert.equal(1, res.headers['set-cookie'].length) + done() + }) + }) + + it('should optionally not overwrite cookies', function (done) { + serve(function (req, res) { + var cookies = Cookies(req, res) + var overwrite = { + overwrite: false + } + cookies.set('unsigned', '1', overwrite) + cookies.set('unsigned', '2', overwrite) + cookies.set('unsigned', '3', overwrite) + res.end() + }) + + request(server) + .get('/') + .expect('Set-Cookie', 'unsigned=1; path=/; httponly,unsigned=2; path=/; httponly,unsigned=3; path=/; httponly') + .expect(200, function (err, res) { + if (err) return done(err) + assert.equal(3, res.headers['set-cookie'].length) + done() + }) + }) + }) + + describe('Max Age', function () { + it('should set max age', function (done) { + var date + + serve(function (req, res) { + var cookies = Cookies(req, res) + cookies.set('foo', 'bar', { + maxAge: 30000 + }) + date = new Date(Date.now() + 30000) + res.end() + }) + + request(server) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + + assert.equal( + 'foo=bar; path=/; expires=' + date.toUTCString() + '; httponly', + res.headers['set-cookie'][0] + ) + + done() + }) + }) + }) +}) diff --git a/test/restify.js b/test/restify.js deleted file mode 100644 index 5fdaa02..0000000 --- a/test/restify.js +++ /dev/null @@ -1,86 +0,0 @@ -var assert = require('assert'), - restify = require('restify'), - keys = require('keygrip')(['a', 'b']), - http = require('http'), - Cookies = require('../'), - request = require('supertest') - -describe('Restify', function () { - var header - var server - - before(function setup(done) { - server = restify.createServer() - - server.get('/set', function (req, res) { - setCookies(req, res) - res.json({ status : 'ok'}) - }) - - server.get('/get', function (req, res) { - assertCookies(req, res) - res.send(200) - }) - - server.listen(done) - }) - - it('should set cookies', function (done) { - request(server) - .get('/set') - .expect(200, function (err, res) { - if (err) return done(err) - - header = res.headers['set-cookie'] - assertSetCookieHeader(header) - done() - }) - }) - - it('should get cookies', function (done) { - request(server) - .get('/get') - .set('Cookie', header.join(';')) - .expect(200, done) - }) -}) - -function setCookies(req, res) { - var cookies = new Cookies(req, res, keys) - cookies - .set('unsigned', 'foo', { signed:false, httpOnly: false }) - .set('signed', 'bar', { signed: true }) - .set('tampered', 'baz') - .set('tampered.sig', 'bogus') - .set('overwrite', 'old-value', { signed: true }) - .set('overwrite', 'new-value', { overwrite: true, signed: true }) -} - -function assertCookies(req, res) { - var cookies = new Cookies(req, res, keys) - var unsigned = cookies.get('unsigned'), - signed = cookies.get('signed', { signed: true }), - tampered = cookies.get('tampered', { signed: true }), - overwrite = cookies.get('overwrite', { signed: true }) - - assert.equal(unsigned, 'foo') - assert.equal(cookies.get('unsigned.sig', { signed:false }), undefined) - assert.equal(signed, 'bar') - assert.equal(cookies.get('signed.sig', { signed: false }), keys.sign('signed=bar')) - assert.notEqual(tampered, 'baz') - assert.equal(tampered, undefined) - assert.equal(overwrite, 'new-value') - assert.equal(cookies.get('overwrite.sig', { signed:false }), keys.sign('overwrite=new-value')) - assert.equal(res.getHeader('Set-Cookie'), 'tampered.sig=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; httponly') -} - -function assertSetCookieHeader(header) { - assert.equal(header.length, 7) - assert.equal(header[0], 'unsigned=foo; path=/') - assert.equal(header[1], 'signed=bar; path=/; httponly') - assert.ok(/^signed\.sig=.{27}; path=\/; httponly$/.test(header[2])) - assert.equal(header[3], 'tampered=baz; path=/; httponly') - assert.equal(header[4], 'tampered.sig=bogus; path=/; httponly') - assert.equal(header[5], 'overwrite=new-value; path=/; httponly') - assert.ok(/^overwrite\.sig=.{27}; path=\/; httponly$/.test(header[6])) -} diff --git a/test/signed.js b/test/signed.js new file mode 100644 index 0000000..e3f3236 --- /dev/null +++ b/test/signed.js @@ -0,0 +1,139 @@ + +describe('Signed Cookies', function () { + describe('Getting/Setting', function () { + var string = "bar" + var cookies + + before(function () { + serve(function (req, res) { + var cookies = Cookies(req, res) + + if (req.method === 'POST') { + cookies.sign('foo', string) + } else { + res.end(cookies.unsign('foo')) + } + + res.end() + }) + }) + + it('should set a signed cookie', function (done) { + request(server) + .post('/') + .expect(200, function (err, res) { + if (err) return done(err) + + cookies = res.headers['set-cookie'] + assert.equal('foo=bar; path=/; httponly', cookies[0]) + assert(/^foo.sig=[\w-]+; path=\/; httponly$/.test(cookies[1])) + done() + }) + }) + + it('should get a signed cookie', function (done) { + request(server) + .get('/') + .set('Cookie', cookies.join(';')) + .expect(200) + .expect(string, done) + }) + }) + + describe('Getting/Setting with base64 encoding', function () { + var string = "bar lkajsdfljk !*()@)#*(!@#)" + var cookies + + before(function () { + serve(function (req, res) { + var cookies = Cookies(req, res) + + if (req.method === 'POST') { + cookies.sign('foo', string, { + encoded: true + }) + } else { + res.end(cookies.unsign('foo', { + encoded: true + })) + } + + res.end() + }) + }) + + it('should set a signed cookie', function (done) { + request(server) + .post('/') + .expect(200, function (err, res) { + if (err) return done(err) + + cookies = res.headers['set-cookie'] + assert(/^foo.b64=[\w-]+; path=\/; httponly$/.test(cookies[0])) + assert(/^foo.sig=[\w-]+; path=\/; httponly$/.test(cookies[1])) + done() + }) + }) + + it('should get a signed cookie', function (done) { + request(server) + .get('/') + .set('Cookie', cookies.join(';')) + .expect(200) + .expect(string, done) + }) + }) + + describe('Invalid Signatures', function () { + it('should return nothing when no signature exists', function (done) { + serve(function (req, res) { + var cookies = Cookies(req, res) + if (req.method === 'POST') { + cookies.set('foo', 'bar') + } else { + res.end(cookies.unsign('foo') || '') + } + + res.end() + }) + + request(server) + .post('/') + .expect(200, function (err, res) { + if (err) return done(err) + + request(server) + .get('/') + .set('Cookie', res.headers['set-cookie'].join(';')) + .expect(200) + .expect('', done) + }) + }) + + it('should return nothing when the signature is invalid', function (done) { + serve(function (req, res) { + var cookies = Cookies(req, res) + if (req.method === 'POST') { + cookies.set('foo', 'bar') + cookies.set('foo.sig', 'lkjasdfasdf') + } else { + res.end(cookies.unsign('foo') || '') + } + + res.end() + }) + + request(server) + .post('/') + .expect(200, function (err, res) { + if (err) return done(err) + + request(server) + .get('/') + .set('Cookie', res.headers['set-cookie'].join(';')) + .expect(200) + .expect('', done) + }) + }) + }) +}) From 027c56e9da9683349a320784556471906f319283 Mon Sep 17 00:00:00 2001 From: Fishrock123 Date: Thu, 19 Jun 2014 11:34:36 -0400 Subject: [PATCH 2/2] better keygrip install for dev testing + travis --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9f05779..04a3fb4 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Cookies, optionally signed using Keygrip.", "main": "./lib/cookies", "dependencies": { - "keygrip": "~2.0.0", + "keygrip": "expressjs/keygrip", "base64-url": "~1.0.0" }, "devDependencies": {