ibm-cos-sdk
Version:
IBM SDK for JavaScript
402 lines (362 loc) • 15.6 kB
JavaScript
var querystring = require('querystring');
var AWS = require('../../lib/core');
/**
* Create a `Token` object given the access token and refresh token
* @param {Object} token
* @param {string} token.accessToken The access token needed to validate I/O operations.
* @param {string} token.refreshToken The token used to refresh the access token on expiration.
* @param {number} token.expiration Expiry time of the token.
* @param {string} [token.tokenType] Type of token being used. Currently only `bearer` is supported. Defaults
* to `bearer`.
* @returns {Token}
*/
function Token(token) {
if (!token.accessToken) {
throw new Error('Token constructor must have an access token provided.');
}
this.tokenType = token.tokenType || 'Bearer';
this.accessToken = token.accessToken;
this.refreshToken = token.refreshToken;
if (typeof token.expiration === 'number') {
this.expiration = new Date(token.expiration);
} else {
this.expiration = token.expiration;
}
}
/**
* Check if the token is expired. If a grace period is specified, will return true if it's almost expired.
* @param {number} [gracePeriod] The amount of milliseconds within which a token will be considered "expired".
* @returns {boolean}
*/
Token.prototype.isExpired = function(gracePeriod) {
return (!this.expiration || (Date.now() > +this.expiration - gracePeriod));
};
/**
* The IBM IAM Token Manger. This class is responsible for authorizing, maintaining, and refreshing IAM tokens for
* services that require them. It can be initialized with either an IAM API Key, or a currently valid token
* directly. If a token is provided directly, it is suggested that the refresh token be provided alongside it, so
* that the token manager can keep valid tokens throughout its lifetime.
*
* @param {Object} [config] Configuration for the token manager.
* @param {string} [config.ibmAuthEndpoint] Endpoint to request IAM token from. Defaults to
* 'https://iam.cloud.ibm.com/identity/token'.
* @param {string} [config.apiKeyId] IAM API Key to use to request tokens. If not provided, a token must be set
* directly.
* @param {string} [config.token] A valid IAM token to use, in the case that an IAM API Key cannot be provided.
* @param {string} [config.refreshToken] A refresh token which can be used to refresh an IAM token when the IAM
* token is about to expire, in the case that an IAM API Key cannot be provided.
* @param {number} [config.tokenExpiration] Time until a token/refreshToken pair expires, in the case that an IAM
* API Key cannot be provided.
* @param {TokenManager~authCallback} [config.authCall] Callback function to be called when a mechanism to retrieve
* an authentication key is required.
*/
function TokenManager() {
var options = {};
var config = {};
var authCallback;
if (typeof arguments[0] === 'object') {
options = arguments[0];
} else if (typeof arguments[0] === 'function') {
authCallback = arguments[0];
}
this.DEFAULT_HEADERS = {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic Yng6Yng=', // 'bx:bx'
'cache-control': 'no-cache'
};
// Default config
config = {
ibmAuthEndpoint: 'https://iam.cloud.ibm.com/identity/token',
apiKeyId: null,
token: null,
refreshToken: null,
tokenExpiration: null,
authCallback: authCallback
};
// Check that we have some sort of authentication mechanism
if (!(options.apiKeyId || options.token || (authCallback || options.authCallback))) {
throw new Error('An authentication mechanism must be provided to the IAM token manager. This could be ' +
'either an API Key, a valid API token, or a custom authentication callback.');
}
// Check that we still have an endpoint. This can happen if `null` or `''` is passed in by accident.
if (typeof options.ibmAuthEndpoint !== 'undefined' && !options.ibmAuthEndpoint) {
throw new Error('Endpoint must not be null or empty string.');
}
// Merge passed in config with default config
AWS.util.each(options, function(key, value) {
if (value) {
config[key] = value;
}
});
// API Key is "hidden" from arrayEach
config.apiKeyId = options.apiKeyId;
this.config = config;
}
/**
* Return the cached token synchronously. No guarantee is made as to its validity. This would usually be called
* right after refreshToken().
* @returns {Token}
*/
TokenManager.prototype.getToken = function() {
return this.token;
};
function generateTokenRefreshRequest(config, callbacks, body) {
var tokenRequestConfig = {
uri: config.ibmAuthEndpoint,
headers: config.headers,
body: body
};
AWS.util.update(tokenRequestConfig, this.config.httpOptions);
if (typeof tokenRequestConfig.body === 'object') {
tokenRequestConfig.body = JSON.stringify(tokenRequestConfig.body);
}
var httpRequest = new AWS.HttpRequest(tokenRequestConfig.uri, '');
httpRequest.headers = tokenRequestConfig.headers;
httpRequest.body = tokenRequestConfig.body;
var httpRequestOptions = {
httpOptions: this.config.httpOptions,
maxRetries : 2,
errorMessage: 'Unable to get IAM token'
};
var sendRefreshTokenRequest = function(resolve, reject) {
AWS.util.handleRequestWithRetries(httpRequest, httpRequestOptions, function(requestErr, response) {
if (requestErr) {
callbacks(requestErr);
if (reject) {
return reject(requestErr);
}
return;
}
try {
var token = JSON.parse(response);
var newToken;
try {
newToken = this.createToken({
tokenType: token.token_type && token.token_type.toLowerCase(),
accessToken: token.access_token || token.uaa_token,
refreshToken: token.refresh_token,
expiration: token.expiration * 1000 // Expiration is returned in seconds; convert to epoch here.
});
} catch (e) {
var err = new AWS.util.error(e, new Error(
'Expected valid token in IAM authentication request. Received: ' + token));
callbacks(err);
if (reject) {
return reject(err);
}
return;
}
callbacks(null, newToken);
if (resolve) {
return resolve(newToken);
}
} catch (createTokenErr) {
callbacks(createTokenErr);
if (reject) {
return reject(createTokenErr);
}
}
}.bind(this));
}.bind(this);
return sendRefreshTokenRequest;
}
function handleAuthCallback(config, callbacks) {
return function(resolve, reject) {
var _callback = function(token) {
try {
var t = this.createToken(token);
callbacks(null, token);
if (resolve) {
return resolve(t);
}
} catch (createTokenErr) {
callbacks(createTokenErr);
if (reject) {
return reject(createTokenErr);
}
}
}.bind(this);
var postCallback = config.authCallback(_callback);
if (postCallback && postCallback.then) {
postCallback.then(function(token) {
try {
var t = this.createToken(token);
callbacks(null, token);
if (resolve) {
return resolve(t);
}
} catch (createTokenErr) {
callbacks(createTokenErr);
if (reject) {
return reject(createTokenErr);
}
}
}.bind(this))
.catch(function(err) {
callbacks(err);
if (reject) {
return reject(err);
}
}.bind(this));
}
}.bind(this);
}
/**
* Retrieve a new token from the IAM endpoint.
* @param {Object} [options] Optional overrides for the request.
* @param {string} [options.ibmAuthEndpoint]
* @param {string} [options.redirectUrl]
* @param {string} [options.apiKeyId]
* @param {string} [options.sslEnabled]
* @param {Object} [options.httpOptions] Options to pass through to network requests.
* @param {TokenManager~authCallback} [options.authCallback] Callback function to be called when a mechanism to retrieve
* an authentication key is required. Should return a Valid Token.
* @param {object} [options.authCallback.config] The TokenManager's current configuration
* @param {function} [options.authCallback.callback] Callback to call when finished, if a Promise is not returned
* @param {boolean} [forceRenew] Renew the token even if it not about to expire.
* @param {TokenManager~refreshTokenCallback} [callback] function to call when the token is renewed.
* @param {Object} [callback.error] An error response, if encountered
* @param {Token} [callback.token] A valid token, if there is no error
* @returns {Promise} A promise which will return the token, if Promise support is detected.
*/
TokenManager.prototype.refreshToken = function(options, forceRenew, callback) {
if (typeof callback === 'undefined') {
if (typeof forceRenew === 'function') {
callback = forceRenew;
forceRenew = undefined;
} else if (typeof forceRenew === 'undefined' && typeof options === 'function') {
callback = options;
options = undefined;
}
}
var callbacks = function() {
var args = arguments;
AWS.util.arrayEach(this._callbacks, function(cb) {
cb.apply(this, args);
}.bind(this));
this._callbacks = [];
}.bind(this);
var sdkPromise = AWS.config.getPromisesDependency() || (typeof Promise === 'function' && Promise);
var config = AWS.util.merge(this.config, options);
config.headers = AWS.util.merge(this.DEFAULT_HEADERS, config.headers);
var rejectOrCallbackError = function(message) {
var err = new Error(message);
if (this._callbacks) {
this._callbacks.forEach(function (cb) {
cb(err);
});
} else if (callback) {
callback(err)
}
if (sdkPromise !== undefined) {
return sdkPromise.reject(err);
}
this._callbacks = [];
}.bind(this);
if (!config.apiKeyId && !config.token && !config.authCallback) {
return rejectOrCallbackError('Client API Key or access token must be provided to retrieve a token.');
}
if (!config.token && !config.ibmAuthEndpoint) {
return rejectOrCallbackError('IAM Endpoint is required to fetch a token');
}
if (!forceRenew && this.token && !this.token.isExpired(15000)) {
if (typeof callback === 'function') {
return callback(null, this.token);
} else if (sdkPromise !== undefined) {
return sdkPromise.resolve(this.token);
}
}
var grantType;
if (config.refreshToken) {
grantType = 'refresh_token';
} else if (!config.apiKeyId && config.ltpacookie) {
grantType = 'grant_type=urn:ibm:params:oauth:grant-type:identity-cookie';
} else {
grantType = 'urn:ibm:params:oauth:grant-type:apikey';
}
var body = querystring.stringify({
grant_type: grantType,
response_type: 'cloud_iam',
apikey: config.apiKeyId,
redirect_uri: config.redirectUri,
client_id: config.apiKeyId,
refresh_token: config.refreshToken
});
if (config.query) {
var query = querystring.stringify(config.query);
config.url = (config.url.indexOf('?') === -1 ? '?' : '&') + query;
}
// Cache auth requests to _callbacks so we don't send multiple authentication requests at once. Similar to
// how we handle multiple requests with promises, except we have to track multiple callbacks instead of being
// able to rely on native Promise handling.
if (callback) {
if (!this._callbacks) {
this._callbacks = [callback];
} else {
this._callbacks.push(callback);
}
}
// Cache any auth requests to _tokenRequest so we don't suddenly send many authentication requests when the
// token is or is almost expired. When any auth requests are triggered when one is pending, return the
// already pending request.
if (this._tokenRequest) {
return this._tokenRequest;
}
if (typeof config.authCallback === 'function') {
var authFn = handleAuthCallback.call(this, config, callbacks);
if (sdkPromise) {
this._tokenRequest = new Promise(authFn).then(function(token) {
this._tokenRequest = undefined;
return token;
}.bind(this));
return this._tokenRequest;
} else {
return authFn();
}
}
// Otherwise, use the default IAM API key method.
var sendRefreshTokenRequest = generateTokenRefreshRequest.call(this, config, callbacks, body);
if (sdkPromise) {
this._tokenRequest = new sdkPromise(sendRefreshTokenRequest).then(function(token) {
this._tokenRequest = undefined;
return token;
}.bind(this)).catch(function(err) {
this._tokenRequest = undefined;
return err;
}.bind(this));
return this._tokenRequest;
} else {
return sendRefreshTokenRequest();
}
};
/**
* Create and cache a `Token` object given the access token, a refresh token, and the token's expiration time.
* @param {Object} tokenConfig
* @param {string} tokenConfig.accessToken The access token needed to validate I/O operations.
* @param {string} tokenConfig.refreshToken The token used to refresh the access token on expiration.
* @param {Number} tokenConfig.expiration Expiry time of the token, as Unix epoch time.
* @param {string} [tokenConfig.tokenType] Type of token being used. Currently only `bearer` is supported. Defaults
* to `bearer`.
* @returns {Token}
*/
TokenManager.prototype.createToken = function(tokenConfig) {
// Cache token
this.token = new Token(tokenConfig);
// Refresh the token 75% of the way through its validity period.
// If the refresh fails, try twice more after 30 seconds each.
if (this.token.expiration) {
var refreshTime = (+this.token.expiration - Date.now()) * 0.75;
var t1 = setTimeout(function preemptiveRefresh(attempt) {
this.refreshToken(function(err) {
if (err && attempt < 2) {
var t = setTimeout(preemptiveRefresh, 30000, attempt + 1);
t.unref();
}
});
}.bind(this), refreshTime);
t1.unref();
}
return this.token;
};
module.exports = TokenManager;