diff --git a/Readme.md b/Readme.md index beea8b5..7cba14e 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 ============== @@ -22,7 +24,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( @@ -36,7 +38,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); @@ -50,7 +52,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; @@ -75,6 +77,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 +163,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/lib/oauth.js b/lib/oauth.js index e0a5812..2215c4e 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; @@ -29,7 +32,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 = ","; }; @@ -40,9 +44,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; @@ -86,7 +93,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; @@ -94,7 +101,7 @@ exports.OAuth.prototype._normalizeUrl= function(url) { } if( !parsedUrl.pathname || parsedUrl.pathname == "" ) parsedUrl.pathname ="/"; - + return parsedUrl.protocol + "//" + parsedUrl.hostname + port + parsedUrl.pathname; } @@ -124,7 +131,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; } @@ -143,33 +150,33 @@ 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; } -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 ) { 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 { @@ -400,12 +412,14 @@ 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 != "" ) { request.write(post_body); } @@ -417,7 +431,7 @@ exports.OAuth.prototype._performSecureRequest= function( oauth_token, oauth_toke } return request; } - + return; } @@ -444,7 +458,7 @@ exports.OAuth.prototype.getOAuthAccessToken= function(oauth_token, oauth_token_s } else { extraParams.oauth_verifier= oauth_verifier; } - + this._performSecureRequest( oauth_token, oauth_token_secret, this._clientOptions.accessTokenHttpMethod, this._accessUrl, extraParams, null, null, function(error, data, response) { if( error ) callback(error); else { @@ -484,7 +498,7 @@ exports.OAuth.prototype._putOrPost= function(method, url, oauth_token, oauth_tok } return this._performSecureRequest( oauth_token, oauth_token_secret, method, url, extra_params, post_body, post_content_type, callback ); } - + exports.OAuth.prototype.put= function(url, oauth_token, oauth_token_secret, post_body, post_content_type, callback) { return this._putOrPost("PUT", url, oauth_token, oauth_token_secret, post_body, post_content_type, callback); @@ -500,7 +514,7 @@ exports.OAuth.prototype.post= function(url, oauth_token, oauth_token_secret, pos * * The callback should expect a function of the following form: * - * function(err, token, token_secret, parsedQueryString) {} + * function(err, token, token_secret, parsedQueryString) {} * * This method has optional parameters so can be called in the following 2 ways: * @@ -519,7 +533,7 @@ exports.OAuth.prototype.getOAuthRequestToken= function( extraParams, callback ) callback = extraParams; extraParams = {}; } - // Callbacks are 1.0A related + // Callbacks are 1.0A related if( this._authorize_callback ) { extraParams["oauth_callback"]= this._authorize_callback; } @@ -546,12 +560,12 @@ exports.OAuth.prototype.signUrl= function(url, oauth_token, oauth_token_secret, var orderedParameters= this._prepareParameters(oauth_token, oauth_token_secret, method, url, {}); var parsedUrl= URL.parse( url, false ); - var query=""; + var query=""; for( var i= 0 ; i < orderedParameters.length; i++) { query+= orderedParameters[i][0]+"="+ this._encodeData(orderedParameters[i][1]) + "&"; } query= query.substring(0, query.length-1); - + return parsedUrl.protocol + "//"+ parsedUrl.host + parsedUrl.pathname + "?" + query; }; diff --git a/lib/oauth2.js b/lib/oauth2.js index 32c8aae..efe5601 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 ) { @@ -74,6 +80,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= {}; @@ -129,16 +139,15 @@ 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(); + 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 +155,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; 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 " diff --git a/tests/oauth.js b/tests/oauth.js index 563cd39..e88b754 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; @@ -21,21 +22,65 @@ DummyRequest.prototype.write= function(post_body){ } 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 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) { - 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"); } }, + '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) { - 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 +103,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 +117,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 +238,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 +274,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 +282,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 +294,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; @@ -413,7 +458,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; @@ -466,7 +511,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"); @@ -495,7 +540,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(); } @@ -521,7 +566,7 @@ vows.describe('OAuth').addBatch({ oa._createClient= op; } } - } + }, }, 'PUT' : { 'if no callback is passed' : { @@ -656,11 +701,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; @@ -682,7 +727,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(); } @@ -728,7 +773,7 @@ vows.describe('OAuth').addBatch({ } finally { oa._createClient= op; - } + } } }, 'and a 210 response code is received' : { @@ -748,7 +793,7 @@ vows.describe('OAuth').addBatch({ } finally { oa._createClient= op; - } + } } }, 'And A 301 redirect is received' : { @@ -817,6 +862,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' : { @@ -885,7 +1002,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}); + } + } + } } } } diff --git a/tests/oauth2.js b/tests/oauth2.js index 3eda163..f27e263 100644 --- a/tests/oauth2.js +++ b/tests/oauth2.js @@ -125,12 +125,89 @@ 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 ); + } + } + }, + '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("", {}); } } }