UNPKG

box-node-sdk

Version:

Official SDK for Box Plaform APIs

239 lines 12.3 kB
"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