Skip to content

Commit

Permalink
Accept array of secrets in addition to a single secret
Browse files Browse the repository at this point in the history
closes #17
  • Loading branch information
dougwilson committed Sep 18, 2015
1 parent 07749f6 commit ad5b071
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 13 deletions.
1 change: 1 addition & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
unreleased
==========

* Accept array of secrets in addition to a single secret
* Fix `JSONCookie` to return `undefined` for non-string arguments
* Fix `signedCookie` to return `undefined` for non-string arguments
* deps: cookie@0.2.2
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ app.use(cookieParser())

### cookieParser(secret, options)

- `secret` a string used for signing cookies. This is optional and if not specified, will not parse signed cookies.
- `secret` a string or array used for signing cookies. This is optional and if not specified, will not parse signed cookies. If a string is provided, this is used as the secret. If an array is provided, an attempt will be made to unsign the cookie with each secret in order.
- `options` an object that is passed to `cookie.parse` as the second option. See [cookie](https://www.npmjs.org/package/cookie) for more information.
- `decode` a function to decode the value of the cookie

Expand All @@ -44,10 +44,14 @@ Given an object, this will iterate over the keys and call `JSONCookie` on each v

Parse a cookie value as a signed cookie. This will return the parsed unsigned value if it was a signed cookie and the signature was valid, otherwise it will return the passed value.

The `secret` argument can be an array or string. If a string is provided, this is used as the secret. If an array is provided, an attempt will be made to unsign the cookie with each secret in order.

### cookieParser.signedCookies(cookies, secret)

Given an object, this will iterate over the keys and check if any value is a signed cookie. If it is a signed cookie and the signature is valid, the key will be deleted from the object and added to the new object that is returned.

The `secret` argument can be an array or string. If a string is provided, this is used as the secret. If an array is provided, an attempt will be made to unsign the cookie with each secret in order.

## Example

```js
Expand Down
44 changes: 32 additions & 12 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,24 @@ module.exports.signedCookies = signedCookies;
* Parse Cookie header and populate `req.cookies`
* with an object keyed by the cookie names.
*
* @param {String} [secret]
* @param {string|array} [secret] A string (or array of strings) representing cookie signing secret(s).
* @param {Object} [options]
* @return {Function}
* @public
*/

function cookieParser(secret, options) {
return function cookieParser(req, res, next) {
if (req.cookies) return next();
if (req.cookies) {
return next();
}

var cookies = req.headers.cookie;
var secrets = !secret || Array.isArray(secret)
? (secret || [])
: [secret];

req.secret = secret;
req.secret = secrets[0];
req.cookies = Object.create(null);
req.signedCookies = Object.create(null);

Expand All @@ -51,8 +57,8 @@ function cookieParser(secret, options) {
req.cookies = cookie.parse(cookies, options);

// parse signed cookies
if (secret) {
req.signedCookies = signedCookies(req.cookies, secret);
if (secrets.length !== 0) {
req.signedCookies = signedCookies(req.cookies, secrets);
req.signedCookies = JSONCookies(req.signedCookies);
}

Expand Down Expand Up @@ -112,7 +118,7 @@ function JSONCookies(obj) {
* Parse a signed cookie string, return the decoded value.
*
* @param {String} str signed cookie string
* @param {String} secret
* @param {string|array} secret
* @return {String} decoded value
* @public
*/
Expand All @@ -122,17 +128,31 @@ function signedCookie(str, secret) {
return undefined;
}

return str.substr(0, 2) === 's:'
? signature.unsign(str.slice(2), secret)
: str;
if (str.substr(0, 2) !== 's:') {
return str;
}

var secrets = !secret || Array.isArray(secret)
? (secret || [])
: [secret];

for (var i = 0; i < secrets.length; i++) {
var val = signature.unsign(str.slice(2), secrets[i]);

if (val !== false) {
return val;
}
}

return false;
}

/**
* Parse signed cookies, returning an object
* containing the decoded key/value pairs,
* while removing the signed key from `obj`.
* Parse signed cookies, returning an object containing the decoded key/value
* pairs, while removing the signed key from obj.
*
* @param {Object} obj
* @param {string|array} secret
* @return {Object}
* @public
*/
Expand Down
46 changes: 46 additions & 0 deletions test/cookieParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ describe('cookieParser()', function(){
})
})

describe('when multiple secrets are given', function () {
it('should populate req.signedCookies', function (done) {
request(createServer(['keyboard cat', 'nyan cat']))
.get('/signed')
.set('Cookie', 'buzz=s:foobar.N5r0C3M8W+IPpzyAJaIddMWbTGfDSO+bfKlZErJ+MeE; fizz=s:foobar.JTCAgiMWsnuZpN3mrYnEUjXlGxmDi4POCBnWbRxse88')
.expect(200, '{"buzz":"foobar","fizz":"foobar"}', done)
})
})

describe('when no secret is given', function () {
var server
before(function () {
Expand Down Expand Up @@ -157,6 +166,29 @@ describe('cookieParser.signedCookie(str, secret)', function () {
it('should return unsigned value for signed string', function () {
assert.strictEqual(cookieParser.signedCookie('s:foobar.N5r0C3M8W+IPpzyAJaIddMWbTGfDSO+bfKlZErJ+MeE', 'keyboard cat'), 'foobar')
})

describe('when secret is an array', function () {
it('should return false for tampered signed string', function () {
assert.strictEqual(cookieParser.signedCookie('s:foobaz.N5r0C3M8W+IPpzyAJaIddMWbTGfDSO+bfKlZErJ+MeE', [
'keyboard cat',
'nyan cat'
]), false)
})

it('should return unsigned value for first secret', function () {
assert.strictEqual(cookieParser.signedCookie('s:foobar.N5r0C3M8W+IPpzyAJaIddMWbTGfDSO+bfKlZErJ+MeE', [
'keyboard cat',
'nyan cat'
]), 'foobar')
})

it('should return unsigned value for second secret', function () {
assert.strictEqual(cookieParser.signedCookie('s:foobar.JTCAgiMWsnuZpN3mrYnEUjXlGxmDi4POCBnWbRxse88', [
'keyboard cat',
'nyan cat'
]), 'foobar')
})
})
})

describe('cookieParser.signedCookies(obj, secret)', function () {
Expand Down Expand Up @@ -204,6 +236,20 @@ describe('cookieParser.signedCookies(obj, secret)', function () {
assert.deepEqual(cookieParser.signedCookies(obj, 'keyboard cat'), { foo: 'foobar' })
assert.deepEqual(obj, { fizz: 'buzz' })
})

describe('when secret is an array', function () {
it('should include unsigned strings for all secrets', function () {
var obj = {
buzz: 's:foobar.N5r0C3M8W+IPpzyAJaIddMWbTGfDSO+bfKlZErJ+MeE',
fizz: 's:foobar.JTCAgiMWsnuZpN3mrYnEUjXlGxmDi4POCBnWbRxse88'
}

assert.deepEqual(cookieParser.signedCookies(obj, [ 'keyboard cat', 'nyan cat' ]), {
buzz: 'foobar',
fizz: 'foobar'
})
})
})
})

function createServer(secret) {
Expand Down

0 comments on commit ad5b071

Please sign in to comment.