diff --git a/lib/oauth.js b/lib/oauth.js new file mode 100644 index 000000000..3224601cc --- /dev/null +++ b/lib/oauth.js @@ -0,0 +1,121 @@ +'use strict' + +var querystring = require('querystring') + , qs = require('qs') + , caseless = require('caseless') + , uuid = require('node-uuid') + , oauth = require('oauth-sign') + + +exports.buildParams = function (_oauth, uri, method, query, form, qsLib) { + var oa = {} + for (var i in _oauth) { + oa['oauth_' + i] = _oauth[i] + } + if (!oa.oauth_version) { + oa.oauth_version = '1.0' + } + if (!oa.oauth_timestamp) { + oa.oauth_timestamp = Math.floor( Date.now() / 1000 ).toString() + } + if (!oa.oauth_nonce) { + oa.oauth_nonce = uuid().replace(/-/g, '') + } + if (!oa.oauth_signature_method) { + oa.oauth_signature_method = 'HMAC-SHA1' + } + + var consumer_secret_or_private_key = oa.oauth_consumer_secret || oa.oauth_private_key + delete oa.oauth_consumer_secret + delete oa.oauth_private_key + + var token_secret = oa.oauth_token_secret + delete oa.oauth_token_secret + + var realm = oa.oauth_realm + delete oa.oauth_realm + delete oa.oauth_transport_method + + var baseurl = uri.protocol + '//' + uri.host + uri.pathname + var params = qsLib.parse([].concat(query, form, qsLib.stringify(oa)).join('&')) + + oa.oauth_signature = oauth.sign( + oa.oauth_signature_method, + method, + baseurl, + params, + consumer_secret_or_private_key, + token_secret) + + if (realm) { + oa.realm = realm + } + + return oa +} + +exports.concatParams = function (oa, sep, wrap) { + wrap = wrap || '' + + var params = Object.keys(oa).filter(function (i) { + return i !== 'realm' && i !== 'oauth_signature' + }).sort() + + if (oa.realm) { + params.splice(0, 1, 'realm') + } + params.push('oauth_signature') + + return params.map(function (i) { + return i + '=' + wrap + oauth.rfc3986(oa[i]) + wrap + }).join(sep) +} + +exports.oauth = function (args) { + var uri = args.uri || {} + , method = args.method || '' + , headers = caseless(args.headers) + , body = args.body || '' + , _oauth = args.oauth || {} + , qsLib = args.qsLib || qs + + var form + , query + , contentType = headers.get('content-type') || '' + , formContentType = 'application/x-www-form-urlencoded' + , transport = _oauth.transport_method || 'header' + + if (contentType.slice(0, formContentType.length) === formContentType) { + contentType = formContentType + form = body + } + if (uri.query) { + query = uri.query + } + if (transport === 'body' && (method !== 'POST' || contentType !== formContentType)) { + throw new Error('oauth: transport_method of \'body\' requires \'POST\' ' + + 'and content-type \'' + formContentType + '\'') + } + + var oa = this.buildParams(_oauth, uri, method, query, form, qsLib) + + var data + switch (transport) { + case 'header': + data = 'OAuth ' + this.concatParams(oa, ',', '"') + break + + case 'query': + data = (query ? '&' : '?') + this.concatParams(oa, '&') + break + + case 'body': + data = (form ? form + '&' : '') + this.concatParams(oa, '&') + break + + default: + throw new Error('oauth: transport_method invalid') + } + + return {oauth:data, transport:transport} +} diff --git a/request.js b/request.js index 8d7b1ce47..39d8db3ff 100644 --- a/request.js +++ b/request.js @@ -10,7 +10,6 @@ var http = require('http') , zlib = require('zlib') , helpers = require('./lib/helpers') , bl = require('bl') - , oauth = require('oauth-sign') , hawk = require('hawk') , aws = require('aws-sign2') , httpSignature = require('http-signature') @@ -28,6 +27,7 @@ var http = require('http') , isstream = require('isstream') , getProxyFromURI = require('./lib/getProxyFromURI') , Auth = require('./lib/auth').Auth + , oauth = require('./lib/oauth') var safeStringify = helpers.safeStringify , md5 = helpers.md5 @@ -1516,87 +1516,29 @@ Request.prototype.hawk = function (opts) { Request.prototype.oauth = function (_oauth) { var self = this - var form, query, contentType = '', formContentType = 'application/x-www-form-urlencoded' - if (self.hasHeader('content-type') && - self.getHeader('content-type').slice(0, formContentType.length) === formContentType) { - contentType = formContentType - form = self.body - } - if (self.uri.query) { - query = self.uri.query - } - - var transport = _oauth.transport_method || 'header' - if (transport === 'body' && ( - self.method !== 'POST' || contentType !== formContentType)) { - - throw new Error('oauth.transport_method of \'body\' requires \'POST\' ' + - 'and content-type \'' + formContentType + '\'') - } - - delete _oauth.transport_method - - var oa = {} - for (var i in _oauth) { - oa['oauth_' + i] = _oauth[i] - } - if ('oauth_realm' in oa) { - delete oa.oauth_realm - } - if (!oa.oauth_version) { - oa.oauth_version = '1.0' - } - if (!oa.oauth_timestamp) { - oa.oauth_timestamp = Math.floor( Date.now() / 1000 ).toString() - } - if (!oa.oauth_nonce) { - oa.oauth_nonce = uuid().replace(/-/g, '') - } - if (!oa.oauth_signature_method) { - oa.oauth_signature_method = 'HMAC-SHA1' - } - - var consumer_secret_or_private_key = oa.oauth_consumer_secret || oa.oauth_private_key - delete oa.oauth_consumer_secret - delete oa.oauth_private_key - var token_secret = oa.oauth_token_secret - delete oa.oauth_token_secret - - var baseurl = self.uri.protocol + '//' + self.uri.host + self.uri.pathname - var params = self.qsLib.parse([].concat(query, form, self.qsLib.stringify(oa)).join('&')) - - var signature = oauth.sign( - oa.oauth_signature_method, - self.method, - baseurl, - params, - consumer_secret_or_private_key, - token_secret) - - var buildSortedParams = function (sep, wrap) { - wrap = wrap || '' - return Object.keys(oa).sort().map(function (i) { - return i + '=' + wrap + oauth.rfc3986(oa[i]) + wrap - }).join(sep) + sep + 'oauth_signature=' + wrap + oauth.rfc3986(signature) + wrap - } + var result = oauth.oauth({ + uri: self.uri, + method: self.method, + headers: self.headers, + body: self.body, + oauth: _oauth, + qsLib: self.qsLib + }) - if (transport === 'header') { - var realm = _oauth.realm ? 'realm="' + _oauth.realm + '",' : '' - self.setHeader('Authorization', 'OAuth ' + realm + buildSortedParams(',', '"')) - } - else if (transport === 'query') { - self.path += (query ? '&' : '?') + buildSortedParams('&') + if (result.transport === 'header') { + self.setHeader('Authorization', result.oauth) } - else if (transport === 'body') { - self.body = (form ? form + '&' : '') + buildSortedParams('&') + else if (result.transport === 'query') { + self.path += result.oauth } - else { - throw new Error('oauth.transport_method invalid') + else if (result.transport === 'body') { + self.body = result.oauth } return self } + Request.prototype.jar = function (jar) { var self = this var cookies diff --git a/tests/test-oauth.js b/tests/test-oauth.js index b16165617..cfb587f04 100644 --- a/tests/test-oauth.js +++ b/tests/test-oauth.js @@ -451,20 +451,20 @@ tape('query transport_method with qs parameter and existing query string in url' t.notOk(r.headers.Authorization, 'oauth Authorization header should not be present with transport_method \'query\'') t.notOk(r.path.match(/\?&/), 'there should be no ampersand at the beginning of the query') t.equal('OB33pYjWAnf+xtOHN4Gmbdil168=', qs.parse(r.path).oauth_signature) - var matches = r.path.match(/\?(.*?)&(oauth.*$)/) - t.ok(matches, 'regex to split oauth parameters from qs parameters matched successfully') - var qsParams = qs.parse(matches[1]) - var oauthParams = qs.parse(matches[2]) - - var i, paramNames = ['a2', 'a3[0]', 'a3[1]', 'c@', 'b5', 'c2'] - for (i = 0; i < paramNames.length; i++) { - t.ok(qsParams.hasOwnProperty(paramNames[i]), 'Non-oauth query params should be first in query string: ' + paramNames[i]) - } - - paramNames = ['consumer_key', 'nonce', 'timestamp', 'version', 'signature', 'token', 'signature_method'] - for (i = 0; i < paramNames.length; i++) { - var paramName = 'oauth_' + paramNames[i] - t.ok(oauthParams[paramName], 'OAuth query params should be included after request specific parameters: ' + paramName) + + var params = qs.parse(r.path.split('?')[1]) + , keys = Object.keys(params) + + var paramNames = [ + 'a2', 'b5', 'a3[0]', 'a3[1]', 'c@', 'c2', + 'realm', 'oauth_nonce', 'oauth_signature_method', 'oauth_timestamp', + 'oauth_token', 'oauth_version', 'oauth_signature' + ] + + for (var i = 0; i < keys.length; i++) { + t.ok(keys[i] === paramNames[i], + 'Non-oauth query params should be first, ' + + 'OAuth query params should be second in query string') } r.abort()