box-node-sdk
Version:
Official SDK for Box Plaform APIs
239 lines • 12.3 kB
JavaScript
"use strict";
/**
* @fileoverview A Persistent Box API Session.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
var assert_1 = __importDefault(require("assert"));
var bluebird_1 = require("bluebird");
var http_status_1 = __importDefault(require("http-status"));
var errors_1 = __importDefault(require("../util/errors"));
// ------------------------------------------------------------------------------
// Private
// ------------------------------------------------------------------------------
/**
* Validate that an object is a valid TokenInfo object
*
* @param {Object} obj The object to validate
* @returns {boolean} True if the passed in object is a valid TokenInfo object that
* has all the expected properties, false otherwise
* @private
*/
function isObjectValidTokenInfo(obj) {
return Boolean(obj &&
obj.accessToken &&
obj.refreshToken &&
obj.accessTokenTTLMS &&
obj.acquiredAtMS);
}
/**
* Validate that an object is a valid TokenStore object
*
* @param {Object} obj the object to validate
* @returns {boolean} returns true if the passed in object is a valid TokenStore object that
* has all the expected properties. false otherwise.
* @private
*/
function isObjectValidTokenStore(obj) {
return Boolean(obj && obj.read && obj.write && obj.clear);
}
// ------------------------------------------------------------------------------
// Public
// ------------------------------------------------------------------------------
/**
* A Persistent API Session has the ability to refresh its access token once it becomes expired.
* It takes in a full tokenInfo object for authentication. It can detect when its tokens have
* expired and will request new, valid tokens if needed. It can also interface with a token
* data-store if one is provided.
*
* Persistent API Session a good choice for long-running applications or web servers that
* must remember users across sessions.
*
* @param {TokenInfo} tokenInfo A valid TokenInfo object. Will throw if improperly formatted.
* @param {TokenStore} [tokenStore] A valid TokenStore object. Will throw if improperly formatted.
* @param {Config} config The SDK configuration options
* @param {TokenManager} tokenManager The token manager
* @constructor
*/
var PersistentSession = /** @class */ (function () {
function PersistentSession(tokenInfo, tokenStore, config, tokenManager) {
this._config = config;
this._tokenManager = tokenManager;
// Keeps track of if tokens are currently being refreshed
this._refreshPromise = null;
// Set valid PersistentSession credentials. Throw if expected credentials are invalid or not given.
(0, assert_1.default)(isObjectValidTokenInfo(tokenInfo), 'tokenInfo is improperly formatted. Properties required: accessToken, refreshToken, accessTokenTTLMS and acquiredAtMS.');
this._setTokenInfo(tokenInfo);
// If tokenStore was provided, set the persistent data & current store operations
if (tokenStore) {
(0, assert_1.default)(isObjectValidTokenStore(tokenStore), 'Token store provided but is improperly formatted. Methods required: read(), write(), clear().');
this._tokenStore = bluebird_1.Promise.promisifyAll(tokenStore);
}
}
/**
* Sets all relevant token info for this client.
*
* @param {TokenInfo} tokenInfo A valid TokenInfo object.
* @returns {void}
* @private
*/
PersistentSession.prototype._setTokenInfo = function (tokenInfo) {
this._tokenInfo = {
accessToken: tokenInfo.accessToken,
refreshToken: tokenInfo.refreshToken,
accessTokenTTLMS: tokenInfo.accessTokenTTLMS,
acquiredAtMS: tokenInfo.acquiredAtMS,
};
};
/**
* Attempts to refresh tokens for the client.
* Will use the Box refresh token grant to complete the refresh. On refresh failure, we'll
* check the token store for more recently updated tokens and load them if found. Otherwise
* an error will be propagated.
*
* @param {TokenRequestOptions} [options] - Sets optional behavior for the token grant
* @returns {Promise<string>} Promise resolving to the access token
* @private
*/
PersistentSession.prototype._refreshTokens = function (options) {
var _this = this;
// If not already refreshing, kick off a token refresh request and set a lock so that additional
// client requests don't try as well
if (!this._refreshPromise) {
this._refreshPromise = this._tokenManager
.getTokensRefreshGrant(this._tokenInfo.refreshToken, options)
.catch(function (err) {
// If we got an error response from Box API, but it was 400 invalid_grant, it indicates we may have just
// made the request with an invalidated refresh token. Since only a max of 2 refresh tokens can be valid
// at any point in time, and a horizontally scaled app could have multiple Node instances running in parallel,
// it is possible to hit cases where too many servers all refresh a user's tokens at once
// and cause this server's token to become invalidated. However, the user should still be alive, but
// we'll need to check the central data store for the latest valid tokens that some other server in the app
// cluster would have received. So, instead pull tokens from the central store and attempt to use them.
if (err.statusCode === http_status_1.default.BAD_REQUEST &&
_this._tokenStore) {
var invalidGrantError = err;
// Check the tokenStore to see if tokens have been updated recently. If they have, then another
// instance of the session may have already refreshed the user tokens, which would explain why
// we couldn't refresh.
return _this._tokenStore
.readAsync()
.catch(function (e) { return errors_1.default.unwrapAndThrow(e); })
.then(function (storeTokenInfo) {
// if the tokens we got from the central store are the same as the tokens we made the failed request with
// already, then we can be sure that no other servers have valid tokens for this server either.
// Thus, this user truly has an expired refresh token. So, propagate an "Expired Tokens" error.
if (!storeTokenInfo ||
storeTokenInfo.refreshToken === _this._tokenInfo.refreshToken) {
throw errors_1.default.buildAuthError(invalidGrantError.response);
}
// Propagate the fresh tokens that we found in the session
return storeTokenInfo;
});
}
// Box API returned a permanent error that is not retryable and we can't recover.
// We have no usable tokens for the user and no way to refresh them - propagate a permanent error.
throw err;
})
.then(function (tokenInfo) {
// Success! We got back a TokenInfo object from the API.
// If we have a token store, we'll write it there now before finishing up the request.
if (_this._tokenStore) {
return _this._tokenStore
.writeAsync(tokenInfo)
.catch(function (e) { return errors_1.default.unwrapAndThrow(e); })
.then(function () { return tokenInfo; });
}
// If no token store, Set and propagate the access token immediately
return tokenInfo;
})
.then(function (tokenInfo) {
// Set and propagate the new access token
_this._setTokenInfo(tokenInfo);
return tokenInfo.accessToken;
})
.catch(function (err) { return _this.handleExpiredTokensError(err); })
.finally(function () {
// Refresh complete, clear promise
_this._refreshPromise = null;
});
}
return this._refreshPromise;
};
// ------------------------------------------------------------------------------
// Public Instance
// ------------------------------------------------------------------------------
/**
* Returns the clients access token.
*
* If tokens don't yet exist, first attempt to retrieve them.
* If tokens are expired, first attempt to refresh them.
*
* @param {TokenRequestOptions} [options] - Sets optional behavior for the token grant
* @returns {Promise<string>} Promise resolving to the access token
*/
PersistentSession.prototype.getAccessToken = function (options) {
// If our tokens are not fresh, we need to refresh them
var expirationBuffer = this._config.expiredBufferMS;
if (!this._tokenManager.isAccessTokenValid(this._tokenInfo, expirationBuffer)) {
return this._refreshTokens(options);
}
// Current access token is still valid. Return it.
return bluebird_1.Promise.resolve(this._tokenInfo.accessToken);
};
/**
* Revokes the session's tokens. If the session has a refresh token we'll use that,
* since it is more likely to be up to date. Otherwise, we'll revoke the accessToken.
* Revoking either one will disable the other as well.
*
* @param {TokenRequestOptions} [options] - Sets optional behavior for the token grant
* @returns {Promise} Promise that resolves when the revoke succeeds
*/
PersistentSession.prototype.revokeTokens = function (options) {
return this._tokenManager.revokeTokens(this._tokenInfo.refreshToken, options);
};
/**
* Exchange the client access token for one with lower scope
* @param {string|string[]} scopes The scope(s) requested for the new token
* @param {string} [resource] The absolute URL of an API resource to scope the new token to
* @param {Object} [options] - Optional parameters
* @param {TokenRequestOptions} [options.tokenRequestOptions] - Sets optional behavior for the token grant
* @returns {void}
*/
PersistentSession.prototype.exchangeToken = function (scopes, resource, options) {
var _this = this;
return this.getAccessToken(options).then(function (accessToken) {
return _this._tokenManager.exchangeToken(accessToken, scopes, resource, options);
});
};
/**
* Handle an an "Expired Tokens" Error. If our tokens are expired, we need to clear the token
* store (if present) before continuing.
*
* @param {Errors~ExpiredTokensError} err An "expired tokens" error including information
* about the request/response.
* @returns {Promise<Error>} Promise resolving to an error. This will
* usually be the original response error, but could an error from trying to access the
* token store as well.
*/
PersistentSession.prototype.handleExpiredTokensError = function (err /* FIXME */) {
if (!this._tokenStore) {
return bluebird_1.Promise.resolve(err);
}
// If a token store is available, clear the store and throw either error
// eslint-disable-next-line promise/no-promise-in-callback
return this._tokenStore
.clearAsync()
.catch(function (e) { return errors_1.default.unwrapAndThrow(e); })
.then(function () {
throw err;
});
};
return PersistentSession;
}());
module.exports = PersistentSession;
//# sourceMappingURL=persistent-session.js.map