oauth.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. var parseUrl = require("url").parse;
  2. var querystring = require("./util/querystring");
  3. var whenPromise = require("./promise").whenPromise;
  4. var makeRequest = require("./http-client").request;
  5. var crypto = require("crypto");
  6. function encodeRfc3986(str){
  7. return !str ? "" : encodeURIComponent(str)
  8. .replace(/\!/g, "%21")
  9. .replace(/\'/g, "%27")
  10. .replace(/\(/g, "%28")
  11. .replace(/\)/g, "%29")
  12. .replace(/\*/g, "%2A");
  13. }
  14. function parseResponse(response){
  15. return response.body.join("").then(function(body){
  16. if(response.status == 200){
  17. return querystring.parse(body);
  18. }else{
  19. var err = new Error(response.status + ": " + body);
  20. err.status = response.status;
  21. err.headers = response.headers;
  22. err.body = body;
  23. throw err;
  24. }
  25. });
  26. }
  27. exports.Client = Client;
  28. function Client(identifier, secret, tempRequestUrl, tokenRequestUrl, callback, version, signatureMethod, nonceGenerator, headers){
  29. this.identifier = identifier;
  30. this.tempRequestUrl = tempRequestUrl;
  31. this.tokenRequestUrl = tokenRequestUrl;
  32. this.callback = callback;
  33. this.version = version || false;
  34. // _createSignature actually uses the variable, not the instance property
  35. this.signatureMethod = signatureMethod = signatureMethod || "HMAC-SHA1";
  36. this.generateNonce = nonceGenerator || Client.makeNonceGenerator(32);
  37. this.headers = headers || Client.Headers;
  38. if(this.signatureMethod != "PLAINTEXT" && this.signatureMethod != "HMAC-SHA1"){
  39. throw new Error("Unsupported signature method: " + this.signatureMethod);
  40. }
  41. // We don't store the secrets on the instance itself, that way it can
  42. // be passed to other actors without leaking
  43. secret = encodeRfc3986(secret);
  44. this._createSignature = function(tokenSecret, baseString){
  45. if(baseString === undefined){
  46. baseString = tokenSecret;
  47. tokenSecret = "";
  48. }
  49. var key = secret + "&" + tokenSecret;
  50. if(signatureMethod == "PLAINTEXT"){
  51. return key;
  52. }else{
  53. return crypto.createHmac("SHA1", key).update(baseString).digest("base64");
  54. }
  55. };
  56. }
  57. Client.Headers = {
  58. Accept: "*/*",
  59. Connection: "close",
  60. "User-Agent": "promised-io/oauth"
  61. };
  62. // The default headers shouldn't change after clients have been created,
  63. // but you're free to replace the object or pass headers to the Client
  64. // constructor.
  65. Object.freeze(Client.Headers);
  66. Client.NonceChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
  67. Client.makeNonceGenerator = function(nonceSize){
  68. var nonce = Array(nonceSize + 1).join("-").split("");
  69. var chars = Client.NonceChars.split("");
  70. return function nonceGenerator(){
  71. return nonce.map(getRandomChar).join("");
  72. };
  73. function getRandomChar(){
  74. return chars[Math.floor(Math.random() * chars.length)];
  75. }
  76. };
  77. Client.getTimestamp = function(){
  78. return Math.floor(Date.now() / 1000).toString();
  79. };
  80. // Binds the client against a set of token credentials.
  81. // The resulting object can be used to make signed requests.
  82. // The secret won't be exposed on the object itself.
  83. Client.prototype.bind = function(tokenIdentifier, tokenSecret){
  84. var bound = {
  85. identifier: this.identifier,
  86. tokenIdentifier: tokenIdentifier,
  87. signatureMethod: this.signatureMethod,
  88. version: this.version,
  89. headers: this.headers,
  90. generateNonce: this.generateNonce
  91. };
  92. bound._createSignature = this._createSignature.bind(this, encodeRfc3986(tokenSecret));
  93. bound._createSignatureBase = this._createSignatureBase;
  94. bound._normalizeUrl = this._normalizeUrl;
  95. bound._collectOAuthParams = this._collectOAuthParams;
  96. bound._normalizeParams = this._normalizeParams;
  97. bound._signRequest = this._signRequest;
  98. bound.request = this.request;
  99. return bound;
  100. };
  101. // Wrapper for `http-client.request` which signs the request
  102. // with the client credentials, and optionally token credentials if bound.
  103. Client.prototype.request = function(originalRequest){
  104. var request = {};
  105. for(var key in originalRequest){
  106. if(originalRequest.hasOwnProperty(key)){
  107. request[key] = originalRequest[key];
  108. }
  109. }
  110. // Normalize the request. `engines/node/http-client.request` is
  111. // quite flexible, but this should do it.
  112. if(request.url){
  113. var parsed = parseUrl(request.url);
  114. parsed.pathInfo = parsed.pathname;
  115. parsed.queryString = parsed.query;
  116. for(var i in parsed){
  117. request[i] = parsed[i];
  118. }
  119. }
  120. request.pathname = request.pathname || request.pathInfo || "/";
  121. request.queryString = request.queryString || request.query || "";
  122. request.method = (request.method || "GET").toUpperCase();
  123. request.protocol = request.protocol.toLowerCase();
  124. request.hostname = (request.host || request.hostname).toLowerCase();
  125. request.headers = {};
  126. for(var h in this.headers){
  127. request.headers[h] = this.headers[h];
  128. }
  129. for(var h in originalRequest.headers){
  130. request.headers[h] = originalRequest.headers[h];
  131. }
  132. // We'll be setting the Authorization header; due to how `engines/node/http-client.request`
  133. // is implemented we need to set the Host header as well.
  134. request.headers.host = request.headers.host || request.hostname + (request.port ? ":" + request.port : "");
  135. // Parse all request parameters into a flattened array of parameter pairs.
  136. // Note that this array contains munged parameter names.
  137. var requestParams = [], uncombinedRequestParams = [];
  138. // Start with parameters that were defined in the query string
  139. if(request.queryString){
  140. querystring.parseToArray(requestParams, request.queryString);
  141. }
  142. // Allow parameters to be defined in object notation, this is *not* part of `http-client.request`!
  143. // It saves an extra stringify+parse though.
  144. if(request.requestParams){
  145. for(var i in request.requestParams){
  146. if(request.requestParams.hasOwnProperty(i)){
  147. querystring.addToArray(uncombinedRequestParams, i, request.requestParams[i]);
  148. }
  149. }
  150. }
  151. // Send the parameters from `request.requestParams` in the query string
  152. // for GET and DELETE requests. We immediately concat to the `requestParams` array,
  153. // which is then built into the query string.
  154. if(request.method == "GET" || request.method == "DELETE"){
  155. requestParams = requestParams.concat(uncombinedRequestParams);
  156. }
  157. // Rebuild the query string
  158. request.queryString = requestParams.reduce(function(qs, v, i){
  159. return qs + (i % 2 ? "=" + querystring.escape(v) : (qs.length ? "&" : "") + querystring.escape(v));
  160. }, "");
  161. // Depending on the request content type, look for request parameters in the body
  162. var waitForBody = false;
  163. if(request.headers && request.headers["Content-Type"] == "application/x-www-form-urlencoded"){
  164. waitForBody = whenPromise(request.body.join(""), function(body){
  165. querystring.parseToArray(requestParams, body);
  166. return body;
  167. });
  168. }
  169. // If we're a POST or PUT and are not sending any content, or are sending urlencoded content,
  170. // add the `request.request` to the request body. If we are sending non-urlencoded content through
  171. // a POST or PUT, the `request.requestParams` are ignored.
  172. if(request.requestParams && (request.method == "POST" || request.method == "PUT") && (!request.headers || !request.headers["Content-Type"] || request.headers["Content-Type"] == "application/x-www-form-urlencoded")){
  173. waitForBody = whenPromise(waitForBody, function(body){
  174. requestParams = requestParams.concat(uncombinedRequestParams);
  175. body = (body ? body + "&" : "") + querystring.stringify(request.requestParams);
  176. request.body = [body];
  177. request.headers["Content-Type"] = "application/x-www-form-urlencoded";
  178. });
  179. }
  180. // Sign the request and then actually make it.
  181. return whenPromise(waitForBody, function(){
  182. this._signRequest(request, requestParams);
  183. return makeRequest(request);
  184. }.bind(this));
  185. };
  186. Client.prototype._normalizeUrl = function(request){
  187. var normalized = request.protocol + "//" + request.hostname;
  188. if(request.protocol == "http:" && request.port && (request.port + "") != "80"){
  189. normalized += ":" + request.port;
  190. }
  191. if(request.protocol == "https:" && request.port && (request.port + "") != "443"){
  192. normalized += ":" + request.port;
  193. }
  194. return normalized + request.pathname;
  195. };
  196. Client.prototype._collectOAuthParams = function(request, requestParams){
  197. var oauthParams = {};
  198. if(request.oauthParams){
  199. for(var p in request.oauthParams){
  200. // Don't allow `request.oauthParams` to override standard values.
  201. // `oauth_token` and `oauth_version` are conditionally added,
  202. // the other parameters are always set. Hence we just test for
  203. // the first two.
  204. if(p != "oauth_token" && p != "oauth_version"){
  205. oauthParams[p] = request.oauthParams[p];
  206. }
  207. }
  208. }
  209. oauthParams.oauth_consumer_key = this.identifier;
  210. oauthParams.oauth_signature_method = this.signatureMethod;
  211. oauthParams.oauth_timestamp = Client.getTimestamp();
  212. oauthParams.oauth_nonce = this.generateNonce();
  213. if(this.tokenIdentifier){
  214. oauthParams.oauth_token = this.tokenIdentifier;
  215. }
  216. if(this.version){
  217. oauthParams.oauth_version = this.version;
  218. }
  219. for(var i in oauthParams){
  220. requestParams.push(i, oauthParams[i]);
  221. }
  222. return oauthParams;
  223. };
  224. Client.prototype._normalizeParams = function(requestParams){
  225. // Encode requestParams
  226. requestParams = requestParams.map(encodeRfc3986);
  227. // Unflatten the requestParams for sorting
  228. requestParams = requestParams.reduce(function(result, _, i, arr){
  229. if(i % 2 == 0){
  230. result.push(arr.slice(i, i + 2));
  231. }
  232. return result;
  233. }, []);
  234. // Sort the unflattened requestParams
  235. requestParams.sort(function(a, b){
  236. if(a[0] == b[0]){
  237. return a[1] < b[1] ? -1 : 1;
  238. }else{
  239. return a[0] < b[0] ? -1 : 1;
  240. }
  241. });
  242. return requestParams.map(function(pair){ return pair.join("="); }).join("&");
  243. };
  244. Client.prototype._createSignatureBase = function(requestMethod, baseUri, params){
  245. return [requestMethod, baseUri, params].map(encodeRfc3986).join("&");
  246. };
  247. Client.prototype._signRequest = function(request, requestParams){
  248. // Calculate base URI string
  249. var baseUri = this._normalizeUrl(request);
  250. // Register OAuth parameters and add to the request parameters
  251. // Additional parameters can be specified via the `request.oauthParams` object
  252. var oauthParams = this._collectOAuthParams(request, requestParams);
  253. // Generate parameter string
  254. var params = this._normalizeParams(requestParams);
  255. // Sign the base string
  256. var baseString = this._createSignatureBase(request.method, baseUri, params);
  257. oauthParams.oauth_signature = this._createSignature(baseString);
  258. // Add Authorization header
  259. request.headers.authorization = "OAuth " + Object.keys(oauthParams).map(function(name){
  260. return encodeRfc3986(name) + "=\"" + encodeRfc3986(oauthParams[name]) + "\"";
  261. }).join(",");
  262. // Now the request object can be used to make a signed request
  263. return request;
  264. };
  265. Client.prototype.obtainTempCredentials = function(oauthParams, extraParams){
  266. oauthParams = oauthParams || {};
  267. if(this.callback && !oauthParams.oauth_callback){
  268. oauthParams.oauth_callback = this.callback;
  269. }
  270. return this.request({
  271. method: "POST",
  272. url: this.tempRequestUrl,
  273. oauthParams: oauthParams,
  274. requestParams: extraParams || {}
  275. }).then(parseResponse);
  276. };
  277. Client.prototype.obtainTokenCredentials = function(tokenIdentifier, tokenSecret, verifierToken, extraParams){
  278. return this.bind(tokenIdentifier, tokenSecret).request({
  279. method: "POST",
  280. url: this.tokenRequestUrl,
  281. oauthParams: { oauth_verifier: verifierToken },
  282. requestParams: extraParams
  283. }).then(parseResponse);
  284. };