This commit is contained in:
Eran Hammer 2012-12-20 23:01:45 -08:00
Родитель ad756f1dbc
Коммит 1f60f2ce83
3 изменённых файлов: 163 добавлений и 31 удалений

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

@ -7,7 +7,9 @@ var Err = require('./error');
// Declare internals
var internals = {};
var internals = {
randomSource: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
};
// Hawk authentication
@ -41,9 +43,12 @@ var internals = {};
*
* 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.
* 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.
*
* nonceFunc - optional nonce validation function. The function signature is function(nonce, ts, callback)
* where 'callback' must be called using the signature function(err).
*/
exports.authenticate = function (req, credentialsFunc, arg1, arg2) {
@ -51,9 +56,14 @@ exports.authenticate = function (req, credentialsFunc, arg1, arg2) {
var callback = (arg2 ? arg2 : arg1);
var options = (arg2 ? arg1 : {});
// Default options
options.hostHeaderName = (options.hostHeaderName ? options.hostHeaderName.toLowerCase() : 'host');
options.nonceFunc = options.nonceFunc || function (nonce, ts, callback) { return callback(); };
// Check required HTTP headers: host, authentication
var hostHeader = (options.hostHeaderName ? req.headers[options.hostHeaderName.toLowerCase()] : req.headers.host);
var hostHeader = req.headers[options.hostHeaderName];
if (!hostHeader) {
return callback(Err.badRequest('Missing Host header'), null, null);
}
@ -76,6 +86,7 @@ exports.authenticate = function (req, credentialsFunc, arg1, arg2) {
if (!attributes.id ||
!attributes.ts ||
!attributes.nonce ||
!attributes.mac) {
return callback(Err.badRequest('Missing attributes'), null, attributes.ext);
@ -120,21 +131,30 @@ exports.authenticate = function (req, credentialsFunc, arg1, arg2) {
// Calculate MAC
var mac = exports.calculateMAC(credentials.key, credentials.algorithm, attributes.ts, req.method, req.url, host, port, attributes.ext);
var mac = exports.calculateMAC(credentials.key, credentials.algorithm, attributes.ts, attributes.nonce, req.method, req.url, host, port, attributes.ext);
if (mac !== attributes.mac) {
return callback(Err.unauthorized('Bad mac'), credentials, attributes.ext);
}
// Successful authentication
// Check nonce
return callback(null, credentials, attributes.ext);
options.nonceFunc(attributes.nonce, attributes.ts, function (err) {
if (err) {
return callback(Err.unauthorized('Invalid nonce'), credentials, attributes.ext);
}
// Successful authentication
return callback(null, credentials, attributes.ext);
});
});
};
// Calculate the request MAC
exports.calculateMAC = function (key, algorithm, timestamp, method, uri, host, port, ext) {
exports.calculateMAC = function (key, algorithm, timestamp, nonce, method, uri, host, port, ext) {
// Parse request URI
@ -143,6 +163,7 @@ exports.calculateMAC = function (key, algorithm, timestamp, method, uri, host, p
// Construct normalized req string
var normalized = timestamp + '\n' +
nonce + '\n' +
method.toUpperCase() + '\n' +
url.pathname + (url.search || '') + '\n' +
host.toLowerCase() + '\n' +
@ -184,7 +205,7 @@ exports.parseHeader = function (header) {
var attributes = {};
var attributesRegex = /(id|ts|ext|mac)="([^"\\]*)"\s*(?:,\s*|$)/g;
var attributesRegex = /(id|ts|nonce|ext|mac)="([^"\\]*)"\s*(?:,\s*|$)/g;
var verify = headerParts[2].replace(attributesRegex, function ($0, $1, $2) {
if (attributes[$1] === undefined) {
@ -207,7 +228,7 @@ exports.parseHeader = function (header) {
* credentials is an object with the following keys: 'id, 'key', 'algorithm'.
*/
exports.getAuthorizationHeader = function (credentials, method, uri, host, port, ext, timestamp) {
exports.getAuthorizationHeader = function (credentials, method, uri, host, port, ext, timestamp, nonce) {
// Check request
@ -222,7 +243,8 @@ exports.getAuthorizationHeader = function (credentials, method, uri, host, port,
// Calculate signature
timestamp = timestamp || Math.floor(((new Date()).getTime() / 1000));
var mac = exports.calculateMAC(credentials.key, credentials.algorithm, timestamp, method, uri, host, port, ext);
nonce = nonce || exports.randomString(6);
var mac = exports.calculateMAC(credentials.key, credentials.algorithm, timestamp, nonce, method, uri, host, port, ext);
if (!mac) {
return '';
@ -230,7 +252,22 @@ exports.getAuthorizationHeader = function (credentials, method, uri, host, port,
// Construct header
var header = 'Hawk id="' + credentials.id + '", ts="' + timestamp + (ext ? '", ext="' + ext : '') + '", mac="' + mac + '"';
var header = 'Hawk id="' + credentials.id + '", ts="' + timestamp + '", nonce="' + nonce + (ext ? '", ext="' + ext : '') + '", mac="' + mac + '"';
return header;
};
// Generate a random string of given size (not for crypto)
exports.randomString = function (size) {
var result = [];
var len = internals.randomSource.length;
for (var i = 0; i < size; ++i) {
result.push(internals.randomSource[Math.floor(Math.random() * len)]);
}
return result.join('');
};

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

@ -1,7 +1,7 @@
{
"name": "hawk",
"description": "HTTP Hawk Authentication Scheme",
"version": "0.0.8",
"version": "0.1.0",
"author": "Eran Hammer <eran@hueniverse.com> (http://hueniverse.com)",
"contributors": [],
"repository": "git://github.com/hueniverse/hawk",

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

@ -40,7 +40,7 @@ describe('Hawk', function () {
credentialsFunc('123456', function (err, credentials) {
req.headers.authorization = Hawk.getAuthorizationHeader(credentials, req.method, req.url, 'example.com', 8080, 'some-app-data', 1353809207);
req.headers.authorization = Hawk.getAuthorizationHeader(credentials, req.method, req.url, 'example.com', 8080, 'some-app-data');
Hawk.authenticate(req, credentialsFunc, {}, function (err, credentials, ext) {
@ -64,7 +64,7 @@ describe('Hawk', function () {
credentialsFunc('123456', function (err, credentials) {
req.headers.authorization = Hawk.getAuthorizationHeader(credentials, req.method, req.url, 'example.com', 8080, 'some-app-data', 1353809207);
req.headers.authorization = Hawk.getAuthorizationHeader(credentials, req.method, req.url, 'example.com', 8080, 'some-app-data');
req.url = '/something/else';
Hawk.authenticate(req, credentialsFunc, {}, function (err, credentials, ext) {
@ -82,7 +82,7 @@ describe('Hawk', function () {
var req = {
headers: {
authorization: 'Hawk id="1", ts="1353788437", mac="lDdDLlWQhgcxTvYgzzLo3EZExog=", ext="hello"',
authorization: 'Hawk id="1", ts="1353788437", nonce="k3j4h2", mac="qrP6b5tiS2CO330rpjUEym/USBM=", ext="hello"',
host: 'example.com:8080'
},
method: 'GET',
@ -101,7 +101,7 @@ describe('Hawk', function () {
var req = {
headers: {
authorization: 'Hawk id="123", ts="1353788437", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="ZPa2zWC3WUAYXrwPzJ3DpF54xjQ2ZDLe8GF1ny6JJFI=", ext="hello"',
host: 'example.com:8080'
},
method: 'GET',
@ -116,6 +116,44 @@ describe('Hawk', function () {
});
});
it('should fail on a replay', function (done) {
var req = {
headers: {
authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="ZPa2zWC3WUAYXrwPzJ3DpF54xjQ2ZDLe8GF1ny6JJFI=", ext="hello"',
host: 'example.com:8080'
},
method: 'GET',
url: '/resource/4?filter=a'
};
var memoryCache = {};
var options = {
nonceFunc: function (nonce, ts, callback) {
if (memoryCache[nonce]) {
return callback(new Error());
}
memoryCache[nonce] = true;
return callback();
}
};
Hawk.authenticate(req, credentialsFunc, options, function (err, credentials, ext) {
expect(err).to.not.exist;
expect(credentials.user).to.equal('steve');
Hawk.authenticate(req, credentialsFunc, options, function (err, credentials, ext) {
expect(err).to.exist;
expect(err.toResponse().payload.message).to.equal('Invalid nonce');
done();
});
});
});
it('should fail on an invalid authentication header: wrong scheme', function (done) {
var req = {
@ -157,7 +195,7 @@ describe('Hawk', function () {
var req = {
headers: {
authorization: 'Hawk id="123", ts="1353788437", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"'
authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"'
},
method: 'GET',
url: '/resource/4?filter=a'
@ -171,11 +209,68 @@ describe('Hawk', function () {
});
});
it('should fail on an missing authorization attribute', function (done) {
it('should fail on an missing authorization attribute (id)', function (done) {
var req = {
headers: {
authorization: 'Hawk ts="1353788437", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
authorization: 'Hawk ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
host: 'example.com:8080'
},
method: 'GET',
url: '/resource/4?filter=a'
};
Hawk.authenticate(req, credentialsFunc, {}, function (err, credentials, ext) {
expect(err).to.exist;
expect(err.toResponse().payload.message).to.equal('Missing attributes');
done();
});
});
it('should fail on an missing authorization attribute (ts)', function (done) {
var req = {
headers: {
authorization: 'Hawk id="123", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
host: 'example.com:8080'
},
method: 'GET',
url: '/resource/4?filter=a'
};
Hawk.authenticate(req, credentialsFunc, {}, function (err, credentials, ext) {
expect(err).to.exist;
expect(err.toResponse().payload.message).to.equal('Missing attributes');
done();
});
});
it('should fail on an missing authorization attribute (nonce)', function (done) {
var req = {
headers: {
authorization: 'Hawk id="123", ts="1353788437", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
host: 'example.com:8080'
},
method: 'GET',
url: '/resource/4?filter=a'
};
Hawk.authenticate(req, credentialsFunc, {}, function (err, credentials, ext) {
expect(err).to.exist;
expect(err.toResponse().payload.message).to.equal('Missing attributes');
done();
});
});
it('should fail on an missing authorization attribute (mac)', function (done) {
var req = {
headers: {
authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", ext="hello"',
host: 'example.com:8080'
},
method: 'GET',
@ -194,7 +289,7 @@ describe('Hawk', function () {
var req = {
headers: {
authorization: 'Hawk id="123", ts="1353788437", x="3", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", x="3", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
host: 'example.com:8080'
},
method: 'GET',
@ -232,7 +327,7 @@ describe('Hawk', function () {
var req = {
headers: {
authorization: 'Hawk id="123", ts="1353788437", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
host: 'example.com:8080:90'
},
method: 'GET',
@ -251,7 +346,7 @@ describe('Hawk', function () {
var req = {
headers: {
authorization: 'Hawk id="123", ts="1353788437", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
host: 'example.com:8080'
},
method: 'GET',
@ -275,7 +370,7 @@ describe('Hawk', function () {
var req = {
headers: {
authorization: 'Hawk id="123", ts="1353788437", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
host: 'example.com:8080'
},
method: 'GET',
@ -299,7 +394,7 @@ describe('Hawk', function () {
var req = {
headers: {
authorization: 'Hawk id="123", ts="1353788437", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
host: 'example.com:8080'
},
method: 'GET',
@ -329,7 +424,7 @@ describe('Hawk', function () {
var req = {
headers: {
authorization: 'Hawk id="123", ts="1353788437", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
host: 'example.com:8080'
},
method: 'GET',
@ -360,7 +455,7 @@ describe('Hawk', function () {
var req = {
headers: {
authorization: 'Hawk id="123", ts="1353788437", mac="/qwS4UjfVWMcU4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcU4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
host: 'example.com:8080'
},
method: 'GET',
@ -391,7 +486,7 @@ describe('Hawk', function () {
it('should return an empty value on unknown algorithm', function (done) {
expect(Hawk.calculateMAC('dasdfasdf', 'hmac-sha-0', Date.now() / 1000, 'GET', '/resource/something', 'example.com', 8080)).to.equal('');
expect(Hawk.calculateMAC('dasdfasdf', 'hmac-sha-0', Date.now() / 1000, 'k3k4j5', 'GET', '/resource/something', 'example.com', 8080)).to.equal('');
done();
});
});
@ -406,8 +501,8 @@ describe('Hawk', function () {
algorithm: 'hmac-sha-256'
};
var header = Hawk.getAuthorizationHeader(credentials, 'POST', '/somewhere/over/the/rainbow', 'example.net', 443, 'Bazinga!', 1353809207);
expect(header).to.equal('Hawk id="123456", ts="1353809207", ext="Bazinga!", mac="LYUkYKYkQsQstqNQHcnAzDXce0oHsmS049rv4EalMb8="');
var header = Hawk.getAuthorizationHeader(credentials, 'POST', '/somewhere/over/the/rainbow', 'example.net', 443, 'Bazinga!', 1353809207, 'Ygvqdz');
expect(header).to.equal('Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ext="Bazinga!", mac="qSK1cZEkqPwE2ttBX8QSXxO+NE3epFMu4tyVpGKjdnU="');
done();
});