From b35418cb38a4c8e1961e5b9f7e90ddef34a2567b Mon Sep 17 00:00:00 2001 From: Aesop Wolf Date: Mon, 13 Apr 2015 16:08:19 -0700 Subject: [PATCH 1/8] initial support for oauth_body_hash on json payloads --- request.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/request.js b/request.js index 201be667d..99ae491fa 100644 --- a/request.js +++ b/request.js @@ -27,6 +27,7 @@ var http = require('http') , OAuth = require('./lib/oauth').OAuth , Multipart = require('./lib/multipart').Multipart , Redirect = require('./lib/redirect').Redirect + , crypto = require('crypto') var safeStringify = helpers.safeStringify , isReadStream = helpers.isReadStream @@ -541,6 +542,44 @@ Request.prototype.init = function (options) { self.path = '/' } + // Add support for oauth_body_hash + // See https://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html + // Only supports JSON payloads at the moment + // ==================== + + // Check for an explicit 'true' value, in case someone is already generating their own hash + if (options.oauth && typeof options.oauth.body_hash === 'boolean') { + // 4.1.1b: OAuth Consumers MUST NOT include an oauth_body_hash parameter on requests with form-encoded request bodies. + // -------------------- + if (self.getHeader('content-type') === 'application/x-www-form-urlencoded') { + throw new Error('oauth_body_hash is not supported with form-encoded request bodies.') + } + else { + // 3.1a: If the OAuth signature method is HMAC-SHA1 or RSA-SHA1, SHA1 [RFC3174] MUST be used as the body hash algorithm. + // -------------------- + var acceptedSignatureMethods = ['HMAC-SHA1', 'RSA-SHA1'] + var index = acceptedSignatureMethods.indexOf(options.oauth.signature_method) + + if (!options.oauth.signature_method || index > -1) { + // 3.2a: The body hash value is calculated by executing the selected hash algorithm over the request body. The request body is the entity body as defined in [RFC2616] section 7.2. If the request does not have an entity body, the hash should be taken over the empty string. + // -------------------- + var shasum = crypto.createHash('sha1') + shasum.update(options.json.toString() || '') + var sha1 = shasum.digest('hex') + + // 3.2b: The calculated body hash value is encoded using Base64 per [RFC4648]. + // -------------------- + // oauth_body_hash_base64 = new Buffer(sha1).toString('base64'); + options.oauth.body_hash = new Buffer(sha1).toString('base64') + } + else { + // 3.1b: If the OAuth signature method is PLAINTEXT, use of this specification provides no security benefit and is NOT RECOMMENDED. + // -------------------- + throw new Error(options.oauth.signature_method + ' signature_method not supported with body_hash signing.') + } + } + } + // Auth must happen last in case signing is dependent on other headers if (options.oauth) { self.oauth(options.oauth) From bcfe1266811fed98904c6ed3c98d8b3c0808af85 Mon Sep 17 00:00:00 2001 From: Aesop Wolf Date: Tue, 14 Apr 2015 10:25:40 -0700 Subject: [PATCH 2/8] oauth_body_hash cleanup/structuring --- lib/oauth.js | 20 ++++++++++++++++++++ request.js | 49 +++++-------------------------------------------- 2 files changed, 25 insertions(+), 44 deletions(-) diff --git a/lib/oauth.js b/lib/oauth.js index fc1cac6d5..2f01cb571 100644 --- a/lib/oauth.js +++ b/lib/oauth.js @@ -4,6 +4,7 @@ var qs = require('qs') , caseless = require('caseless') , uuid = require('node-uuid') , oauth = require('oauth-sign') + , crypto = require('crypto') function OAuth (request) { @@ -57,6 +58,21 @@ OAuth.prototype.buildParams = function (_oauth, uri, method, query, form, qsLib) return oa } +OAuth.prototype.buildBodyHash = function(_oauth, body) { + var acceptedSignatureMethods = ['HMAC-SHA1', 'RSA-SHA1'] + var index = acceptedSignatureMethods.indexOf(_oauth.signature_method) + + if (!_oauth.signature_method || index > -1) { + var shasum = crypto.createHash('sha1') + shasum.update(body || '') + var sha1 = shasum.digest('hex') + + return new Buffer(sha1).toString('base64') + } else { + throw new Error('oauth: ' + _oauth.signature_method + ' signature_method not supported with body_hash signing.') + } +} + OAuth.prototype.concatParams = function (oa, sep, wrap) { wrap = wrap || '' @@ -102,6 +118,10 @@ OAuth.prototype.onRequest = function (_oauth) { 'and content-type \'' + formContentType + '\'') } + if (!form && typeof _oauth.body_hash === 'boolean') { + _oauth.body_hash = this.buildBodyHash(_oauth, this.request.body.toString()) + } + var oa = this.buildParams(_oauth, uri, method, query, form, qsLib) switch (transport) { diff --git a/request.js b/request.js index 99ae491fa..397ef91a9 100644 --- a/request.js +++ b/request.js @@ -27,7 +27,6 @@ var http = require('http') , OAuth = require('./lib/oauth').OAuth , Multipart = require('./lib/multipart').Multipart , Redirect = require('./lib/redirect').Redirect - , crypto = require('crypto') var safeStringify = helpers.safeStringify , isReadStream = helpers.isReadStream @@ -542,49 +541,6 @@ Request.prototype.init = function (options) { self.path = '/' } - // Add support for oauth_body_hash - // See https://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html - // Only supports JSON payloads at the moment - // ==================== - - // Check for an explicit 'true' value, in case someone is already generating their own hash - if (options.oauth && typeof options.oauth.body_hash === 'boolean') { - // 4.1.1b: OAuth Consumers MUST NOT include an oauth_body_hash parameter on requests with form-encoded request bodies. - // -------------------- - if (self.getHeader('content-type') === 'application/x-www-form-urlencoded') { - throw new Error('oauth_body_hash is not supported with form-encoded request bodies.') - } - else { - // 3.1a: If the OAuth signature method is HMAC-SHA1 or RSA-SHA1, SHA1 [RFC3174] MUST be used as the body hash algorithm. - // -------------------- - var acceptedSignatureMethods = ['HMAC-SHA1', 'RSA-SHA1'] - var index = acceptedSignatureMethods.indexOf(options.oauth.signature_method) - - if (!options.oauth.signature_method || index > -1) { - // 3.2a: The body hash value is calculated by executing the selected hash algorithm over the request body. The request body is the entity body as defined in [RFC2616] section 7.2. If the request does not have an entity body, the hash should be taken over the empty string. - // -------------------- - var shasum = crypto.createHash('sha1') - shasum.update(options.json.toString() || '') - var sha1 = shasum.digest('hex') - - // 3.2b: The calculated body hash value is encoded using Base64 per [RFC4648]. - // -------------------- - // oauth_body_hash_base64 = new Buffer(sha1).toString('base64'); - options.oauth.body_hash = new Buffer(sha1).toString('base64') - } - else { - // 3.1b: If the OAuth signature method is PLAINTEXT, use of this specification provides no security benefit and is NOT RECOMMENDED. - // -------------------- - throw new Error(options.oauth.signature_method + ' signature_method not supported with body_hash signing.') - } - } - } - - // Auth must happen last in case signing is dependent on other headers - if (options.oauth) { - self.oauth(options.oauth) - } - if (options.aws) { self.aws(options.aws) } @@ -669,6 +625,11 @@ Request.prototype.init = function (options) { } } + // Auth must happen last in case signing is dependent on other headers + if (options.oauth) { + self.oauth(options.oauth) + } + var protocol = self.proxy && !self.tunnel ? self.proxy.protocol : self.uri.protocol , defaultModules = {'http:':http, 'https:':https} , httpModules = self.httpModules || {} From a17d16dd35de7dc8df5ce7b91c36ab990220d610 Mon Sep 17 00:00:00 2001 From: Aesop Wolf Date: Tue, 21 Apr 2015 16:10:12 -0700 Subject: [PATCH 3/8] added test for body_hash --- lib/oauth.js | 17 +++++++---------- request.js | 2 +- tests/test-oauth.js | 27 +++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/lib/oauth.js b/lib/oauth.js index 2f01cb571..2918fb2d5 100644 --- a/lib/oauth.js +++ b/lib/oauth.js @@ -59,18 +59,15 @@ OAuth.prototype.buildParams = function (_oauth, uri, method, query, form, qsLib) } OAuth.prototype.buildBodyHash = function(_oauth, body) { - var acceptedSignatureMethods = ['HMAC-SHA1', 'RSA-SHA1'] - var index = acceptedSignatureMethods.indexOf(_oauth.signature_method) - - if (!_oauth.signature_method || index > -1) { - var shasum = crypto.createHash('sha1') - shasum.update(body || '') - var sha1 = shasum.digest('hex') - - return new Buffer(sha1).toString('base64') - } else { + if (['HMAC-SHA1', 'RSA-SHA1'].indexOf(_oauth.signature_method || 'HMAC-SHA1') < 0) { throw new Error('oauth: ' + _oauth.signature_method + ' signature_method not supported with body_hash signing.') } + + var shasum = crypto.createHash('sha1') + shasum.update(body || '') + var sha1 = shasum.digest('hex') + + return new Buffer(sha1).toString('base64') } OAuth.prototype.concatParams = function (oa, sep, wrap) { diff --git a/request.js b/request.js index 397ef91a9..1339435df 100644 --- a/request.js +++ b/request.js @@ -541,6 +541,7 @@ Request.prototype.init = function (options) { self.path = '/' } + // Auth must happen last in case signing is dependent on other headers if (options.aws) { self.aws(options.aws) } @@ -625,7 +626,6 @@ Request.prototype.init = function (options) { } } - // Auth must happen last in case signing is dependent on other headers if (options.oauth) { self.oauth(options.oauth) } diff --git a/tests/test-oauth.js b/tests/test-oauth.js index bc0b131ef..7d7a6bf0c 100644 --- a/tests/test-oauth.js +++ b/tests/test-oauth.js @@ -527,3 +527,30 @@ tape('body transport_method with prexisting body params', function(t) { t.end() }) }) + +tape('body_hash integrity check', function(t) { + function getBodyHash(r) { + var body_hash + r.headers.Authorization.slice('OAuth '.length).replace(/,\ /g, ',').split(',').forEach(function(v) { + if (v.slice(0, 'oauth_body_hash="'.length) === 'oauth_body_hash="') { + body_hash = v.slice('oauth_body_hash="'.length, -1) + } + }) + return body_hash + } + + var body_hash = request.post( + { url: 'http://example.com' + , oauth: + { consumer_secret: 'consumer_secret' + , body_hash: true + } + , json: {foo: 'bar'} + }) + + process.nextTick(function() { + t.equal('YTVlNzQ0ZDAxNjQ1NDBkMzNiMWQ3ZWE2MTZjMjhmMmZhOTdlNzU0YQ%3D%3D', getBodyHash(body_hash)) + body_hash.abort() + t.end() + }) +}) From c48eb5d3efa47aa26cf4ca30af85824b32b2d1d8 Mon Sep 17 00:00:00 2001 From: simov Date: Wed, 22 Apr 2015 15:59:54 +0300 Subject: [PATCH 4/8] Simplify oauth_body_hash integrity check test --- tests/test-oauth.js | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/tests/test-oauth.js b/tests/test-oauth.js index 7d7a6bf0c..29f75a96f 100644 --- a/tests/test-oauth.js +++ b/tests/test-oauth.js @@ -529,17 +529,7 @@ tape('body transport_method with prexisting body params', function(t) { }) tape('body_hash integrity check', function(t) { - function getBodyHash(r) { - var body_hash - r.headers.Authorization.slice('OAuth '.length).replace(/,\ /g, ',').split(',').forEach(function(v) { - if (v.slice(0, 'oauth_body_hash="'.length) === 'oauth_body_hash="') { - body_hash = v.slice('oauth_body_hash="'.length, -1) - } - }) - return body_hash - } - - var body_hash = request.post( + var r = request.post( { url: 'http://example.com' , oauth: { consumer_secret: 'consumer_secret' @@ -549,8 +539,9 @@ tape('body_hash integrity check', function(t) { }) process.nextTick(function() { - t.equal('YTVlNzQ0ZDAxNjQ1NDBkMzNiMWQ3ZWE2MTZjMjhmMmZhOTdlNzU0YQ%3D%3D', getBodyHash(body_hash)) - body_hash.abort() + var body_hash = r.headers.Authorization.replace(/.*oauth_body_hash="([^"]+)".*/, '$1') + t.equal('YTVlNzQ0ZDAxNjQ1NDBkMzNiMWQ3ZWE2MTZjMjhmMmZhOTdlNzU0YQ%3D%3D', body_hash) + r.abort() t.end() }) }) From 3f55a40102eee629045d9e62ac2e5ea9171ffef2 Mon Sep 17 00:00:00 2001 From: simov Date: Wed, 22 Apr 2015 16:24:04 +0300 Subject: [PATCH 5/8] Add test for manual built oauth_body_hash --- tests/test-oauth.js | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/test-oauth.js b/tests/test-oauth.js index 29f75a96f..92c195b77 100644 --- a/tests/test-oauth.js +++ b/tests/test-oauth.js @@ -6,6 +6,7 @@ var oauth = require('oauth-sign') , path = require('path') , request = require('../index') , tape = require('tape') + , crypto = require('crypto') function getSignature(r) { var sign @@ -528,7 +529,33 @@ tape('body transport_method with prexisting body params', function(t) { }) }) -tape('body_hash integrity check', function(t) { +tape('body_hash manual built', function(t) { + function buildBodyHash (body) { + var shasum = crypto.createHash('sha1') + shasum.update(body || '') + var sha1 = shasum.digest('hex') + return new Buffer(sha1).toString('base64') + } + + var json = {foo: 'bar'} + var r = request.post( + { url: 'http://example.com' + , oauth: + { consumer_secret: 'consumer_secret' + , body_hash: buildBodyHash(JSON.stringify(json)) + } + , json: json + }) + + process.nextTick(function() { + var body_hash = r.headers.Authorization.replace(/.*oauth_body_hash="([^"]+)".*/, '$1') + t.equal('YTVlNzQ0ZDAxNjQ1NDBkMzNiMWQ3ZWE2MTZjMjhmMmZhOTdlNzU0YQ%3D%3D', body_hash) + r.abort() + t.end() + }) +}) + +tape('body_hash automatic built', function(t) { var r = request.post( { url: 'http://example.com' , oauth: From 5498b463e143dd475e0db75bc856d0e5921ebbff Mon Sep 17 00:00:00 2001 From: Aesop Wolf Date: Fri, 24 Apr 2015 10:08:51 -0700 Subject: [PATCH 6/8] added test for PLAINTEXT oauth_body_hash --- tests/test-oauth.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test-oauth.js b/tests/test-oauth.js index 92c195b77..a4148a9c8 100644 --- a/tests/test-oauth.js +++ b/tests/test-oauth.js @@ -572,3 +572,22 @@ tape('body_hash automatic built', function(t) { t.end() }) }) + +tape('body_hash PLAINTEXT signature_method', function(t) { + try { + request.post( + { url: 'http://example.com' + , oauth: + { consumer_secret: 'consumer_secret' + , body_hash: true + , signature_method: 'PLAINTEXT' + } + , json: {foo: 'bar'} + }) + } catch(e) { + process.nextTick(function() { + t.equal(typeof e, 'object') + t.end() + }) + } +}) From b9c3c432ac1dd0489c86dcbca9527fc6d0804bbe Mon Sep 17 00:00:00 2001 From: Aesop Wolf Date: Fri, 24 Apr 2015 15:11:28 -0700 Subject: [PATCH 7/8] refactored test for oauth_body_hash PLAINTEXT signature_method --- tests/test-oauth.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test-oauth.js b/tests/test-oauth.js index a4148a9c8..2da346c10 100644 --- a/tests/test-oauth.js +++ b/tests/test-oauth.js @@ -574,7 +574,7 @@ tape('body_hash automatic built', function(t) { }) tape('body_hash PLAINTEXT signature_method', function(t) { - try { + t.throws(function() { request.post( { url: 'http://example.com' , oauth: @@ -583,11 +583,10 @@ tape('body_hash PLAINTEXT signature_method', function(t) { , signature_method: 'PLAINTEXT' } , json: {foo: 'bar'} - }) - } catch(e) { - process.nextTick(function() { - t.equal(typeof e, 'object') + }, function () { + t.fail('body_hash is not allowed with PLAINTEXT signature_method') t.end() }) - } + }, /oauth: PLAINTEXT signature_method not supported with body_hash signing/) + t.end() }) From e2fa5e9278e9f25b57d865167c263c271e629079 Mon Sep 17 00:00:00 2001 From: simov Date: Tue, 28 Apr 2015 10:36:01 +0300 Subject: [PATCH 8/8] Add OAuth body_hash to the docs --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index abcf7feb8..fcda9ff4a 100644 --- a/README.md +++ b/README.md @@ -435,6 +435,10 @@ section of the oauth1 spec: options object. * `transport_method` defaults to `'header'` +To use [Request Body Hash](https://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html) you can either +* Manually generate the body hash and pass it as a string `body_hash: '...'` +* Automatically generate the body hash by passing `body_hash: true` + [back to top](#table-of-contents)