mashape-oauth
Version:
Library for handling OAuth (1.0a, Echo, XAuth, and 2.0) Requests and Responses
572 lines (485 loc) • 22.1 kB
JavaScript
var crypto = require('crypto');
var http = require('http');
var https = require('https');
var URL = require('url');
var query = require('querystring');
var utils = require('./utils');
var zlib = require('zlib');
// Constructor for starting an OAuth (1.0) handshake
//
// - `options` `Object` *OAuth request options*
// - `echo` `Object` ___Optional___ *If it exists we treat the request as OAuth Echo request. See [Twitter](https://dev.twitter.com/docs/auth/oauth/oauth-echo)*
// - `verifyCredentials` `String` *What is the credentials URI to delegate against?*
// - `realm` `String` ___Optional___ *Access Authentication Framework Realm Value, Commonly used in Echo Requests, allowed in all however: [Section 3.5.1](http://tools.ietf.org/html/rfc5849#section-3.5.1)*
// - `requestUrl` `String` *Request Token URL. [Section 6.1](http://oauth.net/core/1.0/#auth_step1)*
// - `accessUrl` `String` *Access Token URL. [Section 6.2](http://oauth.net/core/1.0/#auth_step2)*
// - `callback` `String` *URL the Service Provider will use to redirect User back to Consumer after obtaining User Authorization has been completed. [Section 6.2.1](http://oauth.net/core/1.0/#auth_step2)*
// - `consumerKey` `String` *The Consumer Key*
// - `consumerSecret` `String` *The Consumer Secret*
// - `version` `String` ___Optional___ *By spec this is `1.0` by default. [Section 6.3.1](http://oauth.net/core/1.0/#auth_step3)*
// - `signatureMethod` `String` *Type of signature to generate, must be one of:*
// - PLAINTEXT
// - RSA-SHA1
// - HMAC-SHA1
// - `nonceLength` `Number` ___Optional___ *Length of nonce string. Default `32`*
// - `headers` `Object` ___Optional___ *Headers to be sent along with request, by default these are already set.*
// - `clientOptions` `Object` ___Optional___ *Contains `requestTokenHttpMethod` and `accessTokenHttpMethod` value.*
// - `parameterSeperator` `String` ___Optional___ *Seperator for OAuth header parameters. Default is `,`*
//
// Example: (javascript)
//
// var OAuth = require('mashape-oauth').OAuth;
// var oa = new OAuth({ /* ... options ... */ }, callback);
//
var OAuth = module.exports = exports = function (options) {
options = options || {};
if (options.echo) {
this.echo = true;
this.verifyCredentials = options.echo.verifyCredentials;
} else {
this.echo = false;
this.requestUrl = options.requestUrl;
this.accessUrl = options.accessUrl;
this.authorizeCallback = options.callback ? options.callback : "oob";
}
this.realm = options.realm || undefined;
this.consumerKey = options.consumerKey;
this.consumerSecret = utils.encodeData(options.consumerSecret);
this.version = options.version;
this.signatureMethod = typeof options.signatureMethod === 'string' ? options.signatureMethod.toUpperCase() : undefined;
this.nonceLength = options.nonceLength || options.nonceSize || 32;
this.customTimestamp = options.timestamp || undefined;
this.customNonce = options.nonce || undefined;
if (this.signatureMethod !== OAuth.signatures.plaintext &&
this.signatureMethod !== OAuth.signatures.hmac &&
this.signatureMethod !== OAuth.signatures.rsa)
throw new Error("Un-supported signature method: " + this.signatureMethod);
if (this.signatureMethod === OAuth.signatures.rsa)
this.privateKey = options.consumerSecret;
this.headers = options.headers || {
"Accept": "*/*",
"Connection": "close",
"User-Agent": "Gatekeeper-OAUTH"
};
this.clientOptions = options.clientOptions || {
"requestTokenHttpMethod": "POST",
"accessTokenHttpMethod": "POST"
};
this._clientOptions = this.clientOptions;
this.parameterSeperator = options.parameterSeperator || ",";
};
OAuth.prototype.setClientOptions = function (options) {
this.clientOptions = utils.extend(this._clientOptions, options);
};
// OAuth 1.0 Signature Enum
OAuth.signatures = {
plaintext: "PLAINTEXT",
hmac: "HMAC-SHA1",
rsa: "RSA-SHA1"
};
// Calculates current timestamp in seconds.
// Utilizes flooring to prevent being sent too soon.
OAuth.getTimestamp = function () {
return Math.floor((new Date()).getTime() / 1000);
};
// Generates randomized string of a certain length given a character table.
//
// - `length` `Number` ___Optional___ *Size of string in character length. Default `32`*
//
// Example: (javascript)
//
// var nonce = OAuth.nonce();
//
OAuth.nonce = function (length) {
var result = [], i = 0; length = length || 32;
for (i; i < length; i++)
result.push(OAuth.nonce.chars[Math.floor(Math.random() * OAuth.nonce.chars.length)]);
return result.join('');
};
// Nonce Character Table.
// By Default this character table is `a-zA-Z0-9`.
OAuth.nonce.chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
// Sorts tuple array by key (0), then value (1) when keys (0) are equal (non-strict).
//
// - `tuple` `Array`
//
// Return: `Array`
OAuth.tupleSorter = function (tuple) {
tuple.sort(function (a, b) {
if (a[0] == b[0]) return a[1] < b[1] ? -1 : 1;
else return a[0] < b[0] ? -1 : 1;
});
return tuple;
};
// Takes an object of key-value store and converts each item into a tuple.
// If they value of a key is an array we iterate upon it creating multiple tuples of that key.
//
// Example: (javascript)
//
// // Would become: [["hello", "world"], ["name", "woah"], ["array", 1], ["array", 2], ["array", 3]];
// var tuple = OAuth.tupleArguments({ "hello": "world", "name": "woah", "array": [1,2,3] });
//
// Return: `Array`
OAuth.tupleArguments = function (args) {
var tuple = [], i = 0, key, value;
for (key in args) {
if (!args.hasOwnProperty(key)) continue;
value = args[key];
if (Array.isArray(value))
for (i; i < value.length; i++)
tuple.push([key, value[i]]);
else
tuple.push([key, value]);
}
return tuple;
};
// Normalizes argument object. First by tupling, encoding & sorting the tuples, and
// finally creating a psuedo-query string of key-value information from the tupled array.
//
// Example: (javascript)
//
// // Would become: array=1&array=2&array=3&hello=world&name=woah
// var tuple = OAuth.tupleArguments({ "hello": "world", "name": "woah", "array": [1,2,3] });
//
// Return: `String`
OAuth.normalizeArguments = function (args) {
var tuple = OAuth.tupleArguments(args), i = 0, output = "";
for (i; i < tuple.length; i++)
tuple[i][0] = utils.encodeData(tuple[i][0]),
tuple[i][1] = utils.encodeData(tuple[i][1]);
tuple = OAuth.tupleSorter(tuple);
for (i = 0; i < tuple.length; i++)
output += tuple[i][0] + "=" + tuple[i][1] + ((i < tuple.length-1) ? "&" : "");
return output;
};
// Formats and normalizes url port, protocol and slashes.
//
// - `url` `String` URL to be normalized
//
// Return: `String`
OAuth.normalizeUrl = function (url) {
var parsed = URL.parse(url, true), port = "";
if (parsed.port)
if ((parsed.protocol === 'http:' && parsed.port != '80') || (parsed.protocol === 'https:' && parsed.port != '443'))
port = ":" + parsed.port;
return parsed.protocol + "//" + parsed.hostname + port + ((!parsed.pathname || parsed.pathname === "") ? "/" : parsed.pathname);
};
// Details whether given parameter is a direct oauth parameter or not.
// We can tell this by checking whether the parameter begins with `oauth_` or not.
//
// - `parameter` `String` *Parameter name or key name*
//
// Return: `Boolean`
OAuth.isOAuthParameter = function (parameter) {
var match = parameter.match('^oauth_');
return (match && (match[0] === "oauth_"));
};
// Generates signature by creating the signature base first then delegating the information to `createSignature`.
//
// Return: `String`
OAuth.prototype.getSignature = function (options) {
return this.createSignature(this.createSignatureBase(options.method, options.url, options.parameters), options.token_secret);
};
// Encodes and normalizes url, parameters, then joins them together with the `&` char, in ordinance of arguments.
//
// - `method` `String` *Request method*
// - `url` `String` *URL being utilized in request*
// - `parameters` `String` *Tupled, encoded and normalized parameters list*
//
// Return: `String`
OAuth.prototype.createSignatureBase = function (method, url, parameters) {
url = utils.encodeData(OAuth.normalizeUrl(url));
parameters = utils.encodeData(parameters);
return method.toUpperCase() + "&" + url + "&" + parameters;
};
// Generates signature by hashing against the base using a key made up of the consumer secret and token secret.
//
// - `base` `String` *Joint string, made up of request method, url, and parameters.*
// - `token_secret` `String`
//
// Return: `String`
OAuth.prototype.createSignature = function (base, token_secret) {
token_secret = (token_secret === undefined) ? "" : utils.encodeData(token_secret);
var key = this.consumerSecret + "&" + token_secret;
if (this.signatureMethod === OAuth.signatures.plaintext)
return key;
else if (this.signatureMethod === OAuth.signatures.rsa)
return crypto.createSign("RSA-SHA1").update(base).sign(this.privateKey || "", 'base64');
else if (crypto.Hmac)
return crypto.createHmac("sha1", key).update(base).digest('base64');
else
return utils.SHA1.hmacSha1(key, base);
};
OAuth.prototype.createClient = function (options) {
return ((options.ssl) ? https : http).request(options);
};
OAuth.prototype.buildAuthorizationHeaders = function (parameters) {
var header = 'OAuth ', realm, i = 0;
for (i; i < parameters.length; i++) if (parameters[i][0] === "realm") realm = parameters[i][1];
if (realm || this.realm) header += 'realm="' + (realm || this.realm) + '",';
for (i = 0; i < parameters.length; i++)
if (OAuth.isOAuthParameter(parameters[i][0]))
header += '' + utils.encodeData(parameters[i][0]) + '="' + utils.encodeData(parameters[i][1]) + '"' + this.parameterSeperator;
return header.substring(0, header.length - this.parameterSeperator.length);
};
OAuth.prototype.buildAuthorizationQuery = function (parameters) {
var query = "", realm, i = 0;
for (i; i < parameters.length; i++)
if (parameters[i][0] === "realm")
realm = parameters[i][1];
if (realm || this.realm)
query += 'realm=' + (realm || this.realm) + '&';
for (i = 0; i < parameters.length; i++)
query += utils.encodeData(parameters[i][0]) + '=' + utils.encodeData(parameters[i][1]) + '&';
return query.substring(0, query.length-1);
};
OAuth.prototype.prepareParameters = function (options) {
var parameters = {}, signature = {}, parsed, key, value, extras, sorted;
if (typeof options.body === "string") {
try {
parameters = JSON.parse(options.body);
} catch (e) {
if (typeof query.parse(options.body) === "object")
parameters = utils.extend(parameters, query.parse(options.body));
}
}
parameters.oauth_timestamp = typeof this.customTimestamp === 'function' ? this.customTimestamp() : OAuth.getTimestamp();
parameters.oauth_nonce = typeof this.customNonce === 'function' ? this.customNonce(this.nonceLength) : OAuth.nonce(this.nonceLength);
parameters.oauth_version = this.version;
parameters.oauth_signature_method = this.signatureMethod;
parameters.oauth_consumer_key = this.consumerKey;
if (typeof options.oauth_token !== 'undefined')
parameters.oauth_token = options.oauth_token;
if (this.echo)
signature = {
method: "GET",
url: this.verifyCredentials,
parameters: OAuth.normalizeArguments(parameters)
};
else {
if (options.parameters)
for (key in options.parameters)
if (options.parameters.hasOwnProperty(key))
parameters[key] = options.parameters[key];
parsed = URL.parse(options.url, false);
if (parsed.query)
extras = query.parse(parsed.query),
parameters = utils.serialExtend(parameters, extras);
signature = { method: options.method, url: options.url, parameters: OAuth.normalizeArguments(parameters) };
}
if (options.oauth_token_secret || options.token_secret)
signature.token_secret = options.oauth_token_secret || options.token_secret;
sorted = OAuth.tupleSorter(OAuth.tupleArguments(parameters));
sorted.push(["oauth_signature", this.getSignature(signature)]);
return sorted;
};
// Correctly handles and parses information required for an OAuth Request
//
// - `options` `Object`
// - `oauth_token` `String` ___Required___
// - `oauth_token_secret` `String` ___Required___
// - `type` `String` *Content Type*
// - `method` `String` *Request Method Type*
// - `realm` `String` *Realm for Echo request or basic request*
// - `url` `String` *Request location*
// - `parameters` `Object` *Extra parameters for body*
// - `body` `Mixed`
//
OAuth.prototype.performSecureRequest = function (options, callback) {
var $this = this, sorted, parsed, data, type = false, headers = {}, request, key, path;
options.type = options.type || "application/x-www-form-urlencoded";
options.method = options.method.toUpperCase();
options.realm = options.realm || this.realm || undefined;
sorted = this.prepareParameters(options);
callback = callback || options.callback;
parsed = URL.parse(options.url, false);
parsed.port = parsed.port || (parsed.protocol === 'http:' ? 80 : 443);
if (this.echo) headers["X-Auth-Service-Provider"] = this.verifyCredentials;
headers[(this.echo) ? "X-Verify-Credentials-Authorization" : "Authorization"] = this.buildAuthorizationHeaders(sorted);
headers.Host = parsed.host;
headers = utils.extend(headers, this.headers);
for (key in options.parameters)
if (options.parameters.hasOwnProperty(key))
if (OAuth.isOAuthParameter(key))
delete options.parameters[key];
if ((options.method === "POST" || options.method === "PUT") && (!options.body && options.parameters))
options.body = query.stringify(options.parameters).
replace(/\!/g, "%21").
replace(/\'/g, "%27").
replace(/\(/g, "%28").
replace(/\)/g, "%29").
replace(/\*/g, "%2A");
if (this.clientOptions.accessTokenHttpMethod === 'GET' && options.url === this.accessUrl) {
delete headers.Authorization;
parsed.query = this.buildAuthorizationQuery(sorted);
}
headers["Content-Length"] = options.body ? Buffer.isBuffer(options.body) ? options.body.length : Buffer.byteLength(options.body) : 0;
headers["Content-Type"] = options.type;
if (!parsed.pathname || parsed.pathname === "") parsed.pathname = "/";
if (parsed.query) path = parsed.pathname + "?" + parsed.query;
else path = parsed.pathname;
request = this.createClient({
port: parsed.port,
host: parsed.hostname,
path: path,
method: options.method,
headers: headers,
ssl: (parsed.protocol === 'https:')
});
if (callback) {
var earlyClose = utils.isAnEarlyCloseHost(parsed.hostname), called = false;
var respond = function (response) {
if (type === 2) data = data.toString('utf8');
if (called) return; else called = true;
if (response.statusCode >= 200 && response.statusCode <= 299)
callback(null, data, response);
else if ((response.statusCode == 301 || response.statusCode == 302) && response.headers && response.headers.location) {
options.url = response.headers.location,
$this.performSecureRequest(options, callback);
} else
callback({
statusCode: response.statusCode,
data: data
}, data, response);
};
request.on('response', function (response) {
var output;
if ($this.clientOptions.detectResponseContentType && utils.isBinaryContent(response)) {
data = new Buffer(0);
type = 1;
output = response;
} else if (response.headers['content-encoding'] === 'gzip') {
var gunzip = zlib.createGunzip();
data = new Buffer(0);
type = 2;
response.pipe(gunzip);
output = gunzip;
} else {
response.setEncoding('utf8');
data = "";
output = response;
}
output.on('data', function (chunk) {
if (type === 1 || type === 2) data = Buffer.concat([data, chunk]);
else data += chunk;
});
output.on('close', function () {
if (earlyClose) respond(response);
});
output.on('end', function () {
respond(response);
});
});
request.on('error', function (error) {
called = true; callback(error);
});
if ((options.method === "POST" || options.method === "PUT") && (options.body && options.body !== ""))
request.write(options.body);
request.end();
} else {
if ((options.method === "POST" || options.method === "PUT") && (options.body && options.body !== ""))
request.write(options.body);
return request;
}
return;
};
OAuth.prototype.handleRequest = function (options, callback) {
return this.performSecureRequest(options, callback);
};
OAuth.prototype.handleRequestLong = function (url, method, token, secret, body, type, parameters, callback) {
return this.handleRequest({
url: url,
method: method,
oauth_token: token || undefined,
oauth_token_secret: secret,
body: body,
type: type,
parameters: typeof parameters === 'function' ? undefined : parameters
}, typeof parameters === 'function' ? parameters : callback);
};
[ 'delete', 'put', 'post', 'get', 'patch' ].forEach(function (k, i) {
OAuth.prototype[k] = function (url, token, secret, body, type, parameters, callback) {
if (typeof token === 'function' || typeof url === 'object') { url.method = k.toUpperCase(); return this.handleRequest(url, token);
} else return this.handleRequestLong(url, k.toUpperCase(), token, secret, body, type, parameters, callback);
};
});
// Create & handles Access Token call while extracting information from response such as Token and Secret.
//
// - `options` `Object`
// - `oauth_verifier` `String` *Verification code tied to the Request Token. [Section 2.3](http://tools.ietf.org/html/rfc5849#section-2.3)*
// - `oauth_token` `String` *Request Token*
// - `oauth_token_secret` `String` *Request Token Secret, used to help generation of signatures.*
// - `parameters` `Object` ___Optional___ *Additional headers to be sent along with request.*
// - `callback` `Function` ___Optional___ *Method to be invoked upon result, over-ridden by argument if set.*
// - `callback` `Function` *Anonymous Function to be invoked upon response or failure, setting this overrides previously set callback inside options object.*
//
// Example: (javascript)
//
// oa.getOAuthRequestToken({/* ... Parameters ... */}, callback);
//
OAuth.prototype.getOAuthAccessToken = function (options, callback) {
callback = options.callback || callback;
options.parameters = options.parameters || {};
if (options.oauth_verifier)
options.parameters.oauth_verifier = options.oauth_verifier,
delete options.oauth_verifier;
options.method = this.clientOptions.accessTokenHttpMethod;
options.url = this.accessUrl;
options.body = null;
options.type = null;
this.performSecureRequest(options, function (error, data, response) {
if (error) return callback(error);
var results = query.parse(data), token = results.oauth_token, secret = results.oauth_token_secret;
delete results.oauth_token; delete results.oauth_token_secret;
callback(null, token, secret, results);
});
};
// Create & handles Request Token call while extracting information from response such as Token and Secret.
//
// - `parameters` `Object` ___Optional___ *Additional Headers you might want to pass along.*
// - *If omitted, you can treat parameters argument as callback and pass along a function as a single parameter.*
// - `callback` `Function` *Anonymous Function to be invoked upon response or failure.*
//
// Example: (javascript)
//
// oa.getOAuthRequestToken({/* ... Parameters ... */}, callback);
//
OAuth.prototype.getOAuthRequestToken = function (parameters, callback) {
if (typeof parameters === 'function')
callback = parameters, parameters = {};
if (this.authorizeCallback)
parameters.oauth_callback = this.authorizeCallback;
this.handleRequestLong(this.requestUrl, this.clientOptions.requestTokenHttpMethod, null, null, null, null, parameters, function (error, data, response) {
if (error) return callback(error);
var results = query.parse(data), token = results.oauth_token, secret = results.oauth_token_secret;
delete results.oauth_token; delete results.oauth_token_secret;
callback(null, token, secret, results);
});
};
OAuth.prototype.getXAuthAccessToken = function (username, password, permissions, callback) {
if (typeof permissions === 'function')
callback = permissions, permissions = undefined;
var parameters = { 'x_auth_mode': 'client_auth', 'x_auth_password': password, 'x_auth_username': username };
if (permissions)
parameters.x_auth_permission = permissions;
this.handleRequestLong(this.accessUrl, this.clientOptions.accessTokenHttpMethod, null, null, null, null, parameters, function (error, data, response) {
var results = query.parse(data), token = results.oauth_token, secret = results.oauth_token_secret;
delete results.oauth_token; delete results.oauth_token_secret;
callback(null, token, secret, results);
});
};
OAuth.prototype.signUrl = function (url, token, secret, method) {
var ordered, parsed = URL.parse(url, false), i = 0, query = "";
ordered = this.prepareParameters({ url: url, oauth_token: token, oauth_token_secret: secret, method: method || "GET" });
for (i; i < ordered.length; i++) query += ordered[i][0] + "=" + utils.encodeData(ordered[i][1]) + "&";
return parsed.protocol + "//" + parsed.host + parsed.pathname + "?" + query.substring(0, query.length-1);
};
OAuth.prototype.authHeader = function (options) {
return this.buildAuthorizationHeaders(this.prepareParameters({
url: options.url,
oauth_token: options.token,
oauth_token_secret: options.secret,
method: options.method || "GET",
body: options.body
}));
};