diff --git a/lib/index.js b/lib/index.js index 604b488..396dbd5 100755 --- a/lib/index.js +++ b/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(''); +}; + diff --git a/package.json b/package.json index c6c88d1..5debe58 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hawk", "description": "HTTP Hawk Authentication Scheme", - "version": "0.0.8", + "version": "0.1.0", "author": "Eran Hammer (http://hueniverse.com)", "contributors": [], "repository": "git://github.com/hueniverse/hawk", diff --git a/test/index.js b/test/index.js index cfac9e4..3cceeb5 100755 --- a/test/index.js +++ b/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", 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(); });