Skip to content

Commit

Permalink
Merge pull request #1328 from fantapop/master
Browse files Browse the repository at this point in the history
Added lesser used oauth transport methods body and query as described in the OAuth1 spec: http://oauth.net/core/1.0/#consumer_req_param
  • Loading branch information
simov committed Dec 29, 2014
2 parents df447b9 + 15289e6 commit 4455e53
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 9 deletions.
7 changes: 7 additions & 0 deletions README.md
Expand Up @@ -442,6 +442,13 @@ the following changes to the OAuth options object:
* Instead of `consumer_secret`, specify a `private_key` string in
[PEM format](http://how2ssl.com/articles/working_with_pem_files/)
To send OAuth parameters via query params or in a post body as described in The
[Consumer Request Parameters](http://oauth.net/core/1.0/#consumer_req_param)
section of the oauth1 spec:
* Pass `transport_method : 'query'` or `transport_method : 'body'` in the OAuth
options object.
* `transport_method` defaults to `'header'`
## Custom HTTP Headers
HTTP Headers, such as `User-Agent`, can be set in the `options` object.
Expand Down
44 changes: 35 additions & 9 deletions request.js
Expand Up @@ -1635,17 +1635,27 @@ Request.prototype.hawk = function (opts) {

Request.prototype.oauth = function (_oauth) {
var self = this
var form, query
var form, query, contentType = '', formContentType = 'application/x-www-form-urlencoded'

if (self.hasHeader('content-type') &&
self.getHeader('content-type').slice(0, 'application/x-www-form-urlencoded'.length) ===
'application/x-www-form-urlencoded'
) {
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]
Expand Down Expand Up @@ -1683,11 +1693,27 @@ Request.prototype.oauth = function (_oauth) {
consumer_secret_or_private_key,
token_secret)

var realm = _oauth.realm ? 'realm="' + _oauth.realm + '",' : ''
var authHeader = 'OAuth ' + realm +
Object.keys(oa).sort().map(function (i) {return i + '="' + oauth.rfc3986(oa[i]) + '"'}).join(',')
authHeader += ',oauth_signature="' + oauth.rfc3986(signature) + '"'
self.setHeader('Authorization', authHeader)
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
}

if (transport === 'header') {
var realm = _oauth.realm ? 'realm="' + _oauth.realm + '",' : ''
self.setHeader('Authorization', 'OAuth ' + realm + buildSortedParams(',', '"'))
}
else if (transport === 'query') {
self.path += (query ? '&' : '?') + buildSortedParams('&')
}
else if (transport === 'body') {
self.body = (form ? form + '&' : '') + buildSortedParams('&')
}
else {
throw new Error('oauth.transport_method invalid')
}

return self
}
Request.prototype.jar = function (jar) {
Expand Down
209 changes: 209 additions & 0 deletions tests/test-oauth.js
Expand Up @@ -301,3 +301,212 @@ tape('rfc5849 RSA example', function(t) {
t.end()
})
})

tape('invalid transport_method', function(t) {
t.throws(
function () {
request.post(
{ url: 'http://example.com/'
, oauth:
{ transport_method: 'some random string'
}
})
}, /transport_method invalid/)

t.throws(
function () {
request.post(
{ url: 'http://example.com/'
, oauth:
{ transport_method: 'headerquery'
}
})
}, /transport_method invalid/)
t.end()
})

tape('invalid method while using transport_method \'body\'', function(t) {
t.throws(
function () {
request.get(
{ url: 'http://example.com/'
, headers: { 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' }
, oauth:
{ transport_method: 'body'
}
})
}, /requires 'POST'/)
t.end()
})

tape('invalid content-type while using transport_method \'body\'', function(t) {
t.throws(
function () {
request.post(
{ url: 'http://example.com/'
, headers: { 'content-type': 'application/json; charset=UTF-8' }
, oauth:
{ transport_method: 'body'
}
})
}, /requires 'POST'/)
t.end()
})

tape('query transport_method simple url', function(t) {
var r = request.post(
{ url: 'https://api.twitter.com/oauth/access_token'
, oauth:
{ consumer_key: 'GDdmIQH6jhtmLUypg82g'
, nonce: '9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8'
, signature_method: 'HMAC-SHA1'
, token: '8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc'
, timestamp: '1272323047'
, verifier: 'pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY'
, version: '1.0'
, consumer_secret: 'MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98'
, token_secret: 'x6qpRnlEmW9JbQn4PQVVeVG8ZLPEx6A0TOebgwcuA'
, transport_method: 'query'
}
})

process.nextTick(function() {
t.notOk(r.headers.Authorization, 'oauth Authorization header should not be present with transport_method \'query\'')
t.equal(accsign, qs.parse(r.path).oauth_signature)
t.notOk(r.path.match(/\?&/), 'there should be no ampersand at the beginning of the query')
r.abort()
t.end()
})
})

tape('query transport_method with prexisting url params', function(t) {
var r = request.post(
{ url: 'http://example.com/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b'
, oauth:
{ consumer_key: '9djdj82h48djs9d2'
, nonce: '7d8f3e4a'
, signature_method: 'HMAC-SHA1'
, token: 'kkk9d7dh3k39sjv7'
, timestamp: '137131201'
, consumer_secret: 'j49sk3j29djd'
, token_secret: 'dh893hdasih9'
, realm: 'Example'
, transport_method: 'query'
}
, form: {
c2: '',
a3: '2 q'
}
})

process.nextTick(function() {
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)
r.abort()
t.end()
})
})

tape('query transport_method with qs parameter and existing query string in url', function(t) {
var r = request.post(
{ url: 'http://example.com/request?a2=r%20b'
, oauth:
{ consumer_key: '9djdj82h48djs9d2'
, nonce: '7d8f3e4a'
, signature_method: 'HMAC-SHA1'
, token: 'kkk9d7dh3k39sjv7'
, timestamp: '137131201'
, consumer_secret: 'j49sk3j29djd'
, token_secret: 'dh893hdasih9'
, realm: 'Example'
, transport_method: 'query'
}
, qs: {
b5: '=%3D',
a3: ['a', '2 q'],
'c@': '',
c2: ''
}
})

process.nextTick(function() {
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)
}

r.abort()
t.end()
})
})

tape('body transport_method empty body', function(t) {
var r = request.post(
{ url: 'https://api.twitter.com/oauth/access_token'
, headers: { 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' }
, oauth:
{ consumer_key: 'GDdmIQH6jhtmLUypg82g'
, nonce: '9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8'
, signature_method: 'HMAC-SHA1'
, token: '8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc'
, timestamp: '1272323047'
, verifier: 'pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY'
, version: '1.0'
, consumer_secret: 'MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98'
, token_secret: 'x6qpRnlEmW9JbQn4PQVVeVG8ZLPEx6A0TOebgwcuA'
, transport_method: 'body'
}
})

process.nextTick(function() {
t.notOk(r.headers.Authorization, 'oauth Authorization header should not be present with transport_method \'body\'')
t.equal(accsign, qs.parse(r.body.toString()).oauth_signature)
t.notOk(r.body.toString().match(/^&/), 'there should be no ampersand at the beginning of the body')
r.abort()
t.end()
})
})

tape('body transport_method with prexisting body params', function(t) {
var r = request.post(
{ url: 'http://example.com/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b'
, oauth:
{ consumer_key: '9djdj82h48djs9d2'
, nonce: '7d8f3e4a'
, signature_method: 'HMAC-SHA1'
, token: 'kkk9d7dh3k39sjv7'
, timestamp: '137131201'
, consumer_secret: 'j49sk3j29djd'
, token_secret: 'dh893hdasih9'
, realm: 'Example'
, transport_method: 'body'
}
, form: {
c2: '',
a3: '2 q'
}
})

process.nextTick(function() {
t.notOk(r.headers.Authorization, 'oauth Authorization header should not be present with transport_method \'body\'')
t.notOk(r.body.toString().match(/^&/), 'there should be no ampersand at the beginning of the body')
t.equal('OB33pYjWAnf+xtOHN4Gmbdil168=', qs.parse(r.body.toString()).oauth_signature)
r.abort()
t.end()
})
})

0 comments on commit 4455e53

Please sign in to comment.