hawk
Advanced tools
Comparing version 0.0.8 to 0.1.0
@@ -10,3 +10,5 @@ // Load modules | ||
var internals = {}; | ||
var internals = { | ||
randomSource: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' | ||
}; | ||
@@ -45,5 +47,8 @@ | ||
* | ||
* 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). | ||
*/ | ||
@@ -56,5 +61,10 @@ | ||
// 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) { | ||
@@ -82,2 +92,3 @@ return callback(Err.badRequest('Missing Host header'), null, null); | ||
!attributes.ts || | ||
!attributes.nonce || | ||
!attributes.mac) { | ||
@@ -127,10 +138,19 @@ | ||
var mac = exports.calculateMAC(credentials.key, credentials.algorithm, attributes.ts, req.method, req.url, host, port, attributes.ext); | ||
if (mac !== attributes.mac) { | ||
var mac = exports.calculateMAC(credentials.key, credentials.algorithm, attributes.ts, attributes.nonce, req.method, req.url, host, port, attributes.ext); | ||
if (!exports.fixedTimeComparison(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); | ||
}); | ||
}); | ||
@@ -142,3 +162,3 @@ }; | ||
exports.calculateMAC = function (key, algorithm, timestamp, method, uri, host, port, ext) { | ||
exports.calculateMAC = function (key, algorithm, timestamp, nonce, method, uri, host, port, ext) { | ||
@@ -152,2 +172,3 @@ // Parse request URI | ||
var normalized = timestamp + '\n' + | ||
nonce + '\n' + | ||
method.toUpperCase() + '\n' + | ||
@@ -194,3 +215,3 @@ url.pathname + (url.search || '') + '\n' + | ||
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) { | ||
@@ -207,3 +228,3 @@ | ||
} | ||
return attributes; | ||
@@ -219,3 +240,3 @@ }; | ||
exports.getAuthorizationHeader = function (credentials, method, uri, host, port, ext, timestamp) { | ||
exports.getAuthorizationHeader = function (credentials, method, uri, host, port, ext, timestamp, nonce) { | ||
@@ -235,3 +256,4 @@ // Check request | ||
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); | ||
@@ -244,5 +266,39 @@ if (!mac) { | ||
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(''); | ||
}; | ||
// Compare two strings using fixed time algorithm (to prevent time-based analysis of MAC digest match) | ||
exports.fixedTimeComparison = function (a, b) { | ||
var mismatch = (a.length === b.length ? 0 : 1); | ||
if (mismatch) { | ||
b = a; | ||
} | ||
for (var i = 0, il = a.length; i < il; ++i) { | ||
var ac = a.charCodeAt(i); | ||
var bc = b.charCodeAt(i); | ||
mismatch += (ac === bc ? 0 : 1); | ||
} | ||
return (mismatch === 0); | ||
}; | ||
{ | ||
"name": "hawk", | ||
"description": "HTTP Hawk Authentication Scheme", | ||
"version": "0.0.8", | ||
"version": "0.1.0", | ||
"author": "Eran Hammer <eran@hueniverse.com> (http://hueniverse.com)", | ||
@@ -6,0 +6,0 @@ "contributors": [], |
@@ -6,3 +6,3 @@ ![hawk Logo](https://raw.github.com/hueniverse/hawk/master/images/hawk.png) | ||
Current version: **0.0.x** | ||
Current version: **0.1.0** | ||
@@ -83,3 +83,3 @@ [![Build Status](https://secure.travis-ci.org/hueniverse/hawk.png)](http://travis-ci.org/hueniverse/hawk) | ||
Http.createServer(handler).listen(8000, '127.0.0.1'); | ||
Http.createServer(handler).listen(8000, 'example.com'); | ||
``` | ||
@@ -105,6 +105,6 @@ | ||
var options = { | ||
uri: 'http://127.0.0.1:8000/resource/1?b=1&a=2', | ||
uri: 'http://example.com:8000/resource/1?b=1&a=2', | ||
method: 'GET', | ||
headers: { | ||
authorization: Hawk.getAuthorizationHeader(credentials, 'GET', '/resource/1?b=1&a=2', '127.0.0.1', 8000, 'some-app-data') | ||
authorization: Hawk.getAuthorizationHeader(credentials, 'GET', '/resource/1?b=1&a=2', 'example.com', 8000, 'some-app-data') | ||
} | ||
@@ -127,3 +127,3 @@ }; | ||
GET /resource/1?b=1&a=2 HTTP/1.1 | ||
Host: 127.0.0.1:8000 | ||
Host: example.com:8000 | ||
``` | ||
@@ -141,14 +141,15 @@ | ||
* Key identifier: dh37fgj492je | ||
* Key: werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn | ||
* Algorithm: hmac-sha-256 | ||
* Key identifier: dh37fgj492je | ||
* Key: werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn | ||
* Algorithm: hmac-sha-256 | ||
The client generates the authentication header by calculating a timestamp (e.g. the number of seconds since January 1, | ||
1970 00:00:00 GMT) and constructs the normalized request string (newline separated values): | ||
1970 00:00:00 GMT), generates a nonce, and constructs the normalized request string (newline separated values): | ||
``` | ||
1353832234 | ||
j4h3g2 | ||
GET | ||
/resource/1?b=1&a=2 | ||
127.0.0.1 | ||
example.com | ||
8000 | ||
@@ -162,3 +163,3 @@ some-app-data | ||
``` | ||
/uYWR6W5vTbY3WKUAN6fa+7p1t+1Yl6hFxKeMLfR6kk= | ||
hpf5lg0G0rtKrT04CiRf0Q+IDjkGkyvKdMjtqu1XV/s= | ||
``` | ||
@@ -171,4 +172,4 @@ | ||
GET /resource/1?b=1&a=2 HTTP/1.1 | ||
Host: 127.0.0.1:8000 | ||
Authorization: Hawk id="dh37fgj492je", ts="1353832234", ext="some-app-data", mac="/uYWR6W5vTbY3WKUAN6fa+7p1t+1Yl6hFxKeMLfR6kk=" | ||
Host: example.com:8000 | ||
Authorization: Hawk id="dh37fgj492je", ts="1353832234", ext="some-app-data", mac="hpf5lg0G0rtKrT04CiRf0Q+IDjkGkyvKdMjtqu1XV/s=" | ||
``` | ||
@@ -175,0 +176,0 @@ |
@@ -43,3 +43,3 @@ // Load modules | ||
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'); | ||
@@ -68,3 +68,3 @@ Hawk.authenticate(req, credentialsFunc, {}, function (err, credentials, ext) { | ||
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'; | ||
@@ -87,3 +87,3 @@ | ||
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' | ||
@@ -107,7 +107,7 @@ }, | ||
headers: { | ||
authorization: 'Hawk id="123", ts="1353788437", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"', | ||
host: 'example.com:8080' | ||
authorization: 'Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", mac="hpf5lg0G0rtKrT04CiRf0Q+IDjkGkyvKdMjtqu1XV/s=", ext="some-app-data"', | ||
host: 'example.com:8000' | ||
}, | ||
method: 'GET', | ||
url: '/resource/4?filter=a' | ||
url: '/resource/1?b=1&a=2' | ||
}; | ||
@@ -123,2 +123,40 @@ | ||
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) { | ||
@@ -165,3 +203,3 @@ | ||
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"' | ||
}, | ||
@@ -180,7 +218,7 @@ method: 'GET', | ||
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' | ||
@@ -200,2 +238,59 @@ }, | ||
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', | ||
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 unknown authorization attribute', function (done) { | ||
@@ -205,3 +300,3 @@ | ||
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' | ||
@@ -244,3 +339,3 @@ }, | ||
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' | ||
@@ -264,3 +359,3 @@ }, | ||
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' | ||
@@ -289,3 +384,3 @@ }, | ||
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' | ||
@@ -314,3 +409,3 @@ }, | ||
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' | ||
@@ -345,3 +440,3 @@ }, | ||
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' | ||
@@ -377,3 +472,3 @@ }, | ||
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' | ||
@@ -409,3 +504,3 @@ }, | ||
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(); | ||
@@ -425,4 +520,4 @@ }); | ||
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(); | ||
@@ -456,3 +551,47 @@ }); | ||
}); | ||
describe('#fixedTimeComparison', function () { | ||
var a = Hawk.randomString(50000); | ||
var b = Hawk.randomString(150000); | ||
it('should take the same amount of time comparing different string sizes', function (done) { | ||
var now = Date.now(); | ||
Hawk.fixedTimeComparison(b, a); | ||
var t1 = Date.now() - now; | ||
now = Date.now(); | ||
Hawk.fixedTimeComparison(b, b); | ||
var t2 = Date.now() - now; | ||
expect(t2 - t1).to.be.within(-1, 1); | ||
done(); | ||
}); | ||
it('should return true for equal strings', function (done) { | ||
expect(Hawk.fixedTimeComparison(a, a)).to.equal(true); | ||
done(); | ||
}); | ||
it('should return false for different strings (size, a < b)', function (done) { | ||
expect(Hawk.fixedTimeComparison(a, a + 'x')).to.equal(false); | ||
done(); | ||
}); | ||
it('should return false for different strings (size, a > b)', function (done) { | ||
expect(Hawk.fixedTimeComparison(a + 'x', a)).to.equal(false); | ||
done(); | ||
}); | ||
it('should return false for different strings (size, a = b)', function (done) { | ||
expect(Hawk.fixedTimeComparison(a + 'x', a + 'y')).to.equal(false); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
56558
739
291