UNPKG

xero-oauth2.0

Version:

Middleware application to connect Xero by using oAUth2.0 The Code Wok Flow method

564 lines (480 loc) 15 kB
var _ = require('lodash'), querystring = require('querystring'), https = require('https'), http = require('http'), URL = require('url'), OAuthUtils = require('oauth/lib/_utils'); /** * * @param {*} clientId * @param {*} clientSecret * @param {*} redirectURI */ function xeroOAuth2(clientId, clientSecret, redirectURI) { this._clientId = clientId this._clientSecret = clientSecret this._accessTokenName = "Basic"; this._authMethod = "Bearer"; this._redirectURI = redirectURI; } /** * returns the resource uri depend on api * @param {*} api */ function getResourceURI(api) { switch (api) { case 'payroll-au': return 'https://api.xero.com/payroll.xro/1.0'; case 'payroll-nz': return 'https://api.xero.com/payroll.xro/2.0'; case 'payroll-uk': return 'https://api.xero.com/payroll.xro/2.0'; case 'accounts': return `https://api.xero.com/api.xro/2.0`; case 'assets': return 'https://api.xero.com/assets.xro/1.0'; case 'projects': return 'https://api.xero.com/projects.xro/2.0'; case 'bank feeds': return 'https://api.xero.com/bankfeeds.xro/1.0'; case 'connection': return 'https://api.xero.com'; case 'identity': return 'https://identity.xero.com'; case 'consent': return 'https://login.xero.com'; default: throw 'api not found'; } } /** * * @param {*} redirect_uri :String * @param {*} scope:String[] */ function getConsentURL(scope) { if (!scope || scope.length === 0) { return null; } if (typeof scope === 'Array') { scope = scope.join(" "); } var url = ""; try { url = getResourceURI('consent') + `/identity/connect/authorize?response_type=code&client_id=${this._clientId}&redirect_uri=${this._redirectURI}&scope=${scope}&state=123` } catch (err) { return callback(err); }; return url; } /** * * @param {*} code * @param {*} callback */ function getAcceesToken(code, callback) { var params = params || {}; var token = new Buffer(`${this._clientId}:${this._clientSecret}`).toString('base64'); var post_header = { 'Authorization': `Basic ${token}`, 'Content-type': 'application/x-www-form-urlencoded' } params['code'] = code; params['grant_type'] = 'authorization_code'; params['redirect_uri'] = this._redirectURI; var post_data = querystring.stringify(params); var url = ""; try { url = getResourceURI('identity') + `/connect/token` } catch (err) { callback(err); }; this._request('POST', url, post_header, post_data, token, function (err, data) { if (err) { callback(err); } else { var results; try { results = JSON.parse(data); } catch (err) { results = querystring.parse(data); } var access_token = results["access_token"]; var refresh_token = results["refresh_token"]; delete results["refresh_token"]; callback(null, access_token, refresh_token, results) } }); } /** * * @param {*} refreshToken * @param {*} callback */ function refreshToken(refreshToken, callback) { var params = params || {}; var token = new Buffer(`${this._clientId}:${this._clientSecret}`).toString('base64'); var post_header = { 'Authorization': `Basic ${token}`, 'Content-type': 'application/x-www-form-urlencoded' } params['grant_type'] = 'refresh_token'; params['refresh_token'] = refreshToken; var post_data = querystring.stringify(params); var url = ""; try { url = getResourceURI('identity') + `/connect/token` } catch (err) { return callback(err); }; this._request('POST', url, post_header, post_data, token, function (err, data) { if (err) { callback(err); } else { var results; try { results = JSON.parse(data); } catch (err) { results = querystring.parse(data); } var access_token = results["access_token"]; var refresh_token = results["refresh_token"]; delete results["refresh_token"]; callback(null, access_token, refresh_token, results) } }); } /** * * @param {*} acces_token * @param {*} callback */ function getTenants(access_token, callback) { var url = ""; var headers = { 'Authorization': `${this._authMethod} ${access_token}`, 'Accept': 'application/json' } try { url = getResourceURI('connection') + '/connections' } catch (err) { callback(err); }; this._request('GET', url, headers, null, null, function (err, data) { if (err) { callback(err); } else { try { results = JSON.parse(data); } catch (err) { results = querystring.parse(data); } callback(null, results); } }); } /** * disconnect organization * @param {*} tenantId * @param {*} callback */ function disconnect(tenantId, callback) { var url = ""; try { url = getResourceURI('connection') + `/connections/${tenantId}` } catch (err) { callback(err); }; this._request('DELETE', url, null, null, null, function (err, data) { if (err) { callback(err); } else { callback(null, data); } }) } /** * revoke current token * only token with offline access could revoke * @param {*} refreshToken * @param {*} callback */ function revokeToken(refreshToken, callback) { var url = ""; try { url = getResourceURI('identity') + `/connect/revocation` } catch (err) { callback(err); }; var token = new Buffer(`${this._clientId}:${this._clientSecret}`).toString('base64'); var post_header = { 'Authorization': `Basic ${token}`, 'Content-type': 'application/x-www-form-urlencoded' } var post_body = `token=${refreshToken}` this._request('POST', url, post_header, post_body, null, function (err, data) { if (err) { callback(err); } else { callback(null, data); } }) } /** * determines which protocol need to be pick based on url * @param {*} parsedUrl */ function chooseHttpLibrary(parsedUrl) { //default to https var http_library = https; if (parsedUrl.protocol !== "https:") { http_library = http; } return http_library; } /** * * @param {*} method * @param {*} url * @param {*} headers * @param {*} post_body * @param {*} acces_token * @param {*} callback */ function request(method, url, headers, post_body, acces_token, callback) { var parsedUrl = URL.parse(url, true); if (parsedUrl.protocol === "https:" && !parsedUrl.port) { parsedUrl.port = 443; } var http_library = chooseHttpLibrary(parsedUrl); var request_headers = {}; if (headers) { for (var key in headers) { request_headers[key] = headers[key]; } } //set host request_headers['Host'] = parsedUrl.host; if (!request_headers['User-Agent']) { request_headers['User-Agent'] = 'Node-oauth'; } //set request content length if (post_body) { if (Buffer.isBuffer(post_body)) { request_headers['Content-Length'] = post_body.length; } else { request_headers['Content-Length'] = Buffer.byteLength(post_body); } } else { request_headers['Content-Length'] = 0; } // setting up token if not already define in the header if (acces_token && !('Authorization' in request_headers)) { if (!parsedUrl.query) { parsedUrl.query = {}; } parsedUrl.query[this._accessTokenName] = acces_token; } var queryStr = querystring.stringify(parsedUrl.query); if (queryStr) queryStr = "?" + queryStr; var options = { host: parsedUrl.hostname, port: parsedUrl.port, path: parsedUrl.pathname + queryStr, method: method, headers: request_headers } //execute request executeRequest(http_library, options, post_body, callback); } /** * execute request * @param {*} http_library * @param {*} options * @param {*} post_body * @param {*} callback */ function executeRequest(http_library, options, post_body, callback) { var result = ""; var allowEarlyClose = OAuthUtils.isAnEarlyCloseHost(options.host); var callbackCalled = false; function passBackControl(response, result) { if (!callbackCalled) { callbackCalled = true; if (!(response.statusCode >= 200 && response.statusCode <= 299) && (response.statusCode !== 301) && (response.statusCode !== 302)) { callback({ statusCode: response.statusCode, data: result }) } else { callback(null, result, response) } } } var request = http_library.request(options); //on response request.on('response', function (response) { //data response.on('data', function (chunk) { result += chunk; }) //close response.on('close', function (err) { if (allowEarlyClose) { passBackControl(response, result); } }); //end response.addListener('end', function () { passBackControl(response, result); }); }); //on error request.on('error', function (err) { callbackCalled = true; callback(err); }); if ((options.method === 'POST' || options.method === 'PUT') && post_body) { request.write(post_body); } request.end(); } /** * * @param {*} api * @param {*} apiPath * @param {*} access_token * @param {*} tenant_id * @param {*} options * @param {*} callback */ function getRequest(api, apiPath, access_token, tenant_id, options, callback) { var url var headers = { 'Authorization': `${this._authMethod} ${access_token}`, 'Accept': 'application/json' } if (tenant_id) { headers['Xero-tenant-id'] = tenant_id; try { url = getResourceURI(api) + apiPath } catch (err) { callback(err); }; } else { callback({ message: 'tenant id not found' }) } //headers if (options && options.headers) { _.forEach(options.headers, function (item, key) { post_header[key] = item; }) } this._request('GET', url, headers, null, null, function (err, data, response) { if (err) { callback(err,null,response); } else { var results try { results = JSON.parse(data); responseObj = response; } catch (err) { callback(querystring.parse(data),null,response); } callback(null, results, response); } }); } /** * * @param {*} api * @param {*} apiPath * @param {*} post_data * @param {*} access_token * @param {*} tenant_id * @param {*} options * @param {*} callback */ function postRequest(api, apiPath, post_data, access_token, tenant_id, options, callback) { //Payroll - Australia haven't launched the api 2.0 yet hence header had to put xml type var post_header = { 'Authorization': `${this._authMethod} ${access_token}`, 'Xero-tenant-id': `${tenant_id}`, }; var url = null; //headers if (options && options.headers) { _.forEach(options.headers, function (item, key) { post_header[key] = item; }) } try { url = getResourceURI(api) + apiPath } catch (err) { return callback(err); }; this._request('POST', url, post_header, post_data, null, function (err, data, response) { if (err) { callback(err,null,response); } else { var results; try { results = JSON.parse(data); } catch (err) { results = querystring.parse(data); } callback(null, results, response); } }); } /** * * @param {*} apiPath * @param {*} content_type * @param {*} post_data * @param {*} access_token * @param {*} tenant_id * @param {*} options * @param {*} callback */ function postAttachment(apiPath, post_data, access_token, tenant_id, options, callback) { var post_header = { 'Authorization': `${this._authMethod} ${access_token}`, 'Xero-tenant-id': `${tenant_id}`, }; var url = null; //headers if (options && options.headers) { _.forEach(options.headers, function (item, key) { post_header[key] = item; }) } try { url = getResourceURI('accounts') + apiPath; } catch (err) { return callback(err); }; this._request('POST', url, post_header, post_data, null, function (err, data, response) { if (err) { callback(err,null,response); } else { var results; try { results = JSON.parse(data); } catch (err) { results = querystring.parse(data); } callback(null, results, response); } }); } exports.xeroOAuth2 = xeroOAuth2; exports.xeroOAuth2.prototype._getConsentURL = getConsentURL; exports.xeroOAuth2.prototype._getAccessToken = getAcceesToken; exports.xeroOAuth2.prototype._refreshToken = refreshToken; exports.xeroOAuth2.prototype._getTenants = getTenants; exports.xeroOAuth2.prototype._disconnect = disconnect; exports.xeroOAuth2.prototype._revokeToken = revokeToken; exports.xeroOAuth2.prototype._chooseHttpLibrary = chooseHttpLibrary; exports.xeroOAuth2.prototype._request = request; exports.xeroOAuth2.prototype._executeRequest = executeRequest; exports.xeroOAuth2.prototype._get = getRequest; exports.xeroOAuth2.prototype._post = postRequest; exports.xeroOAuth2.prototype._postAttachment = postAttachment;