ibm-cloud-sdk-core
Version:
Core functionality to support SDKs generated with IBM's OpenAPI SDK Generator.
177 lines (176 loc) • 6.94 kB
JavaScript
/* eslint-disable @typescript-eslint/no-unused-vars, class-methods-use-this */
import { stripTrailingSlash } from '../../lib/helper';
import logger from '../../lib/logger';
import { RequestWrapper } from '../../lib/request-wrapper';
import { getCurrentTime } from '../utils/helpers';
/**
* A class for shared functionality for storing, and requesting tokens.
* Intended to be used as a parent to be extended for token request management.
* Child classes should implement "requestToken()" to retrieve the token
* from intended sources and "saveTokenInfo(tokenResponse)" to parse and save
* token information from the response.
*/
export class TokenManager {
/**
* Create a new TokenManager instance.
*
* @param options - Configuration options.
* This should be an object containing these fields:
* - url: (optional) the endpoint URL for the token service
* - disableSslVerification: (optional) a flag that indicates whether verification of the token server's SSL certificate
* should be disabled or not
* - headers: (optional) a set of HTTP headers to be sent with each request to the token service
*/
constructor(options) {
// all parameters are optional
options = options || {};
if (options.url) {
this.url = stripTrailingSlash(options.url);
}
// request options
this.disableSslVerification = Boolean(options.disableSslVerification);
this.headers = options.headers || {};
// any config options for the internal request library, like `proxy`, will be passed here
this.requestWrapperInstance = new RequestWrapper(options);
// Array of requests pending completion of an active token request -- initially empty
this.pendingRequests = [];
}
/**
* Retrieves a new token using "requestToken()" if there is not a
* currently stored token from a previous call, or the previous token
* has expired.
*/
getToken() {
if (!this.accessToken || this.isTokenExpired()) {
// 1. Need a new token.
logger.debug('Performing synchronous token refresh');
return this.pacedRequestToken().then(() => this.accessToken);
}
if (this.tokenNeedsRefresh()) {
// 2. Need to refresh the current (valid) token.
logger.debug('Performing background asynchronous token fetch');
this.requestToken().then((tokenResponse) => {
this.saveTokenInfo(tokenResponse);
}, (err) => {
// If the refresh request failed: catch the error, log a message, and return the stored token.
// The attempt to get a new token will be retried upon the next request.
let message = 'Attempted token refresh failed. The refresh will be retried with the next request.';
if (err && err.message) {
message += ` ${err.message}`;
}
logger.error(message);
logger.debug(err);
});
}
else {
logger.debug('Using cached access token');
}
return Promise.resolve(this.accessToken);
}
/**
* Sets the "disableSslVerification" property.
*
* @param value - the new value for the disableSslVerification property
*/
setDisableSslVerification(value) {
// if they try to pass in a non-boolean value,
// use the "truthy-ness" of the value
this.disableSslVerification = Boolean(value);
}
/**
* Sets the headers to be included with each outbound request to the token server.
*
* @param headers - the set of headers to send with each request to the token server
*/
setHeaders(headers) {
if (typeof headers !== 'object') {
// do nothing, for now
return;
}
this.headers = headers;
}
/**
* Paces requests to requestToken().
*
* This method pseudo-serializes requests for an access_token
* when the current token is undefined or expired.
* The first caller to this method records its `requestTime` and
* then issues the token request. Subsequent callers will check the
* `requestTime` to see if a request is active (has been issued within
* the past 60 seconds), and if so will queue their promise for the
* active requestor to resolve when that request completes.
*/
pacedRequestToken() {
const currentTime = getCurrentTime();
if (this.requestTime > currentTime - 60) {
// token request is active -- queue the promise for this request
return new Promise((resolve, reject) => {
this.pendingRequests.push({ resolve, reject });
});
}
this.requestTime = currentTime;
return this.requestToken()
.then((tokenResponse) => {
this.saveTokenInfo(tokenResponse);
this.pendingRequests.forEach(({ resolve }) => {
resolve();
});
this.pendingRequests = [];
this.requestTime = 0;
})
.catch((err) => {
this.pendingRequests.forEach(({ reject }) => {
reject(err);
});
throw err;
});
}
/**
* Request a token using an API endpoint.
*
* @returns Promise
*/
requestToken() {
const errMsg = '`requestToken` MUST be overridden by a subclass of TokenManager.';
const err = new Error(errMsg);
logger.error(errMsg);
return Promise.reject(err);
}
/**
* Parse and save token information from the response.
* Save the requested token into field `accessToken`.
* Calculate expiration and refresh time from the received info
* and store them in fields `expireTime` and `refreshTime`.
*
* @param tokenResponse - the response object from a token service request
*/
saveTokenInfo(tokenResponse) {
const errMsg = '`saveTokenInfo` MUST be overridden by a subclass of TokenManager.';
logger.error(errMsg);
}
/**
* Checks if currently-stored token is expired
*/
isTokenExpired() {
const { expireTime } = this;
if (!expireTime) {
return true;
}
const currentTime = getCurrentTime();
return expireTime <= currentTime;
}
/**
* Checks if currently-stored token should be refreshed
* i.e. past the window to request a new token
*/
tokenNeedsRefresh() {
const { refreshTime } = this;
const currentTime = getCurrentTime();
if (refreshTime && refreshTime > currentTime) {
return false;
}
// Update refreshTime to 60 seconds from now to avoid redundant refreshes
this.refreshTime = currentTime + 60;
return true;
}
}