diff --git a/src/lib/mojaloop-requests/mojaloopRequests.js b/src/lib/mojaloop-requests/mojaloopRequests.js index d216b82a..0b76799d 100644 --- a/src/lib/mojaloop-requests/mojaloopRequests.js +++ b/src/lib/mojaloop-requests/mojaloopRequests.js @@ -22,6 +22,7 @@ const buildUrl = common.buildUrl; const throwOrJson = common.throwOrJson; const JwsSigner = require('../jws').signer; +const WSO2Auth = require('./wso2auth'); /** @@ -58,11 +59,16 @@ class MojaloopRequests { // Switch or peer DFSP endpoint this.peerEndpoint = `${this.transportScheme}://${config.peerEndpoint}`; + + this.wso2Auth = new WSO2Auth({ + ...config.wso2Auth, + logger: config.logger + }); } /** - * Executes a GET /parties request for the specified identifier type and identifier + * Executes a GET /parties request for the specified identifier type and identifier * * @returns {object} - JSON response body if one was received */ @@ -127,7 +133,7 @@ class MojaloopRequests { async putQuotesError(quoteId, error, destFspId) { return this._put(`quotes/${quoteId}/error`, 'quotes', error, destFspId); } - + /** * Executes a POST /transfers request for the specified transfer prepare @@ -157,14 +163,14 @@ class MojaloopRequests { async putTransfersError(transferId, error, destFspId) { return this._put(`transfers/${transferId}/error`, 'transfers', error, destFspId); } - + /** * Utility function for building outgoing request headers as required by the mojaloop api spec * * @returns {object} - headers object for use in requests to mojaloop api endpoints */ - _buildHeaders (method, resourceType, dest) { + async _buildHeaders (method, resourceType, dest) { let headers = { 'content-type': `application/vnd.interoperability.${resourceType}+json;version=1.0`, 'date': new Date().toUTCString(), @@ -176,8 +182,9 @@ class MojaloopRequests { } //Need to populate Bearer Token for WS02 if Sim is pointing to WS02 - if(this.config.wso2BearerToken) { - headers['Authorization'] = `Bearer ${this.config.wso2BearerToken}`; + const token = await this.wso2Auth.getToken(); + if(token) { + headers['Authorization'] = `Bearer ${token}`; } // dont add accept header to PUT requests @@ -193,10 +200,10 @@ class MojaloopRequests { const reqOpts = { method: 'GET', uri: buildUrl(this.peerEndpoint, url), - headers: this._buildHeaders('GET', resourceType, dest), + headers: await this._buildHeaders('GET', resourceType, dest), agent: this.agent, resolveWithFullResponse: true, - simple: false + simple: false }; // Note we do not JWS sign requests with no body i.e. GET requests @@ -216,7 +223,7 @@ class MojaloopRequests { const reqOpts = { method: 'PUT', uri: buildUrl(this.peerEndpoint, url), - headers: this._buildHeaders('PUT', resourceType, dest), + headers: await this._buildHeaders('PUT', resourceType, dest), body: body, agent: this.agent, resolveWithFullResponse: true, @@ -242,7 +249,7 @@ class MojaloopRequests { const reqOpts = { method: 'POST', uri: buildUrl(this.peerEndpoint, url), - headers: this._buildHeaders('POST', resourceType, dest), + headers: await this._buildHeaders('POST', resourceType, dest), body: body, agent: this.agent, resolveWithFullResponse: true, diff --git a/src/lib/mojaloop-requests/wso2auth.js b/src/lib/mojaloop-requests/wso2auth.js new file mode 100644 index 00000000..fd30f8c7 --- /dev/null +++ b/src/lib/mojaloop-requests/wso2auth.js @@ -0,0 +1,87 @@ +/************************************************************************** + * (C) Copyright ModusBox Inc. 2019 - All rights reserved. * + * * + * This file is made available under the terms of the license agreement * + * specified in the corresponding source code repository. * + * * + * ORIGINAL AUTHOR: * + * Yevhen Kyriukha - yevhen.kyriukha@modusbox.com * + **************************************************************************/ + +'use strict'; + +const request = require('request-promise-native'); + +const DEFAULT_REFRESH_INTERVAL_SECONDS = 3600; + +/** + * Obtain WSO2 bearer token and periodically refresh it + */ +class WSO2Auth { + /** + * + * @param {Object} opts + * @param {String} opts.logger + * @param {String} [opts.clientKey] Customer Key + * @param {String} [opts.clientSecret] Customer Secret + * @param {String} [opts.tokenEndpoint] WSO2 Endpoint URL + * @param {String} [opts.refreshSeconds] WSO2 token refresh interval in seconds + * @param {String} [opts.staticToken] WSO2 static bearer token + */ + constructor(opts) { + this.logger = opts.logger; + this.refreshSeconds = opts.refreshSeconds || DEFAULT_REFRESH_INTERVAL_SECONDS; + + if (this.refreshSeconds <= 0) { + throw new Error('WSO2 auth config: token must be a positive integer value'); + } + if (!this.logger) { + throw new Error('WSO2 auth config requires logger property'); + } + + if (opts.tokenEndpoint && opts.clientKey && opts.clientSecret) { + this.basicToken = Buffer.from(`${opts.clientKey}:${opts.clientSecret}`) + .toString('base64'); + this.endpoint = opts.tokenEndpoint; + } else if (opts.staticToken) { + this.logger.info('WSO2 auth config token API data not set, fallback to static token'); + this.token = opts.staticToken; + } else { + // throw new Error('WSO2 auth error: neither token API data nor static token is set'); + this.token = null; + } + } + + async refreshToken() { + this.logger.debug('WSO2 token refresh initiated'); + const reqOpts = { + method: 'POST', + uri: this.endpoint, + headers: { + 'Authorization': `Basic ${this.basicToken}`, + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: { + grant_type: 'client_credentials' + }, + json: true + }; + try { + const response = await request(reqOpts); + this.token = response.access_token; + this.logger.debug('WSO2 token refreshed successfully'); + } catch (error) { + this.logger.error(`Error performing WSO2 token refresh: ${error.cause}`); + } + } + + async getToken() { + if (this.token === undefined && !this.tokenRefreshInterval) { + await this.refreshToken(); + this.tokenRefreshInterval = setInterval(this.refreshToken.bind(this), this.refreshSeconds * 1000); + } + return this.token; + } +} + +module.exports = WSO2Auth; diff --git a/src/package-lock.json b/src/package-lock.json index aadb8911..efa91d7e 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -399,6 +399,42 @@ "arrify": "^1.0.1" } }, + "@sinonjs/commons": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.6.0.tgz", + "integrity": "sha512-w4/WHG7C4WWFyE5geCieFJF6MZkbW4VAriol5KlmQXpAQdxvV0p26sqNZOW6Qyw6Y0l9K4g+cHvvczR2sEEpqg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", + "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", + "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.3.0", + "array-from": "^2.1.1", + "lodash": "^4.17.15" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "@types/events": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", @@ -541,6 +577,12 @@ "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", "dev": true }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, "array-includes": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", @@ -1458,12 +1500,6 @@ } } }, - "defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", - "dev": true - }, "del": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", @@ -1507,6 +1543,12 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, "dir-glob": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", @@ -2348,15 +2390,6 @@ "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", "dev": true }, - "for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "requires": { - "is-callable": "^1.1.3" - } - }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -3727,6 +3760,12 @@ "verror": "1.10.0" } }, + "just-extend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", + "dev": true + }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -3904,6 +3943,12 @@ "chalk": "^2.0.1" } }, + "lolex": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", + "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==", + "dev": true + }, "long": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", @@ -4171,6 +4216,19 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nise": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.2.tgz", + "integrity": "sha512-/6RhOUlicRCbE9s+94qCUsyE+pKlVJ5AhIv+jEE7ESKwnbXqulKZ1FYU+XAtHHWE9TinYvAxDUJAb912PwPoWA==", + "dev": true, + "requires": { + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^4.1.0", + "path-to-regexp": "^1.7.0" + } + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -4525,6 +4583,23 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, "path-type": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", @@ -5013,15 +5088,6 @@ "signal-exit": "^3.0.2" } }, - "resumer": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", - "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", - "dev": true, - "requires": { - "through": "~2.3.4" - } - }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -5138,6 +5204,32 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, + "sinon": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.4.2.tgz", + "integrity": "sha512-pY5RY99DKelU3pjNxcWo6XqeB1S118GBcVIIdDi6V+h6hevn1izcg2xv1hTHW/sViRXU7sUOxt4wTUJ3gsW2CQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.3", + "diff": "^3.5.0", + "lolex": "^4.2.0", + "nise": "^1.5.2", + "supports-color": "^5.5.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "slash": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", @@ -5440,17 +5532,6 @@ } } }, - "string.prototype.trim": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz", - "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.0", - "function-bind": "^1.0.2" - } - }, "string.prototype.trimleft": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.0.0.tgz", @@ -5613,44 +5694,6 @@ } } }, - "tape": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/tape/-/tape-4.11.0.tgz", - "integrity": "sha512-yixvDMX7q7JIs/omJSzSZrqulOV51EC9dK8dM0TzImTIkHWfe2/kFyL5v+d9C+SrCMaICk59ujsqFAVidDqDaA==", - "dev": true, - "requires": { - "deep-equal": "~1.0.1", - "defined": "~1.0.0", - "for-each": "~0.3.3", - "function-bind": "~1.1.1", - "glob": "~7.1.4", - "has": "~1.0.3", - "inherits": "~2.0.4", - "minimist": "~1.2.0", - "object-inspect": "~1.6.0", - "resolve": "~1.11.1", - "resumer": "~0.0.0", - "string.prototype.trim": "~1.1.2", - "through": "~2.3.8" - }, - "dependencies": { - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", - "dev": true - }, - "resolve": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", - "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - } - } - }, "term-size": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", @@ -5803,6 +5846,12 @@ "prelude-ls": "~1.1.2" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", diff --git a/src/package.json b/src/package.json index 26d0c3c8..687e3616 100644 --- a/src/package.json +++ b/src/package.json @@ -1,6 +1,6 @@ { "name": "@mojaloop/sdk-standard-components", - "version": "7.5.0", + "version": "7.5.1", "description": "An set of standard components for connecting to Mojaloop API enabled switches", "main": "index.js", "scripts": { @@ -28,6 +28,7 @@ "ava": "^1.4.1", "eslint": "^5.3.0", "eslint-config-airbnb-base": "^13.1.0", - "eslint-plugin-import": "^2.17.1" + "eslint-plugin-import": "^2.17.1", + "sinon": "^7.4.2" } } diff --git a/src/test/unit/lib/mojaloop-requests/wso2auth.test.js b/src/test/unit/lib/mojaloop-requests/wso2auth.test.js new file mode 100644 index 00000000..a17e04e8 --- /dev/null +++ b/src/test/unit/lib/mojaloop-requests/wso2auth.test.js @@ -0,0 +1,51 @@ +/************************************************************************** + * (C) Copyright ModusBox Inc. 2019 - All rights reserved. * + * * + * This file is made available under the terms of the license agreement * + * specified in the corresponding source code repository. * + * * + * ORIGINAL AUTHOR: * + * Yevhen Kyriukha - yevhen.kyriukha@modusbox.com * + **************************************************************************/ + +const test = require('ava'); +const request = require('request-promise-native'); +const WSO2Auth = require('../../../../lib/mojaloop-requests/wso2auth'); +const sinon = require('sinon'); + +const loggerStub = { + info() {}, + debug() {}, + error() {}, +}; + +test.afterEach(() => { + sinon.restore(); +}); + +test('should return static token when static token was provided', async t => { + const TOKEN = 'abc123'; + const auth = new WSO2Auth({ + logger: loggerStub, + staticToken: TOKEN + }); + t.is(await auth.getToken(), TOKEN); +}); + +test('should return new token when token API info was provided', async t => { + const TOKEN = 'new-token'; + const opts = { + logger: loggerStub, + clientKey: 'client-key', + clientSecret: 'client-secret', + tokenEndpoint: 'token-endpoint' + }; + const basicToken = Buffer.from(`${opts.clientKey}:${opts.clientSecret}`) + .toString('base64'); + sinon.stub(request, 'Request').resolves({access_token: TOKEN}); + const auth = new WSO2Auth(opts); + const token = await auth.getToken(); + t.assert(request.Request.calledOnce); + t.is(request.Request.getCall(0).args[0].headers['Authorization'], `Basic ${basicToken}`); + t.is(token, TOKEN); +});