Skip to content

Commit

Permalink
Replace hawk dependency with a local implemenation (#2943)
Browse files Browse the repository at this point in the history
* Replace hawk dependency with local implementation

* Fix access

* Fix access

* Improve coverage

* Improve coverage

* Improve coverage

* Fix access

* Fix style
  • Loading branch information
hueniverse authored and mikeal committed May 19, 2018
1 parent a7f0a36 commit a6741d4
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 26 deletions.
89 changes: 89 additions & 0 deletions 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
}
1 change: 0 additions & 1 deletion package.json
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions request.js
Expand Up @@ -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')
Expand All @@ -24,6 +23,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
Expand Down Expand Up @@ -1420,7 +1420,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
Expand Down
178 changes: 155 additions & 23 deletions tests/test-hawk.js
Expand Up @@ -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) {
Expand All @@ -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()
})
})
Expand All @@ -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'
}

0 comments on commit a6741d4

Please sign in to comment.