From 635ee2c1f7d542bd464a0ce231d94569bb353798 Mon Sep 17 00:00:00 2001 From: Andreas Knecht Date: Thu, 20 Dec 2012 10:09:56 +0100 Subject: [PATCH 01/16] Added RSA-SHA1 signature method based on the fork from https://github.com/wraithgar/node-oauth. Added test that uses the RSA-SHA1 method and verifies the signature using a public key. --- lib/oauth.js | 14 ++++++++++++-- tests/oauth.js | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/lib/oauth.js b/lib/oauth.js index 7607ee6..db6f2a9 100644 --- a/lib/oauth.js +++ b/lib/oauth.js @@ -13,6 +13,9 @@ exports.OAuth= function(requestUrl, accessUrl, consumerKey, consumerSecret, vers this._accessUrl= accessUrl; this._consumerKey= consumerKey; this._consumerSecret= this._encodeData( consumerSecret ); + if (signatureMethod == "RSA-SHA1") { + this._privateKey = consumerSecret; + } this._version= version; if( authorize_callback === undefined ) { this._authorize_callback= "oob"; @@ -21,7 +24,7 @@ exports.OAuth= function(requestUrl, accessUrl, consumerKey, consumerSecret, vers this._authorize_callback= authorize_callback; } - if( signatureMethod != "PLAINTEXT" && signatureMethod != "HMAC-SHA1") + if( signatureMethod != "PLAINTEXT" && signatureMethod != "HMAC-SHA1" && signatureMethod != "RSA-SHA1") throw new Error("Un-supported signature method: " + signatureMethod ) this._signatureMethod= signatureMethod; this._nonceSize= nonceSize || 32; @@ -40,9 +43,12 @@ exports.OAuthEcho= function(realm, verify_credentials, consumerKey, consumerSecr this._verifyCredentials = verify_credentials; this._consumerKey= consumerKey; this._consumerSecret= this._encodeData( consumerSecret ); + if (signatureMethod == "RSA-SHA1") { + this._privateKey = consumerSecret; + } this._version= version; - if( signatureMethod != "PLAINTEXT" && signatureMethod != "HMAC-SHA1") + if( signatureMethod != "PLAINTEXT" && signatureMethod != "HMAC-SHA1" && signatureMethod != "RSA-SHA1") throw new Error("Un-supported signature method: " + signatureMethod ); this._signatureMethod= signatureMethod; this._nonceSize= nonceSize || 32; @@ -197,6 +203,10 @@ exports.OAuth.prototype._createSignature= function(signatureBase, tokenSecret) { if( this._signatureMethod == "PLAINTEXT" ) { hash= key; } + else if (this._signatureMethod == "RSA-SHA1") { + key = this._privateKey || ""; + hash= crypto.createSign("RSA-SHA1").update(signatureBase).sign(key, 'base64'); + } else { if( crypto.Hmac ) { hash = crypto.createHmac("sha1", key).update(signatureBase).digest("base64"); diff --git a/tests/oauth.js b/tests/oauth.js index b0a4a90..a002104 100644 --- a/tests/oauth.js +++ b/tests/oauth.js @@ -2,7 +2,8 @@ var vows = require('vows'), assert = require('assert'), events = require('events'), OAuth= require('../lib/oauth').OAuth, - OAuthEcho= require('../lib/oauth').OAuthEcho; + OAuthEcho= require('../lib/oauth').OAuthEcho, + crypto = require('crypto'); var DummyResponse =function( statusCode ) { this.statusCode= statusCode; @@ -23,6 +24,30 @@ DummyRequest.prototype.end= function(){ this.response.emit('end'); } +//Valid RSA keypair used to test RSA-SHA1 signature method +var RsaPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\n" + +"MIICXQIBAAKBgQDizE4gQP5nPQhzof/Vp2U2DDY3UY/Gxha2CwKW0URe7McxtnmE\n" + +"CrZnT1n/YtfrrCNxY5KMP4o8hMrxsYEe05+1ZGFT68ztms3puUxilU5E3BQMhz1t\n" + +"JMJEGcTt8nZUlM4utli7fHgDtWbhvqvYjRMGn3AjyLOfY8XZvnFkGjipvQIDAQAB\n" + +"AoGAKgk6FcpWHOZ4EY6eL4iGPt1Gkzw/zNTcUsN5qGCDLqDuTq2Gmk2t/zn68VXt\n" + +"tVXDf/m3qN0CDzOBtghzaTZKLGhnSewQ98obMWgPcvAsb4adEEeW1/xigbMiaW2X\n" + +"cu6GhZxY16edbuQ40LRrPoVK94nXQpj8p7w4IQ301Sm8PSECQQD1ZlOj4ugvfhEt\n" + +"exi4WyAaM45fylmN290UXYqZ8SYPI/VliDytIlMfyq5Rv+l+dud1XDPrWOQ0ImgV\n" + +"HJn7uvoZAkEA7JhHNmHF9dbdF9Koj86K2Cl6c8KUu7U7d2BAuB6pPkt8+D8+y4St\n" + +"PaCmN4oP4X+sf5rqBYoXywHlqEei2BdpRQJBAMYgR4cZu7wcXGIL8HlnmROObHSK\n" + +"OqN9z5CRtUV0nPW8YnQG+nYOMG6KhRMbjri750OpnYF100kEPmRNI0VKQIECQE8R\n" + +"fQsRleTYz768ahTVQ9WF1ySErMwmfx8gDcD6jjkBZVxZVpURXAwyehopi7Eix/VF\n" + +"QlxjkBwKIEQi3Ks297kCQQCL9by1bueKDMJO2YX1Brm767pkDKkWtGfPS+d3xMtC\n" + +"KJHHCqrS1V+D5Q89x5wIRHKxE5UMTc0JNa554OxwFORX\n" + +"-----END RSA PRIVATE KEY-----"; + +var RsaPublicKey = "-----BEGIN PUBLIC KEY-----\n" + +"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDizE4gQP5nPQhzof/Vp2U2DDY3\n" + +"UY/Gxha2CwKW0URe7McxtnmECrZnT1n/YtfrrCNxY5KMP4o8hMrxsYEe05+1ZGFT\n" + +"68ztms3puUxilU5E3BQMhz1tJMJEGcTt8nZUlM4utli7fHgDtWbhvqvYjRMGn3Aj\n" + +"yLOfY8XZvnFkGjipvQIDAQAB\n" + +"-----END PUBLIC KEY-----"; + vows.describe('OAuth').addBatch({ 'When generating the signature base string described in http://oauth.net/core/1.0/#sig_base_example': { topic: new OAuth(null, null, null, null, null, null, "HMAC-SHA1"), @@ -32,6 +57,20 @@ vows.describe('OAuth').addBatch({ assert.equal( result, "GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jdk%26oauth_version%3D1.0%26size%3Doriginal"); } }, + 'When generating the signature with RSA-SHA1': { + topic: new OAuth(null, null, null, RsaPrivateKey, null, null, "RSA-SHA1"), + 'we get a valid oauth signature': function (oa) { + var signatureBase = "GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DRSA-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jdk%26oauth_version%3D1.0%26size%3Doriginal"; + var oauthSignature = oa._createSignature(signatureBase, "xyz4992k83j47x0b"); + + assert.equal( oauthSignature, "qS4rhWog7GPgo4ZCJvUdC/1ZAax/Q4Ab9yOBvgxSopvmKUKp5rso+Zda46GbyN2hnYDTiA/g3P/d/YiPWa454BEBb/KWFV83HpLDIoqUUhJnlXX9MqRQQac0oeope4fWbGlfTdL2PXjSFJmvfrzybERD/ZufsFtVrQKS3QBpYiw="); + + //now check that given the public key we can verify this signature + var verifier = crypto.createVerify("RSA-SHA1").update(signatureBase); + var valid = verifier.verify(RsaPublicKey, oauthSignature, 'base64'); + assert.ok( valid, "Signature could not be verified with RSA public key"); + } + }, 'When generating the signature base string with PLAINTEXT': { topic: new OAuth(null, null, null, null, null, null, "PLAINTEXT"), 'we get the expected result string': function (oa) { From 6e215f9ad7632b1f54b2339f80fe2b6552253598 Mon Sep 17 00:00:00 2001 From: Pieter Joost van de Sande Date: Fri, 12 Apr 2013 13:24:15 +0200 Subject: [PATCH 02/16] Add failing test for 301 redirect for followRedirect client option --- tests/oauth.js | 136 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 104 insertions(+), 32 deletions(-) diff --git a/tests/oauth.js b/tests/oauth.js index b0a4a90..7db9836 100644 --- a/tests/oauth.js +++ b/tests/oauth.js @@ -21,13 +21,13 @@ DummyRequest.prototype.write= function(post_body){ } DummyRequest.prototype.end= function(){ this.response.emit('end'); -} +} vows.describe('OAuth').addBatch({ 'When generating the signature base string described in http://oauth.net/core/1.0/#sig_base_example': { topic: new OAuth(null, null, null, null, null, null, "HMAC-SHA1"), 'we get the expected result string': function (oa) { - var result= oa._createSignatureBase("GET", "http://photos.example.net/photos", + var result= oa._createSignatureBase("GET", "http://photos.example.net/photos", "file=vacation.jpg&oauth_consumer_key=dpf43f3p2l4k3l03&oauth_nonce=kllo9940pd9333jh&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk&oauth_version=1.0&size=original") assert.equal( result, "GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jdk%26oauth_version%3D1.0%26size%3Doriginal"); } @@ -35,7 +35,7 @@ vows.describe('OAuth').addBatch({ 'When generating the signature base string with PLAINTEXT': { topic: new OAuth(null, null, null, null, null, null, "PLAINTEXT"), 'we get the expected result string': function (oa) { - var result= oa._getSignature("GET", "http://photos.example.net/photos", + var result= oa._getSignature("GET", "http://photos.example.net/photos", "file=vacation.jpg&oauth_consumer_key=dpf43f3p2l4k3l03&oauth_nonce=kllo9940pd9333jh&oauth_signature_method=PLAINTEXT&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk&oauth_version=1.0&size=original", "test"); assert.equal( result, "&test"); @@ -58,7 +58,7 @@ vows.describe('OAuth').addBatch({ topic: new OAuth(null, null, null, null, null, null, "HMAC-SHA1"), 'flatten out arguments that are arrays' : function(oa) { var parameters= {"z": "a", - "a": ["1", "2"], + "a": ["1", "2"], "1": "c" }; var parameterResults= oa._makeArrayOfArgumentsHash(parameters); assert.equal(parameterResults.length, 4); @@ -72,30 +72,30 @@ vows.describe('OAuth').addBatch({ topic: new OAuth(null, null, null, null, null, null, "HMAC-SHA1"), 'Order them by name' : function(oa) { var parameters= {"z": "a", - "a": "b", + "a": "b", "1": "c" }; var parameterResults= oa._sortRequestParams(oa._makeArrayOfArgumentsHash(parameters)) assert.equal(parameterResults[0][0], "1"); - assert.equal(parameterResults[1][0], "a"); - assert.equal(parameterResults[2][0], "z"); + assert.equal(parameterResults[1][0], "a"); + assert.equal(parameterResults[2][0], "z"); }, 'If two parameter names are the same then order by the value': function(oa) { var parameters= {"z": "a", - "a": ["z", "b", "b", "a", "y"], + "a": ["z", "b", "b", "a", "y"], "1": "c" }; var parameterResults= oa._sortRequestParams(oa._makeArrayOfArgumentsHash(parameters)) assert.equal(parameterResults[0][0], "1"); - assert.equal(parameterResults[1][0], "a"); - assert.equal(parameterResults[1][1], "a"); - assert.equal(parameterResults[2][0], "a"); - assert.equal(parameterResults[2][1], "b"); - assert.equal(parameterResults[3][0], "a"); - assert.equal(parameterResults[3][1], "b"); - assert.equal(parameterResults[4][0], "a"); - assert.equal(parameterResults[4][1], "y"); - assert.equal(parameterResults[5][0], "a"); - assert.equal(parameterResults[5][1], "z"); - assert.equal(parameterResults[6][0], "z"); + assert.equal(parameterResults[1][0], "a"); + assert.equal(parameterResults[1][1], "a"); + assert.equal(parameterResults[2][0], "a"); + assert.equal(parameterResults[2][1], "b"); + assert.equal(parameterResults[3][0], "a"); + assert.equal(parameterResults[3][1], "b"); + assert.equal(parameterResults[4][0], "a"); + assert.equal(parameterResults[4][1], "y"); + assert.equal(parameterResults[5][0], "a"); + assert.equal(parameterResults[5][1], "z"); + assert.equal(parameterResults[6][0], "z"); } }, 'When normalising the request parameters': { @@ -193,7 +193,7 @@ vows.describe('OAuth').addBatch({ 'Support variable whitespace separating the arguments': function(oa) { oa._oauthParameterSeperator= ", "; assert.equal( oa.authHeader("http://somehost.com:3323/foo/poop?bar=foo", "token", "tokensecret"), 'OAuth oauth_consumer_key="consumerkey", oauth_nonce="ybHPeOEkAUJ3k2wJT9Xb43MjtSgTvKqp", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1272399856", oauth_token="token", oauth_version="1.0", oauth_signature="zeOR0Wsm6EG6XSg0Vw%2FsbpoSib8%3D"'); - } + } }, 'When get the OAuth Echo authorization header': { topic: function () { @@ -229,7 +229,7 @@ vows.describe('OAuth').addBatch({ } }, 'When building the OAuth Authorization header': { - topic: new OAuth(null, null, null, null, null, null, "HMAC-SHA1"), + topic: new OAuth(null, null, null, null, null, null, "HMAC-SHA1"), 'All provided oauth arguments should be concatentated correctly' : function(oa) { var parameters= [ ["oauth_timestamp", "1234567"], @@ -237,7 +237,7 @@ vows.describe('OAuth').addBatch({ ["oauth_version", "1.0"], ["oauth_signature_method", "HMAC-SHA1"], ["oauth_consumer_key", "asdasdnm2321b3"]]; - assert.equal(oa._buildAuthorizationHeaders(parameters), 'OAuth oauth_timestamp="1234567",oauth_nonce="ABCDEF",oauth_version="1.0",oauth_signature_method="HMAC-SHA1",oauth_consumer_key="asdasdnm2321b3"'); + assert.equal(oa._buildAuthorizationHeaders(parameters), 'OAuth oauth_timestamp="1234567",oauth_nonce="ABCDEF",oauth_version="1.0",oauth_signature_method="HMAC-SHA1",oauth_consumer_key="asdasdnm2321b3"'); }, '*Only* Oauth arguments should be concatentated, others should be disregarded' : function(oa) { var parameters= [ @@ -249,7 +249,7 @@ vows.describe('OAuth').addBatch({ ["oauth_signature_method", "HMAC-SHA1"], ["oauth_consumer_key", "asdasdnm2321b3"], ["foobar", "asdasdnm2321b3"]]; - assert.equal(oa._buildAuthorizationHeaders(parameters), 'OAuth oauth_timestamp="1234567",oauth_nonce="ABCDEF",oauth_version="1.0",oauth_signature_method="HMAC-SHA1",oauth_consumer_key="asdasdnm2321b3"'); + assert.equal(oa._buildAuthorizationHeaders(parameters), 'OAuth oauth_timestamp="1234567",oauth_nonce="ABCDEF",oauth_version="1.0",oauth_signature_method="HMAC-SHA1",oauth_consumer_key="asdasdnm2321b3"'); }, '_buildAuthorizationHeaders should not depends on Array.prototype.toString' : function(oa) { var _toString = Array.prototype.toString; @@ -363,7 +363,7 @@ vows.describe('OAuth').addBatch({ var testStringLength= testString.length; var testStringBytesLength= Buffer.byteLength(testString); assert.notEqual(testStringLength, testStringBytesLength); // Make sure we're testing a string that differs between byte-length and char-length! - + var op= oa._createClient; try { var callbackCalled= false; @@ -416,7 +416,7 @@ vows.describe('OAuth').addBatch({ "and a post_content_type is specified" : { "It should be written as is, with a content length specified, and the encoding should be set to be as specified" : function(oa) { var op= oa._createClient; - try { + try { var callbackCalled= false; oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { assert.equal(headers["Content-Type"], "unicorn/encoded"); @@ -445,7 +445,7 @@ vows.describe('OAuth').addBatch({ 'if no callback is passed' : { 'it should return a request object': function(oa) { var request= oa.get("http://foo.com/blah", "token", "token_secret") - assert.isObject(request); + assert.isObject(request); assert.equal(request.method, "GET"); request.end(); } @@ -471,7 +471,7 @@ vows.describe('OAuth').addBatch({ oa._createClient= op; } } - } + }, }, 'PUT' : { 'if no callback is passed' : { @@ -556,11 +556,11 @@ vows.describe('OAuth').addBatch({ "and a post_content_type is specified" : { "It should be written as is, with a content length specified, and the encoding should be set to be as specified" : function(oa) { var op= oa._createClient; - try { + try { var callbackCalled= false; oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { assert.equal(headers["Content-Type"], "unicorn/encoded"); - assert.equal(headers["Content-length"], 23); + assert.equal(headers["Content-length"], 23); return { write: function(data) { callbackCalled= true; @@ -582,7 +582,7 @@ vows.describe('OAuth').addBatch({ 'if no callback is passed' : { 'it should return a request object': function(oa) { var request= oa.delete("http://foo.com/blah", "token", "token_secret") - assert.isObject(request); + assert.isObject(request); assert.equal(request.method, "DELETE"); request.end(); } @@ -628,7 +628,7 @@ vows.describe('OAuth').addBatch({ } finally { oa._createClient= op; - } + } } }, 'and a 210 response code is received' : { @@ -648,7 +648,7 @@ vows.describe('OAuth').addBatch({ } finally { oa._createClient= op; - } + } } }, 'And A 301 redirect is received' : { @@ -717,6 +717,78 @@ vows.describe('OAuth').addBatch({ oa._createClient= op; } } + }, + 'and followRedirect is true' : { + 'it should (re)perform the secure request but with the new location' : function(oa) { + var op= oa._createClient; + var psr= oa._performSecureRequest; + var responseCounter = 1; + var callbackCalled = false; + var DummyResponse =function() { + if( responseCounter == 1 ){ + this.statusCode= 301; + this.headers= {location:"http://redirectto.com"}; + responseCounter++; + } + else { + this.statusCode= 200; + } + } + DummyResponse.prototype= events.EventEmitter.prototype; + DummyResponse.prototype.setEncoding= function() {} + + try { + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + return new DummyRequest( new DummyResponse() ); + } + oa._performSecureRequest= function( oauth_token, oauth_token_secret, method, url, extra_params, post_body, post_content_type, callback ) { + if( responseCounter == 1 ) { + assert.equal(url, "http://originalurl.com"); + } + else { + assert.equal(url, "http://redirectto.com"); + } + return psr.call(oa, oauth_token, oauth_token_secret, method, url, extra_params, post_body, post_content_type, callback ) + } + + oa._performSecureRequest("token", "token_secret", 'POST', 'http://originalurl.com', {"scope": "foobar,1,2"}, null, null, function() { + // callback + assert.equal(responseCounter, 2); + callbackCalled= true; + }); + assert.equal(callbackCalled, true) + } + finally { + oa._createClient= op; + oa._performSecureRequest= psr; + } + } + }, + 'and followRedirect is false' : { + 'it should not perform the secure request with the new location' : function(oa) { + var op= oa._createClient; + oa.setClientOptions({ followRedirects: false }); + var DummyResponse =function() { + this.statusCode= 301; + this.headers= {location:"http://redirectto.com"}; + } + DummyResponse.prototype= events.EventEmitter.prototype; + DummyResponse.prototype.setEncoding= function() {} + + try { + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + return new DummyRequest( new DummyResponse() ); + } + oa._performSecureRequest("token", "token_secret", 'POST', 'http://originalurl.com', {"scope": "foobar,1,2"}, null, null, function(res, data, response) { + // callback + assert.equal(res.statusCode, 301); + }); + } + finally { + oa._createClient= op; + oa.setClientOptions({followRedirects:true}); + } + } } }, 'And A 302 redirect is received' : { From 04eb6fade316036e6db7bc984ed6d999ffbdabce Mon Sep 17 00:00:00 2001 From: Pieter Joost van de Sande Date: Fri, 12 Apr 2013 13:28:07 +0200 Subject: [PATCH 03/16] Add test cases for 302 response status --- tests/oauth.js | 74 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/tests/oauth.js b/tests/oauth.js index 7db9836..ab1c296 100644 --- a/tests/oauth.js +++ b/tests/oauth.js @@ -857,7 +857,79 @@ vows.describe('OAuth').addBatch({ oa._createClient= op; } } - } + }, + 'and followRedirect is true' : { + 'it should (re)perform the secure request but with the new location' : function(oa) { + var op= oa._createClient; + var psr= oa._performSecureRequest; + var responseCounter = 1; + var callbackCalled = false; + var DummyResponse =function() { + if( responseCounter == 1 ){ + this.statusCode= 302; + this.headers= {location:"http://redirectto.com"}; + responseCounter++; + } + else { + this.statusCode= 200; + } + } + DummyResponse.prototype= events.EventEmitter.prototype; + DummyResponse.prototype.setEncoding= function() {} + + try { + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + return new DummyRequest( new DummyResponse() ); + } + oa._performSecureRequest= function( oauth_token, oauth_token_secret, method, url, extra_params, post_body, post_content_type, callback ) { + if( responseCounter == 1 ) { + assert.equal(url, "http://originalurl.com"); + } + else { + assert.equal(url, "http://redirectto.com"); + } + return psr.call(oa, oauth_token, oauth_token_secret, method, url, extra_params, post_body, post_content_type, callback ) + } + + oa._performSecureRequest("token", "token_secret", 'POST', 'http://originalurl.com', {"scope": "foobar,1,2"}, null, null, function() { + // callback + assert.equal(responseCounter, 2); + callbackCalled= true; + }); + assert.equal(callbackCalled, true) + } + finally { + oa._createClient= op; + oa._performSecureRequest= psr; + } + } + }, + 'and followRedirect is false' : { + 'it should not perform the secure request with the new location' : function(oa) { + var op= oa._createClient; + oa.setClientOptions({ followRedirects: false }); + var DummyResponse =function() { + this.statusCode= 302; + this.headers= {location:"http://redirectto.com"}; + } + DummyResponse.prototype= events.EventEmitter.prototype; + DummyResponse.prototype.setEncoding= function() {} + + try { + oa._createClient= function( port, hostname, method, path, headers, sshEnabled ) { + return new DummyRequest( new DummyResponse() ); + } + oa._performSecureRequest("token", "token_secret", 'POST', 'http://originalurl.com', {"scope": "foobar,1,2"}, null, null, function(res, data, response) { + // callback + assert.equal(res.statusCode, 302); + }); + } + finally { + oa._createClient= op; + oa.setClientOptions({followRedirects:true}); + } + } + } } } } From d4143a64528481543d4271923f926c97a8c6a7af Mon Sep 17 00:00:00 2001 From: Pieter Joost van de Sande Date: Sun, 5 May 2013 23:04:10 +0200 Subject: [PATCH 04/16] Add followRedirect client option to turn auto follow on or off --- lib/oauth.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/oauth.js b/lib/oauth.js index 7607ee6..104fc5c 100644 --- a/lib/oauth.js +++ b/lib/oauth.js @@ -29,7 +29,8 @@ exports.OAuth= function(requestUrl, accessUrl, consumerKey, consumerSecret, vers "Connection" : "close", "User-Agent" : "Node authentication"} this._clientOptions= this._defaultClientOptions= {"requestTokenHttpMethod": "POST", - "accessTokenHttpMethod": "POST"}; + "accessTokenHttpMethod": "POST", + "followRedirects": true}; this._oauthParameterSeperator = ","; }; @@ -352,6 +353,7 @@ exports.OAuth.prototype._performSecureRequest= function( oauth_token, oauth_toke request= this._createClient(parsedUrl.port, parsedUrl.hostname, method, path, headers); } + var clientOptions = this._clientOptions; if( callback ) { var data=""; var self= this; @@ -367,7 +369,7 @@ exports.OAuth.prototype._performSecureRequest= function( oauth_token, oauth_toke callback(null, data, response); } else { // Follow 301 or 302 redirects with Location HTTP header - if((response.statusCode == 301 || response.statusCode == 302) && response.headers && response.headers.location) { + if((response.statusCode == 301 || response.statusCode == 302) && clientOptions.followRedirects && response.headers && response.headers.location) { self._performSecureRequest( oauth_token, oauth_token_secret, method, response.headers.location, extra_params, post_body, post_content_type, callback); } else { From f24590182c380fbdfbe4faf31c4e69dad5dac140 Mon Sep 17 00:00:00 2001 From: Pieter Joost van de Sande Date: Mon, 6 May 2013 08:42:23 +0200 Subject: [PATCH 05/16] Remove trailing whitespaces --- lib/oauth.js | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/lib/oauth.js b/lib/oauth.js index 104fc5c..2998ab4 100644 --- a/lib/oauth.js +++ b/lib/oauth.js @@ -87,7 +87,7 @@ exports.OAuth.prototype._getSignature= function(method, url, parameters, tokenSe exports.OAuth.prototype._normalizeUrl= function(url) { var parsedUrl= URL.parse(url, true) var port =""; - if( parsedUrl.port ) { + if( parsedUrl.port ) { if( (parsedUrl.protocol == "http:" && parsedUrl.port != "80" ) || (parsedUrl.protocol == "https:" && parsedUrl.port != "443") ) { port= ":" + parsedUrl.port; @@ -95,7 +95,7 @@ exports.OAuth.prototype._normalizeUrl= function(url) { } if( !parsedUrl.pathname || parsedUrl.pathname == "" ) parsedUrl.pathname ="/"; - + return parsedUrl.protocol + "//" + parsedUrl.hostname + port + parsedUrl.pathname; } @@ -125,7 +125,7 @@ exports.OAuth.prototype._buildAuthorizationHeaders= function(orderedParameters) } } - authHeader= authHeader.substring(0, authHeader.length-this._oauthParameterSeperator.length); + authHeader= authHeader.substring(0, authHeader.length-this._oauthParameterSeperator.length); return authHeader; } @@ -144,17 +144,17 @@ exports.OAuth.prototype._makeArrayOfArgumentsHash= function(argumentsHash) { argument_pairs[argument_pairs.length]= [key, value]; } } - return argument_pairs; -} + return argument_pairs; +} // Sorts the encoded key value pairs by encoded name, then encoded value exports.OAuth.prototype._sortRequestParams= function(argument_pairs) { // Sort by name, then value. argument_pairs.sort(function(a,b) { if ( a[0]== b[0] ) { - return a[1] < b[1] ? -1 : 1; + return a[1] < b[1] ? -1 : 1; } - else return a[0] < b[0] ? -1 : 1; + else return a[0] < b[0] ? -1 : 1; }); return argument_pairs; @@ -167,10 +167,10 @@ exports.OAuth.prototype._normaliseRequestParams= function(arguments) { argument_pairs[i][0]= this._encodeData( argument_pairs[i][0] ); argument_pairs[i][1]= this._encodeData( argument_pairs[i][1] ); } - + // Then sort them #3.4.1.3.2 .2 argument_pairs= this._sortRequestParams( argument_pairs ); - + // Then concatenate together #3.4.1.3.2 .3 & .4 var args= ""; for(var i=0;i Date: Mon, 6 May 2013 09:40:40 +0200 Subject: [PATCH 06/16] Add test to proof default value for followRedirects is true --- tests/oauth.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/oauth.js b/tests/oauth.js index ab1c296..99e5bb6 100644 --- a/tests/oauth.js +++ b/tests/oauth.js @@ -24,6 +24,12 @@ DummyRequest.prototype.end= function(){ } vows.describe('OAuth').addBatch({ + 'When newing OAuth': { + topic: new OAuth(null, null, null, null, null, null, "PLAINTEXT"), + 'followRedirects is enabled by default': function (oa) { + assert.equal(oa._clientOptions.followRedirects, true) + } + }, 'When generating the signature base string described in http://oauth.net/core/1.0/#sig_base_example': { topic: new OAuth(null, null, null, null, null, null, "HMAC-SHA1"), 'we get the expected result string': function (oa) { From a742d838f5496a695d3dcc7dbce651c98440d7d9 Mon Sep 17 00:00:00 2001 From: Andrew Martens Date: Tue, 7 May 2013 14:38:04 -0700 Subject: [PATCH 07/16] Set default User-Agent if not otherwise specified in customHeaders --- lib/oauth2.js | 4 ++++ tests/oauth2.js | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/lib/oauth2.js b/lib/oauth2.js index 32c8aae..4747579 100644 --- a/lib/oauth2.js +++ b/lib/oauth2.js @@ -74,6 +74,10 @@ exports.OAuth2.prototype._request= function(method, url, headers, post_body, acc } realHeaders['Host']= parsedUrl.host; + if (!realHeaders['User-Agent']) { + realHeaders['User-Agent'] = 'Node-oauth'; + } + realHeaders['Content-Length']= post_body ? Buffer.byteLength(post_body) : 0; if( access_token && !('Authorization' in realHeaders)) { if( ! parsedUrl.query ) parsedUrl.query= {}; diff --git a/tests/oauth2.js b/tests/oauth2.js index 3eda163..d16a0e0 100644 --- a/tests/oauth2.js +++ b/tests/oauth2.js @@ -133,5 +133,29 @@ vows.describe('OAuth2').addBatch({ oa.get("", {}); } } + }, + 'When the user passes in the User-Agent in customHeaders': { + topic: new OAuth2("clientId", "clientSecret", undefined, undefined, undefined, + { 'User-Agent': '123Agent' }), + 'When calling get': { + 'we should see the User-Agent mixed into headers property in options passed to http-library' : function(oa) { + oa._executeRequest= function( http_library, options, callback ) { + assert.equal(options.headers["User-Agent"], "123Agent"); + }; + oa.get("", {}); + } + } + }, + 'When the user does not pass in a User-Agent in customHeaders': { + topic: new OAuth2("clientId", "clientSecret", undefined, undefined, undefined, + undefined), + 'When calling get': { + 'we should see the default User-Agent mixed into headers property in options passed to http-library' : function(oa) { + oa._executeRequest= function( http_library, options, callback ) { + assert.equal(options.headers["User-Agent"], "Node-oauth"); + }; + oa.get("", {}); + } + } } }).export(module); From 8658d7d55be25528a091415ca01671e6ef2a9882 Mon Sep 17 00:00:00 2001 From: Brad Gignac Date: Sat, 27 Jul 2013 18:35:09 -0400 Subject: [PATCH 08/16] Remove type query parameter from OAuth2 requests. While the type parameter was required in older versions of the OAuth2 specification, it was removed in version 8. Currently, this breaks OAuth2 against the Dropbox API. Closes #127. http://tools.ietf.org/html/draft-ietf-oauth-v2-08#section-4.1.1 --- lib/oauth2.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/oauth2.js b/lib/oauth2.js index 32c8aae..835b327 100644 --- a/lib/oauth2.js +++ b/lib/oauth2.js @@ -132,13 +132,12 @@ exports.OAuth2.prototype._executeRequest= function( http_library, options, post_ if( options.method == 'POST' && post_body ) { request.write(post_body); } - request.end(); + request.end(); } exports.OAuth2.prototype.getAuthorizeUrl= function( params ) { var params= params || {}; params['client_id'] = this._clientId; - params['type'] = 'web_server'; return this._baseSite + this._authorizeUrl + "?" + querystring.stringify(params); } @@ -146,7 +145,6 @@ exports.OAuth2.prototype.getOAuthAccessToken= function(code, params, callback) { var params= params || {}; params['client_id'] = this._clientId; params['client_secret'] = this._clientSecret; - params['type']= 'web_server'; var codeParam = (params.grant_type === 'refresh_token') ? 'refresh_token' : 'code'; params[codeParam]= code; From e599d29787d4bb70b24b1b219482eff49f36d9cd Mon Sep 17 00:00:00 2001 From: Derek Brooks Date: Tue, 15 Oct 2013 00:53:34 +0000 Subject: [PATCH 09/16] Write the post_body to OAuth2 PUT requests --- lib/oauth2.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/oauth2.js b/lib/oauth2.js index 32c8aae..39efa43 100644 --- a/lib/oauth2.js +++ b/lib/oauth2.js @@ -129,7 +129,7 @@ exports.OAuth2.prototype._executeRequest= function( http_library, options, post_ callback(e); }); - if( options.method == 'POST' && post_body ) { + if( (options.method == 'POST' || options.method == 'PUT') && post_body ) { request.write(post_body); } request.end(); From 171e668f386a3e1ba0bcb915b8dc7fdc9335aa62 Mon Sep 17 00:00:00 2001 From: ciaranj Date: Sat, 4 Jan 2014 14:49:01 +0000 Subject: [PATCH 10/16] Bumping version to 0.9.11 --- Readme.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index beea8b5..9f0ce21 100644 --- a/Readme.md +++ b/Readme.md @@ -75,6 +75,11 @@ describe('OAuth2',function(){ Change History ============== +* 0.9.11 + - OAuth2: No longer sends the type=webserver argument with the OAuth2 requests (thank you bendiy) + - OAuth2: Provides a default (and overrideable) User-Agent header (thanks to Andrew Martens & Daniel Mahlow) + - OAuth1: New followRedirects client option (true by default) (thanks to Pieter Joost van de Sande) + - OAuth1: Adds RSA-SHA1 support (thanks to Jeffrey D. Van Alstine & Michael Garvin & Andreas Knecht) * 0.9.10 - OAuth2: Addresses 2 issues that came in with 0.9.9, #129 & #125 (thank you José F. Romaniello) * 0.9.9 @@ -156,3 +161,10 @@ Contributors (In no particular order) * rolandboon - http://rolandboon.com * Brian Park - http://github.com/yaru22 * José F. Romaniello - http://github.com/jfromaniello +* bendiy - https://github.com/bendiy +* Andrew Martins - http://www.andrewmartens.com +* Daniel Mahlow - https://github.com/dmahlow +* Pieter Joost van de Sande - https://github.com/pjvds +* Jeffrey D. Van Alstine +* Michael Garvin +* Andreas Knecht diff --git a/package.json b/package.json index 65f8628..14a9b6d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name" : "oauth" , "description" : "Library for interacting with OAuth 1.0, 1.0A, 2 and Echo. Provides simplified client access and allows for construction of more complex apis and OAuth providers." -, "version" : "0.9.10" +, "version" : "0.9.11" , "directories" : { "lib" : "./lib" } , "main" : "index.js" , "author" : "Ciaran Jessup " From f1a470fa2b7c29573da9d8bc4e2012592a8f8bee Mon Sep 17 00:00:00 2001 From: psanchezdl Date: Wed, 8 Jan 2014 20:06:09 -0500 Subject: [PATCH 11/16] Koding button Sorry about that! --- Readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Readme.md b/Readme.md index 9f0ce21..0d0da48 100644 --- a/Readme.md +++ b/Readme.md @@ -6,6 +6,8 @@ Tested against Twitter (http://twitter.com), term.ie (http://term.ie/oauth/examp Also provides rudimentary OAuth2 support, tested against facebook, github, foursquare, google and Janrain. For more complete usage examples please take a look at connect-auth (http://github.com/ciaranj/connect-auth) +[![Clone in Koding](http://learn.koding.com/btn/clone_d.png)][koding] +[koding]: https://koding.com/Teamwork?import=https://github.com/ciaranj/node-oauth/archive/master.zip&c=git1 Installation ============== From 48033df3e440e5d8a0a70b4f67a68bc7c67d5e74 Mon Sep 17 00:00:00 2001 From: josh coffman Date: Sun, 23 Mar 2014 19:38:03 -0700 Subject: [PATCH 12/16] renamed 'OAuth' -> 'oauth' to be linux friendly in the readme --- Readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index 9f0ce21..74237cd 100644 --- a/Readme.md +++ b/Readme.md @@ -22,7 +22,7 @@ To run examples/tests insall Mocha `$ npm install -g mocha` and run `$ mocha you ```javascript describe('OAuth1.0',function(){ - var OAuth = require('OAuth'); + var OAuth = require('oauth'); it('tests trends Twitter API v1.1',function(done){ var oauth = new OAuth.OAuth( @@ -50,7 +50,7 @@ describe('OAuth1.0',function(){ ## OAuth2.0 ```javascript describe('OAuth2',function(){ - var OAuth = require('OAuth'); + var OAuth = require('oauth'); it('gets bearer token', function(done){ var OAuth2 = OAuth.OAuth2; From 54b958436a01b6ceb70e455a886e9dcaa9a5574b Mon Sep 17 00:00:00 2001 From: Don Neufeld Date: Tue, 1 Apr 2014 19:05:17 -0700 Subject: [PATCH 13/16] Added protection against multiple callback invocation on error path --- lib/oauth.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/oauth.js b/lib/oauth.js index b4873b2..4c0e911 100644 --- a/lib/oauth.js +++ b/lib/oauth.js @@ -405,8 +405,10 @@ exports.OAuth.prototype._performSecureRequest= function( oauth_token, oauth_toke }); request.on("error", function(err) { - callbackCalled= true; - callback( err ) + if(!callbackCalled) { + callbackCalled= true; + callback( err ) + } }); if( (method == "POST" || method =="PUT") && post_body != null && post_body != "" ) { From 9b538ba09af03236ceb8e3714e80098565099531 Mon Sep 17 00:00:00 2001 From: Ted Goddard Date: Thu, 17 Apr 2014 11:18:11 -0600 Subject: [PATCH 14/16] support for use_strict --- lib/oauth.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/oauth.js b/lib/oauth.js index 4c0e911..76f68a7 100644 --- a/lib/oauth.js +++ b/lib/oauth.js @@ -166,8 +166,8 @@ exports.OAuth.prototype._sortRequestParams= function(argument_pairs) { return argument_pairs; } -exports.OAuth.prototype._normaliseRequestParams= function(arguments) { - var argument_pairs= this._makeArrayOfArgumentsHash(arguments); +exports.OAuth.prototype._normaliseRequestParams= function(args) { + var argument_pairs= this._makeArrayOfArgumentsHash(args); // First encode them #3.4.1.3.2 .1 for(var i=0;i= 200 && response.statusCode <= 299 ) { From 22366489c8e949152fde11358332e92730cae23b Mon Sep 17 00:00:00 2001 From: Mathias Gebbe Date: Tue, 13 May 2014 17:31:58 +0200 Subject: [PATCH 15/16] Update Readme.md typo --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 74237cd..88407e7 100644 --- a/Readme.md +++ b/Readme.md @@ -36,7 +36,7 @@ describe('OAuth1.0',function(){ ); oauth.get( 'https://api.twitter.com/1.1/trends/place.json?id=23424977', - 'your user toke for this app', //test user token + 'your user token for this app', //test user token 'your user secret for this app', //test user secret function (e, data, res){ if (e) console.error(e); From 1eda281512926b305fbeff8ebeb234b9e9b05802 Mon Sep 17 00:00:00 2001 From: ciaranj Date: Wed, 21 May 2014 20:20:51 +0100 Subject: [PATCH 16/16] Provides a test-case for #160 --- lib/oauth2.js | 16 +++++++++----- tests/oauth2.js | 57 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/lib/oauth2.js b/lib/oauth2.js index 39efa43..c7db2bd 100644 --- a/lib/oauth2.js +++ b/lib/oauth2.js @@ -49,19 +49,25 @@ exports.OAuth2.prototype.buildAuthHeader= function(token) { return this._authMethod + ' ' + token; }; +exports.OAuth2.prototype._chooseHttpLibrary= function( parsedUrl ) { + var http_library= https; + // As this is OAUth2, we *assume* https unless told explicitly otherwise. + if( parsedUrl.protocol != "https:" ) { + http_library= http; + } + return http_library; +}; + exports.OAuth2.prototype._request= function(method, url, headers, post_body, access_token, callback) { - var http_library= https; var creds = crypto.createCredentials({ }); var parsedUrl= URL.parse( url, true ); if( parsedUrl.protocol == "https:" && !parsedUrl.port ) { parsedUrl.port= 443; } - // As this is OAUth2, we *assume* https unless told explicitly otherwise. - if( parsedUrl.protocol != "https:" ) { - http_library= http; - } + var http_library= this._chooseHttpLibrary( parsedUrl ); + var realHeaders= {}; for( var key in this._customHeaders ) { diff --git a/tests/oauth2.js b/tests/oauth2.js index 3eda163..4d01147 100644 --- a/tests/oauth2.js +++ b/tests/oauth2.js @@ -125,13 +125,66 @@ vows.describe('OAuth2').addBatch({ 'Given an OAuth2 instance with clientId, clientSecret and customHeaders': { topic: new OAuth2("clientId", "clientSecret", undefined, undefined, undefined, { 'SomeHeader': '123' }), - 'When calling get': { + 'When GETing': { 'we should see the custom headers mixed into headers property in options passed to http-library' : function(oa) { oa._executeRequest= function( http_library, options, callback ) { assert.equal(options.headers["SomeHeader"], "123"); }; oa.get("", {}); - } + }, } + }, + 'Given an OAuth2 instance with a clientId and clientSecret': { + topic: new OAuth2("clientId", "clientSecret"), + 'When POSTing': { + 'we should see a given string being sent to the request' : function(oa) { + var bodyWritten= false; + oa._chooseHttpLibrary= function() { + return { + request: function(options) { + assert.equal(options.headers["Content-Type"], "text/plain"); + assert.equal(options.headers["Content-Length"], 26); + assert.equal(options.method, "POST"); + return { + end: function() {}, + on: function() {}, + write: function(body) { + bodyWritten= true; + assert.isNotNull(body); + assert.equal(body, "THIS_IS_A_POST_BODY_STRING") + } + } + } + }; + } + oa._request("POST", "", {"Content-Type":"text/plain"}, "THIS_IS_A_POST_BODY_STRING"); + assert.ok( bodyWritten ); + } + }, + 'When PUTing': { + 'we should see a given string being sent to the request' : function(oa) { + var bodyWritten= false; + oa._chooseHttpLibrary= function() { + return { + request: function(options) { + assert.equal(options.headers["Content-Type"], "text/plain"); + assert.equal(options.headers["Content-Length"], 25); + assert.equal(options.method, "PUT"); + return { + end: function() {}, + on: function() {}, + write: function(body) { + bodyWritten= true; + assert.isNotNull(body); + assert.equal(body, "THIS_IS_A_PUT_BODY_STRING") + } + } + } + }; + } + oa._request("PUT", "", {"Content-Type":"text/plain"}, "THIS_IS_A_PUT_BODY_STRING"); + assert.ok( bodyWritten ); + } + } } }).export(module);