Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor OAuth into separate module #1366

Merged
merged 3 commits into from Jan 27, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
121 changes: 121 additions & 0 deletions 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}
}
90 changes: 16 additions & 74 deletions request.js
Expand Up @@ -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')
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
28 changes: 14 additions & 14 deletions tests/test-oauth.js
Expand Up @@ -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()
Expand Down