diff --git a/lib/hawk.js b/lib/hawk.js new file mode 100644 index 000000000..de48a9851 --- /dev/null +++ b/lib/hawk.js @@ -0,0 +1,89 @@ +'use strict' + +var crypto = require('crypto') + +function randomString (size) { + var bits = (size + 1) * 6 + var buffer = crypto.randomBytes(Math.ceil(bits / 8)) + var string = buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') + return string.slice(0, size) +} + +function calculatePayloadHash (payload, algorithm, contentType) { + var hash = crypto.createHash(algorithm) + hash.update('hawk.1.payload\n') + hash.update((contentType ? contentType.split(';')[0].trim().toLowerCase() : '') + '\n') + hash.update(payload || '') + hash.update('\n') + return hash.digest('base64') +} + +exports.calculateMac = function (credentials, opts) { + var normalized = 'hawk.1.header\n' + + opts.ts + '\n' + + opts.nonce + '\n' + + (opts.method || '').toUpperCase() + '\n' + + opts.resource + '\n' + + opts.host.toLowerCase() + '\n' + + opts.port + '\n' + + (opts.hash || '') + '\n' + + if (opts.ext) { + normalized = normalized + opts.ext.replace('\\', '\\\\').replace('\n', '\\n') + } + + normalized = normalized + '\n' + + if (opts.app) { + normalized = normalized + opts.app + '\n' + (opts.dlg || '') + '\n' + } + + var hmac = crypto.createHmac(credentials.algorithm, credentials.key).update(normalized) + var digest = hmac.digest('base64') + return digest +} + +exports.header = function (uri, method, opts) { + var timestamp = opts.timestamp || Math.floor((Date.now() + (opts.localtimeOffsetMsec || 0)) / 1000) + var credentials = opts.credentials + if (!credentials || !credentials.id || !credentials.key || !credentials.algorithm) { + return '' + } + + if (['sha1', 'sha256'].indexOf(credentials.algorithm) === -1) { + return '' + } + + var artifacts = { + ts: timestamp, + nonce: opts.nonce || randomString(6), + method: method, + resource: uri.pathname + (uri.search || ''), + host: uri.hostname, + port: uri.port || (uri.protocol === 'http:' ? 80 : 443), + hash: opts.hash, + ext: opts.ext, + app: opts.app, + dlg: opts.dlg + } + + if (!artifacts.hash && (opts.payload || opts.payload === '')) { + artifacts.hash = calculatePayloadHash(opts.payload, credentials.algorithm, opts.contentType) + } + + var mac = exports.calculateMac(credentials, artifacts) + + var hasExt = artifacts.ext !== null && artifacts.ext !== undefined && artifacts.ext !== '' + var header = 'Hawk id="' + credentials.id + + '", ts="' + artifacts.ts + + '", nonce="' + artifacts.nonce + + (artifacts.hash ? '", hash="' + artifacts.hash : '') + + (hasExt ? '", ext="' + artifacts.ext.replace(/\\/g, '\\\\').replace(/"/g, '\\"') : '') + + '", mac="' + mac + '"' + + if (artifacts.app) { + header = header + ', app="' + artifacts.app + (artifacts.dlg ? '", dlg="' + artifacts.dlg : '') + '"' + } + + return header +} diff --git a/package.json b/package.json index 1c58f20aa..8b1d248b2 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "forever-agent": "~0.6.1", "form-data": "~2.3.1", "har-validator": "~5.0.3", - "hawk": "~6.0.2", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", diff --git a/request.js b/request.js index ff19ca39c..4e343fec0 100644 --- a/request.js +++ b/request.js @@ -6,7 +6,6 @@ var url = require('url') var util = require('util') var stream = require('stream') var zlib = require('zlib') -var hawk = require('hawk') var aws2 = require('aws-sign2') var aws4 = require('aws4') var httpSignature = require('http-signature') @@ -25,6 +24,7 @@ var Querystring = require('./lib/querystring').Querystring var Har = require('./lib/har').Har var Auth = require('./lib/auth').Auth var OAuth = require('./lib/oauth').OAuth +var hawk = require('./lib/hawk') var Multipart = require('./lib/multipart').Multipart var Redirect = require('./lib/redirect').Redirect var Tunnel = require('./lib/tunnel').Tunnel @@ -1426,7 +1426,7 @@ Request.prototype.httpSignature = function (opts) { } Request.prototype.hawk = function (opts) { var self = this - self.setHeader('Authorization', hawk.client.header(self.uri, self.method, opts).field) + self.setHeader('Authorization', hawk.header(self.uri, self.method, opts)) } Request.prototype.oauth = function (_oauth) { var self = this diff --git a/tests/test-hawk.js b/tests/test-hawk.js index 34db8da25..3765908cf 100644 --- a/tests/test-hawk.js +++ b/tests/test-hawk.js @@ -2,27 +2,15 @@ var http = require('http') var request = require('../index') -var hawk = require('hawk') +var hawk = require('../lib/hawk') var tape = require('tape') var assert = require('assert') var server = http.createServer(function (req, res) { - var getCred = function (id, callback) { - assert.equal(id, 'dh37fgj492je') - var credentials = { - key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', - algorithm: 'sha256', - user: 'Steve' - } - return callback(null, credentials) - } - - hawk.server.authenticate(req, getCred, {}, function (err, credentials, attributes) { - res.writeHead(err ? 401 : 200, { - 'Content-Type': 'text/plain' - }) - res.end(err ? 'Shoosh!' : 'Hello ' + credentials.user) + res.writeHead(200, { + 'Content-Type': 'text/plain' }) + res.end(authenticate(req)) }) tape('setup', function (t) { @@ -32,18 +20,124 @@ tape('setup', function (t) { }) }) -tape('hawk', function (t) { - var creds = { - key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', - algorithm: 'sha256', - id: 'dh37fgj492je' - } +var creds = { + key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', + algorithm: 'sha256', + id: 'dh37fgj492je' +} + +tape('hawk-get', function (t) { request(server.url, { hawk: { credentials: creds } }, function (err, res, body) { t.equal(err, null) t.equal(res.statusCode, 200) - t.equal(body, 'Hello Steve') + t.equal(body, 'OK') + t.end() + }) +}) + +tape('hawk-post', function (t) { + request.post({ url: server.url, body: 'hello', hawk: { credentials: creds, payload: 'hello' } }, function (err, res, body) { + t.equal(err, null) + t.equal(res.statusCode, 200) + t.equal(body, 'OK') + t.end() + }) +}) + +tape('hawk-ext', function (t) { + request(server.url, { + hawk: { credentials: creds, ext: 'test' } + }, function (err, res, body) { + t.equal(err, null) + t.equal(res.statusCode, 200) + t.equal(body, 'OK') + t.end() + }) +}) + +tape('hawk-app', function (t) { + request(server.url, { + hawk: { credentials: creds, app: 'test' } + }, function (err, res, body) { + t.equal(err, null) + t.equal(res.statusCode, 200) + t.equal(body, 'OK') + t.end() + }) +}) + +tape('hawk-app+dlg', function (t) { + request(server.url, { + hawk: { credentials: creds, app: 'test', dlg: 'asd' } + }, function (err, res, body) { + t.equal(err, null) + t.equal(res.statusCode, 200) + t.equal(body, 'OK') + t.end() + }) +}) + +tape('hawk-missing-creds', function (t) { + request(server.url, { + hawk: {} + }, function (err, res, body) { + t.equal(err, null) + t.equal(res.statusCode, 200) + t.equal(body, 'FAIL') + t.end() + }) +}) + +tape('hawk-missing-creds-id', function (t) { + request(server.url, { + hawk: { + credentials: {} + } + }, function (err, res, body) { + t.equal(err, null) + t.equal(res.statusCode, 200) + t.equal(body, 'FAIL') + t.end() + }) +}) + +tape('hawk-missing-creds-key', function (t) { + request(server.url, { + hawk: { + credentials: { id: 'asd' } + } + }, function (err, res, body) { + t.equal(err, null) + t.equal(res.statusCode, 200) + t.equal(body, 'FAIL') + t.end() + }) +}) + +tape('hawk-missing-creds-algo', function (t) { + request(server.url, { + hawk: { + credentials: { key: '123', id: '123' } + } + }, function (err, res, body) { + t.equal(err, null) + t.equal(res.statusCode, 200) + t.equal(body, 'FAIL') + t.end() + }) +}) + +tape('hawk-invalid-creds-algo', function (t) { + request(server.url, { + hawk: { + credentials: { key: '123', id: '123', algorithm: 'xx' } + } + }, function (err, res, body) { + t.equal(err, null) + t.equal(res.statusCode, 200) + t.equal(body, 'FAIL') t.end() }) }) @@ -53,3 +147,41 @@ tape('cleanup', function (t) { t.end() }) }) + +function authenticate (req) { + if (!req.headers.authorization) { + return 'FAIL' + } + + var headerParts = req.headers.authorization.match(/^(\w+)(?:\s+(.*))?$/) + assert.equal(headerParts[1], 'Hawk') + var attributes = {} + headerParts[2].replace(/(\w+)="([^"\\]*)"\s*(?:,\s*|$)/g, function ($0, $1, $2) { attributes[$1] = $2 }) + var hostParts = req.headers.host.split(':') + + const artifacts = { + method: req.method, + host: hostParts[0], + port: (hostParts[1] ? hostParts[1] : (req.connection && req.connection.encrypted ? 443 : 80)), + resource: req.url, + ts: attributes.ts, + nonce: attributes.nonce, + hash: attributes.hash, + ext: attributes.ext, + app: attributes.app, + dlg: attributes.dlg, + mac: attributes.mac, + id: attributes.id + } + + assert.equal(attributes.id, 'dh37fgj492je') + var credentials = { + key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', + algorithm: 'sha256', + user: 'Steve' + } + + const mac = hawk.calculateMac(credentials, artifacts) + assert.equal(mac, attributes.mac) + return 'OK' +}