Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Add nonce support, closes #6

  • Loading branch information...
commit 1f60f2ce836365364b832d477c8bd8bd571fdf13 1 parent ad756f1
@hueniverse authored
Showing with 163 additions and 31 deletions.
  1. +50 −13 lib/index.js
  2. +1 −1  package.json
  3. +112 −17 test/index.js
View
63 lib/index.js
@@ -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('');
+};
+
View
2  package.json
@@ -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",
View
129 test/index.js
@@ -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", 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 ts="1353788437", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"',
+ 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();
});
Please sign in to comment.
Something went wrong with that request. Please try again.