From f11626d1a651f505700d26af7bb14fabe7fdf65d Mon Sep 17 00:00:00 2001 From: "Josh C. Wilson" Date: Thu, 5 Mar 2015 06:26:58 -0800 Subject: [PATCH 1/2] breaking change that adds support for receiving credentials.key during server-side nonce verification --- lib/server.js | 44 ++++++++++++++++++++++---------------------- test/message.js | 2 +- test/server.js | 20 +++++++++++++++++--- 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/lib/server.js b/lib/server.js index a803930..c374aed 100755 --- a/lib/server.js +++ b/lib/server.js @@ -16,7 +16,7 @@ var internals = {}; /* req: node's HTTP request object or an object as follows: - + var request = { method: 'GET', url: '/resource/4?a=1&b=2', @@ -24,21 +24,21 @@ var internals = {}; port: 8080, authorization: 'Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", ext="some-app-ext-data", mac="6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE="' }; - + credentialsFunc: required function to lookup the set of Hawk credentials based on the provided credentials id. The credentials include the MAC key, MAC algorithm, and other attributes (such as username) needed by the application. This function is the equivalent of verifying the username and password in Basic authentication. - + var credentialsFunc = function (id, callback) { - + // Lookup credentials in database db.lookup(id, function (err, item) { - + if (err || !item) { return callback(err); } - + var credentials = { // Required key: item.key, @@ -46,27 +46,27 @@ var internals = {}; // Application specific user: item.user }; - + return callback(null, credentials); }); }; - + options: { hostHeaderName: optional header field name, used to override the default 'Host' header when used behind a cache of a proxy. Apache2 changes the value of the 'Host' header while preserving the original (which is what the module must verify) in the 'x-forwarded-host' header field. Only used when passed a node Http.ServerRequest object. - - nonceFunc: optional nonce validation function. The function signature is function(nonce, ts, callback) + + nonceFunc: optional nonce validation function. The function signature is function(key, nonce, ts, callback) where 'callback' must be called using the signature function(err). - + timestampSkewSec: optional number of seconds of permitted clock skew for incoming timestamps. Defaults to 60 seconds. Provides a +/- skew which means actual allowed window is double the number of seconds. - + localtimeOffsetMsec: optional local clock time offset express in a number of milliseconds (positive or negative). Defaults to 0. - + payload: optional payload for validation. The client calculates the hash value and includes it via the 'hash' header attribute. The server always ensures the value provided has been included in the request MAC. When this option is provided, it validates the hash value itself. Validation is done by calculating @@ -85,10 +85,10 @@ var internals = {}; exports.authenticate = function (req, credentialsFunc, options, callback) { callback = Hoek.nextTick(callback); - + // Default options - options.nonceFunc = options.nonceFunc || function (nonce, ts, nonceCallback) { return nonceCallback(); }; // No validation + options.nonceFunc = options.nonceFunc || function (key, nonce, ts, nonceCallback) { return nonceCallback(); }; // No validation options.timestampSkewSec = options.timestampSkewSec || 60; // 60 seconds // Application time @@ -182,7 +182,7 @@ exports.authenticate = function (req, credentialsFunc, options, callback) { // Check nonce - options.nonceFunc(attributes.nonce, attributes.ts, function (err) { + options.nonceFunc(credentials.key, attributes.nonce, attributes.ts, function (err) { if (err) { return callback(Boom.unauthorized('Invalid nonce', 'Hawk'), credentials, artifacts); @@ -325,7 +325,7 @@ exports.authenticateBewit = function (req, credentialsFunc, options, callback) { // Extract bewit - // 1 2 3 4 + // 1 2 3 4 var resource = request.url.match(/^(\/.*)([\?&])bewit\=([^&$]*)(?:&(.+))?$/); if (!resource) { return callback(Boom.unauthorized(null, 'Hawk')); @@ -445,10 +445,10 @@ exports.authenticateBewit = function (req, credentialsFunc, options, callback) { exports.authenticateMessage = function (host, port, message, authorization, credentialsFunc, options, callback) { callback = Hoek.nextTick(callback); - + // Default options - options.nonceFunc = options.nonceFunc || function (nonce, ts, nonceCallback) { return nonceCallback(); }; // No validation + options.nonceFunc = options.nonceFunc || function (key, nonce, ts, nonceCallback) { return nonceCallback(); }; // No validation options.timestampSkewSec = options.timestampSkewSec || 60; // 60 seconds // Application time @@ -456,13 +456,13 @@ exports.authenticateMessage = function (host, port, message, authorization, cred var now = Utils.now(options.localtimeOffsetMsec); // Measure now before any other processing // Validate authorization - + if (!authorization.id || !authorization.ts || !authorization.nonce || !authorization.hash || !authorization.mac) { - + return callback(Boom.badRequest('Invalid authorization')) } @@ -514,7 +514,7 @@ exports.authenticateMessage = function (host, port, message, authorization, cred // Check nonce - options.nonceFunc(authorization.nonce, authorization.ts, function (err) { + options.nonceFunc(credentials.key, authorization.nonce, authorization.ts, function (err) { if (err) { return callback(Boom.unauthorized('Invalid nonce', 'Hawk'), credentials); diff --git a/test/message.js b/test/message.js index b0494cc..69b254c 100755 --- a/test/message.js +++ b/test/message.js @@ -137,7 +137,7 @@ describe('Hawk', function () { var auth = Hawk.client.message('example.com', 8080, 'some message', { credentials: credentials }); expect(auth).to.exist(); - Hawk.server.authenticateMessage('example.com', 8080, 'some message', auth, credentialsFunc, { nonceFunc: function (nonce, ts, callback) { callback (new Error('kaboom')); } }, function (err, credentials) { + Hawk.server.authenticateMessage('example.com', 8080, 'some message', auth, credentialsFunc, { nonceFunc: function (key, nonce, ts, callback) { callback (new Error('kaboom')); } }, function (err, credentials) { expect(err).to.exist(); expect(err.message).to.equal('Invalid nonce'); diff --git a/test/server.js b/test/server.js index 00d7d08..d909c69 100755 --- a/test/server.js +++ b/test/server.js @@ -190,13 +190,13 @@ describe('Hawk', function () { var memoryCache = {}; var options = { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now(), - nonceFunc: function (nonce, ts, callback) { + nonceFunc: function (key, nonce, ts, callback) { - if (memoryCache[nonce]) { + if (memoryCache[key + nonce]) { return callback(new Error()); } - memoryCache[nonce] = true; + memoryCache[key + nonce] = true; return callback(); } }; @@ -970,6 +970,20 @@ describe('Hawk', function () { }); }); }); + + it('errors on nonce collision', function (done) { + + credentialsFunc('123456', function (err, credentials) { + + var auth = Hawk.client.message('example.com', 8080, 'some message', { credentials: credentials }); + Hawk.server.authenticateMessage('example.com', 8080, 'some message', auth, credentialsFunc, {nonceFunc: function (key, nonce, ts, nonceCallback) { nonceCallback(true); }}, function (err, credentials) { + + expect(err).to.exist(); + expect(err.message).to.equal('Invalid nonce'); + done(); + }); + }); + }); }); describe('#authenticatePayloadHash', function () { From 1fed56055bf0ec7fab199caa7674cdda8af8f271 Mon Sep 17 00:00:00 2001 From: "Josh C. Wilson" Date: Mon, 1 Jun 2015 15:53:53 -0700 Subject: [PATCH 2/2] adding test for same nonce but different key situation --- test/server.js | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/test/server.js b/test/server.js index d909c69..6f96890 100755 --- a/test/server.js +++ b/test/server.js @@ -215,6 +215,72 @@ describe('Hawk', function () { }); }); + it('does not error on nonce collision if keys differ', function (done) { + + var reqSteve = { + method: 'GET', + url: '/resource/4?filter=a', + host: 'example.com', + port: 8080, + authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="bXx7a7p1h9QYQNZ8x7QhvDQym8ACgab4m3lVSFn4DBw=", ext="hello"' + }; + + var reqBob = { + method: 'GET', + url: '/resource/4?filter=a', + host: 'example.com', + port: 8080, + authorization: 'Hawk id="456", ts="1353788437", nonce="k3j4h2", mac="LXfmTnRzrLd9TD7yfH+4se46Bx6AHyhpM94hLCiNia4=", ext="hello"' + }; + + var credentialsFunc = function (id, callback) { + + var credentials = { + '123': { + id: id, + key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', + algorithm: (id === '1' ? 'sha1' : 'sha256'), + user: 'steve' + }, + '456': { + id: id, + key: 'xrunpaw3489ruxnpa98w4rxnwerxhqb98rpaxn39848', + algorithm: (id === '1' ? 'sha1' : 'sha256'), + user: 'bob' + } + }; + + return callback(null, credentials[id]); + }; + + var memoryCache = {}; + var options = { + localtimeOffsetMsec: 1353788437000 - Hawk.utils.now(), + nonceFunc: function (key, nonce, ts, callback) { + + if (memoryCache[key + nonce]) { + return callback(new Error()); + } + + memoryCache[key + nonce] = true; + return callback(); + } + }; + + Hawk.server.authenticate(reqSteve, credentialsFunc, options, function (err, credentials, artifacts) { + + expect(err).to.not.exist(); + expect(credentials.user).to.equal('steve'); + + Hawk.server.authenticate(reqBob, credentialsFunc, options, function (err, credentials, artifacts) { + + expect(err).to.not.exist(); + expect(credentials.user).to.equal('bob'); + done(); + }); + }); + }); + it('errors on an invalid authentication header: wrong scheme', function (done) { var req = { @@ -984,6 +1050,7 @@ describe('Hawk', function () { }); }); }); + }); describe('#authenticatePayloadHash', function () {