Merge pull request #141 from jcwilson/master

adding support for receiving credentials.key during server-side nonce verification
This commit is contained in:
Eran Hammer 2015-06-13 11:56:09 -07:00
Родитель a4721cc1dc 1fed56055b
Коммит 1e44abb0c3
3 изменённых файлов: 107 добавлений и 26 удалений

Просмотреть файл

@ -16,7 +16,7 @@ var internals = {};
/* /*
req: node's HTTP request object or an object as follows: req: node's HTTP request object or an object as follows:
var request = { var request = {
method: 'GET', method: 'GET',
url: '/resource/4?a=1&b=2', url: '/resource/4?a=1&b=2',
@ -24,21 +24,21 @@ var internals = {};
port: 8080, port: 8080,
authorization: 'Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", ext="some-app-ext-data", mac="6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE="' 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. 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) 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 needed by the application. This function is the equivalent of verifying the username and
password in Basic authentication. password in Basic authentication.
var credentialsFunc = function (id, callback) { var credentialsFunc = function (id, callback) {
// Lookup credentials in database // Lookup credentials in database
db.lookup(id, function (err, item) { db.lookup(id, function (err, item) {
if (err || !item) { if (err || !item) {
return callback(err); return callback(err);
} }
var credentials = { var credentials = {
// Required // Required
key: item.key, key: item.key,
@ -46,27 +46,27 @@ var internals = {};
// Application specific // Application specific
user: item.user user: item.user
}; };
return callback(null, credentials); return callback(null, credentials);
}); });
}; };
options: { options: {
hostHeaderName: optional header field name, used to override the default 'Host' header when used 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 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. 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. 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). 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. 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. 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). localtimeOffsetMsec: optional local clock time offset express in a number of milliseconds (positive or negative).
Defaults to 0. Defaults to 0.
payload: optional payload for validation. The client calculates the hash value and includes it via the 'hash' 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 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 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) { exports.authenticate = function (req, credentialsFunc, options, callback) {
callback = Hoek.nextTick(callback); callback = Hoek.nextTick(callback);
// Default options // 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 options.timestampSkewSec = options.timestampSkewSec || 60; // 60 seconds
// Application time // Application time
@ -182,7 +182,7 @@ exports.authenticate = function (req, credentialsFunc, options, callback) {
// Check nonce // Check nonce
options.nonceFunc(attributes.nonce, attributes.ts, function (err) { options.nonceFunc(credentials.key, attributes.nonce, attributes.ts, function (err) {
if (err) { if (err) {
return callback(Boom.unauthorized('Invalid nonce', 'Hawk'), credentials, artifacts); return callback(Boom.unauthorized('Invalid nonce', 'Hawk'), credentials, artifacts);
@ -325,7 +325,7 @@ exports.authenticateBewit = function (req, credentialsFunc, options, callback) {
// Extract bewit // Extract bewit
// 1 2 3 4 // 1 2 3 4
var resource = request.url.match(/^(\/.*)([\?&])bewit\=([^&$]*)(?:&(.+))?$/); var resource = request.url.match(/^(\/.*)([\?&])bewit\=([^&$]*)(?:&(.+))?$/);
if (!resource) { if (!resource) {
return callback(Boom.unauthorized(null, 'Hawk')); 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) { exports.authenticateMessage = function (host, port, message, authorization, credentialsFunc, options, callback) {
callback = Hoek.nextTick(callback); callback = Hoek.nextTick(callback);
// Default options // 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 options.timestampSkewSec = options.timestampSkewSec || 60; // 60 seconds
// Application time // 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 var now = Utils.now(options.localtimeOffsetMsec); // Measure now before any other processing
// Validate authorization // Validate authorization
if (!authorization.id || if (!authorization.id ||
!authorization.ts || !authorization.ts ||
!authorization.nonce || !authorization.nonce ||
!authorization.hash || !authorization.hash ||
!authorization.mac) { !authorization.mac) {
return callback(Boom.badRequest('Invalid authorization')) return callback(Boom.badRequest('Invalid authorization'))
} }
@ -514,7 +514,7 @@ exports.authenticateMessage = function (host, port, message, authorization, cred
// Check nonce // Check nonce
options.nonceFunc(authorization.nonce, authorization.ts, function (err) { options.nonceFunc(credentials.key, authorization.nonce, authorization.ts, function (err) {
if (err) { if (err) {
return callback(Boom.unauthorized('Invalid nonce', 'Hawk'), credentials); return callback(Boom.unauthorized('Invalid nonce', 'Hawk'), credentials);

Просмотреть файл

@ -137,7 +137,7 @@ describe('Hawk', function () {
var auth = Hawk.client.message('example.com', 8080, 'some message', { credentials: credentials }); var auth = Hawk.client.message('example.com', 8080, 'some message', { credentials: credentials });
expect(auth).to.exist(); 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).to.exist();
expect(err.message).to.equal('Invalid nonce'); expect(err.message).to.equal('Invalid nonce');

Просмотреть файл

@ -190,13 +190,13 @@ describe('Hawk', function () {
var memoryCache = {}; var memoryCache = {};
var options = { var options = {
localtimeOffsetMsec: 1353788437000 - Hawk.utils.now(), 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()); return callback(new Error());
} }
memoryCache[nonce] = true; memoryCache[key + nonce] = true;
return callback(); return callback();
} }
}; };
@ -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) { it('errors on an invalid authentication header: wrong scheme', function (done) {
var req = { var req = {
@ -970,6 +1036,21 @@ 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 () { describe('#authenticatePayloadHash', function () {