diff --git a/lib/client.js b/lib/client.js index fcc553b..63678ce 100644 --- a/lib/client.js +++ b/lib/client.js @@ -85,7 +85,7 @@ class Client { // instantiate the promised swagger client Object.defineProperty(this, - 'swaggerClient', { + '_swaggerClient', { configurable: true, enumerable: false, value: this.createSwaggerClient(), @@ -93,6 +93,20 @@ class Client { }); } + /** + * Return the swagger client, after updating token. + * All access to the client must go through this. + * @private + */ + get swaggerClient() { + if(this.updateToken) { + return this._swaggerClient + .then(this.updateToken); + } else { + return this._swaggerClient; + } + } + /** * Version number of the REST service used. Currently ‘V2’. */ @@ -125,6 +139,7 @@ class Client { url: schemaUrl, requestInterceptor: (req) => gpiam.apply(req) }); + this.updateToken = gpiam.updateToken; // Call this before using the client. return clientPromise; } else { // assume GP credentials diff --git a/lib/gp-iam.js b/lib/gp-iam.js index 63f59f5..49da8a1 100644 --- a/lib/gp-iam.js +++ b/lib/gp-iam.js @@ -15,6 +15,19 @@ */ /* eslint no-console: "off" */ +const { URL } = require('url'); +const bent = require('bent'); +const querystring = require('querystring'); +const grant_type = 'urn:ibm:params:oauth:grant-type:apikey'; +const tokenExpiryThreshold= process.env.IAM_TOKEN_EXPIRY_THRESHOLD_PROP_KEY || 0.85; +const fetchToken = bent('json', // JSON response + 'POST', // POST method + { + // headers + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json' + }, + 200); // expect 200 /** * Manage use of IAM API Keys and Tokens @@ -29,12 +42,59 @@ const GpIAM = function GpIAM(credentials) { if(!this.credentials || !this.credentials.apikey || !this.credentials.iam_endpoint) { throw new Error('GpIAM: params need to be "apikey, iam_endpoint"'); } + this.tokenUrl = new URL('/identity/token', this.credentials.iam_endpoint); + + if(!this.GP_USE_APIKEY) { + const now = new Date(); + this.ourCacheKey = `${this.credentials.iam_endpoint}#${this.credentials.apikey}`; + + this.updateToken = (async function updateToken(x) { + const apikey = this.credentials.apikey; + const cacheEntry = GpIAM.prototype.tokenCache[this.ourCacheKey] = + GpIAM.prototype.tokenCache[this.ourCacheKey] || {/* initialized to empty */}; + // if(this.VERBOSE) { + // const tokenn = token.n = (token.n || 0) + 1; + // console.log(`Token count: ${tokenn} ${this.ourCacheKey}`); + // } + + if (!cacheEntry.validUntil || // no token + (now > cacheEntry.validUntil)) { // or expired + + delete this.access_token; // in case we run into a failure. + const tokenResponse = await fetchToken(this.tokenUrl, Buffer.from(querystring.stringify({ + grant_type, + apikey + }))); + + // save the entire response + cacheEntry.tokenResponse = tokenResponse; + + // get the response time + const { expires_in } = tokenResponse; + + cacheEntry.validUntil = new Date(now.getTime() + (1000 * expires_in * tokenExpiryThreshold)); + if(this.VERBOSE) { + console.log('Fetched access token '); + } + } + if(this.VERBOSE) { + console.log(`Token valid until ${cacheEntry.validUntil}`); + } + + // save off the access token + this.access_token = cacheEntry.tokenResponse.access_token; + + return x; + }).bind(this); + } }; +GpIAM.prototype.tokenCache = {}; + GpIAM.prototype.API_KEY = "API-KEY"; // GP SPECIFIC header GpIAM.prototype.VERBOSE = process.env.GP_VERBOSE || false; -GpIAM.prototype.GP_USE_APIKEY = process.env.GP_USE_APIKEY || true; // if false: use token manager +GpIAM.prototype.GP_USE_APIKEY = process.env.GP_USE_APIKEY || false; // if false: use token manager /** * Generate HTTP Authorization header. @@ -43,9 +103,15 @@ GpIAM.prototype.apply = function(obj) { if(this.VERBOSE) console.dir(obj, {color: true, depth: null}); if(obj.url.indexOf("/swagger.json") !== -1) return obj; // skip for swagger.json - const authHeader = this.API_KEY + ' ' + this.credentials.apikey; - if(this.VERBOSE) console.log('Authorization: ' + authHeader.replace(this.credentials.apikey, '****')); - obj.headers.Authorization = authHeader; + if(this.access_token) { + const authHeader = 'Bearer ' + this.access_token; + if(this.VERBOSE) console.log('Authorization: ' + authHeader.replace(this.access_token, '****')); + obj.headers.Authorization = authHeader; + } else { + const authHeader = this.API_KEY + ' ' + this.credentials.apikey; + if(this.VERBOSE) console.log('Authorization: ' + authHeader.replace(this.credentials.apikey, '****')); + obj.headers.Authorization = authHeader; + } return obj; }; diff --git a/package-lock.json b/package-lock.json index dfff04e..cf683b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -440,6 +440,25 @@ "tweetnacl": "^0.14.3" } }, + "bent": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/bent/-/bent-1.4.0.tgz", + "integrity": "sha512-nY7O/AsbX4OI05JdLQVYQD1cjB3fKPoCvOQOuZO5XyPlMW6pPKOUy1IVbW3DcurF7q5EjV8zNjfHgxNamZn1Ow==", + "requires": { + "bl": "^2.1.2", + "caseless": "^0.12.0", + "is-stream": "^1.1.0" + } + }, + "bl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.0.tgz", + "integrity": "sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, "bluebird": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", @@ -526,6 +545,11 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, "catharsis": { "version": "0.8.9", "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.9.tgz", @@ -807,8 +831,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "coveralls": { "version": "3.0.1", @@ -1505,26 +1528,6 @@ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", "dev": true }, - "form-data": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.1.tgz", - "integrity": "sha1-rjFduaSQf6BlUCMEpm13M0de43w=", - "requires": { - "async": "^2.0.1", - "combined-stream": "^1.0.5", - "mime-types": "^2.1.11" - }, - "dependencies": { - "async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", - "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", - "requires": { - "lodash": "^4.17.11" - } - } - } - }, "fs-then-native": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fs-then-native/-/fs-then-native-2.0.0.tgz", @@ -1812,8 +1815,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "inquirer": { "version": "3.3.0", @@ -1957,8 +1959,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -1972,6 +1973,26 @@ "integrity": "sha1-Am9ifgMrDNhBPsyHVZKLlKRosGI=", "requires": { "form-data": "^1.0.0-rc3" + }, + "dependencies": { + "async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", + "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", + "requires": { + "lodash": "^4.17.11" + } + }, + "form-data": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.1.tgz", + "integrity": "sha1-rjFduaSQf6BlUCMEpm13M0de43w=", + "requires": { + "async": "^2.0.1", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.11" + } + } } }, "isstream": { @@ -3130,8 +3151,7 @@ "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "progress": { "version": "2.0.0", @@ -3201,7 +3221,6 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -3512,8 +3531,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "semver": { "version": "5.5.0", @@ -3739,7 +3757,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -4049,8 +4066,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { "version": "3.2.1", diff --git a/package.json b/package.json index 6f16d57..c1ad103 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "request": "^2.87.0" }, "dependencies": { + "bent": "^1.4.0", "g11n-pipeline-flatten": "^2.0.0", "minimist": "^1.2.0", "swagger-client": "^3.8.25"