UNPKG

box-node-sdk

Version:

Official SDK for Box Plaform APIs

460 lines 20.7 kB
"use strict"; /** * @fileoverview Token Manager */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ var bluebird_1 = __importDefault(require("bluebird")); var http_status_1 = __importDefault(require("http-status")); var jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); var uuid_1 = require("uuid"); var errors_1 = __importDefault(require("./util/errors")); var exponential_backoff_1 = __importDefault(require("./util/exponential-backoff")); /** * Determines whether a JWT auth error can be retried * @param {Error} err The JWT auth error * @returns {boolean} True if the error is retryable */ function isJWTAuthErrorRetryable(err /* FIXME */) { if (err.authExpired && err.response.headers.date && (err.response.body.error_description.indexOf('exp') > -1 || err.response.body.error_description.indexOf('jti') > -1)) { return true; } else if (err.statusCode === 429 || err.statusCode >= 500) { return true; } return false; } // ------------------------------------------------------------------------------ // Constants // ------------------------------------------------------------------------------ /** * Collection of grant types that can be used to acquire tokens via OAuth2 * * @readonly * @enum {string} */ var grantTypes = { AUTHORIZATION_CODE: 'authorization_code', REFRESH_TOKEN: 'refresh_token', CLIENT_CREDENTIALS: 'client_credentials', JWT: 'urn:ietf:params:oauth:grant-type:jwt-bearer', TOKEN_EXCHANGE: 'urn:ietf:params:oauth:grant-type:token-exchange', }; /** * Collection of paths to interact with Box OAuth2 tokening system * * @readonly * @enum {string} */ var tokenPaths; (function (tokenPaths) { tokenPaths["ROOT"] = "/oauth2"; tokenPaths["GET"] = "/token"; tokenPaths["REVOKE"] = "/revoke"; })(tokenPaths || (tokenPaths = {})); // Timer used to track elapsed time starting with executing an async request and ending with emitting the response. var asyncRequestTimer /* FIXME */; // The XFF header label - Used to give the API better information for uploads, rate-limiting, etc. var HEADER_XFF = 'X-Forwarded-For'; var ACCESS_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token'; var ACTOR_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:id_token'; var BOX_JWT_AUDIENCE = 'https://api.box.com/oauth2/token'; // ------------------------------------------------------------------------------ // Private // ------------------------------------------------------------------------------ /** * Parse the response body to create a new TokenInfo object. * * @param {Object} grantResponseBody - (Request lib) response body containing granted token info from API * @returns {TokenInfo} A TokenInfo object. * @private */ function getTokensFromGrantResponse(grantResponseBody /* FIXME */) { return { // Set the access token & refresh token (if passed) accessToken: grantResponseBody.access_token, refreshToken: grantResponseBody.refresh_token, // Box API sends back expires_in in seconds, we convert to ms for consistency of keeping all time in ms accessTokenTTLMS: parseInt(grantResponseBody.expires_in, 10) * 1000, acquiredAtMS: Date.now(), }; } /** * Determines if a given string could represent an authorization code or token. * * @param {string} codeOrToken The code or token to check. * @returns {boolean} True if codeOrToken is valid, false if not. * @private */ function isValidCodeOrToken(codeOrToken) { return typeof codeOrToken === 'string' && codeOrToken.length > 0; } /** * Determines if a token grant response is valid * * @param {string} grantType the type of token grant * @param {Object} responseBody the body of the response to check * @returns {boolean} True if response body has expected fields, false if not. * @private */ function isValidTokenResponse(grantType, responseBody /* FIXME */) { if (!isValidCodeOrToken(responseBody.access_token)) { return false; } if (typeof responseBody.expires_in !== 'number') { return false; } // Check the refresh_token for certain types of grants if (grantType === 'authorization_code' || grantType === 'refresh_token') { if (!isValidCodeOrToken(responseBody.refresh_token)) { return false; } } return true; } // ------------------------------------------------------------------------------ // Public // ------------------------------------------------------------------------------ /** * Manager for API access abd refresh tokens * * @param {Config} config The config object * @param {APIRequestManager} requestManager The API Request Manager * @constructor */ var TokenManager = /** @class */ (function () { function TokenManager(config, requestManager) { this.config = config; this.oauthBaseURL = config.apiRootURL + tokenPaths.ROOT; this.requestManager = requestManager; } /** * Given a TokenInfo object, returns whether its access token is expired. An access token is considered * expired once its TTL surpasses the current time outside of the given buffer. This is a public method so * that other modules may check the validity of their tokens. * * @param {TokenInfo} tokenInfo the token info to be written * @param {int} [bufferMS] An optional buffer we'd like to test against. The greater this buffer, the more aggressively * we'll call a token invalid. * @returns {boolean} True if token is valid outside of buffer, otherwise false */ TokenManager.prototype.isAccessTokenValid = function (tokenInfo, bufferMS) { if (typeof tokenInfo.acquiredAtMS === 'undefined' || typeof tokenInfo.accessTokenTTLMS === 'undefined') { return false; } bufferMS = bufferMS || 0; var expireTime = tokenInfo.acquiredAtMS + tokenInfo.accessTokenTTLMS - bufferMS; return expireTime > Date.now(); }; /** * Acquires OAuth2 tokens using a grant type (authorization_code, password, refresh_token) * * @param {Object} formParams - should contain all params expected by Box OAuth2 token endpoint * @param {TokenRequestOptions} [options] - Sets optional behavior for the token grant, null for default behavior * @returns {Promise<TokenInfo>} Promise resolving to the token info * @private */ TokenManager.prototype.getTokens = function (formParams, options) { var params = { method: 'POST', url: this.oauthBaseURL + tokenPaths.GET, headers: {}, form: formParams, }; options = options || {}; // add in app-specific id and secret to auth with Box params.form.client_id = this.config.clientID; params.form.client_secret = this.config.clientSecret; if (options.ip) { params.headers[HEADER_XFF] = options.ip; } return this.requestManager .makeRequest(params) .then(function (response /* FIXME */) { // Response Error: The API is telling us that we attempted an invalid token grant. This // means that our refresh token or auth code has exipred, so propagate an "Expired Tokens" // error. if (response.body && response.body.error && response.body.error === 'invalid_grant') { var errDescription = response.body.error_description; var message = errDescription ? "Auth Error: ".concat(errDescription) : undefined; throw errors_1.default.buildAuthError(response, message); } // Unexpected Response: If the token request couldn't get a valid response, then we're // out of options. Build an "Unexpected Response" error and propagate it out for the // consumer to handle. if (response.statusCode !== http_status_1.default.OK || response.body instanceof Buffer) { throw errors_1.default.buildUnexpectedResponseError(response); } // Check to see if token response is valid in case the API returns us a 200 with a malformed token if (!isValidTokenResponse(formParams.grant_type, response.body)) { throw errors_1.default.buildResponseError(response, 'Token format from response invalid'); } // Got valid token response. Parse out the TokenInfo and propagate it back. return getTokensFromGrantResponse(response.body); }); }; /** * Acquires token info using an authorization code * * @param {string} authorizationCode - authorization code issued by Box * @param {TokenRequestOptions} [options] - Sets optional behavior for the token grant * @returns {Promise<TokenInfo>} Promise resolving to the token info */ TokenManager.prototype.getTokensAuthorizationCodeGrant = function (authorizationCode, options) { if (!isValidCodeOrToken(authorizationCode)) { return bluebird_1.default.reject(new Error('Invalid authorization code.')); } var params = { grant_type: grantTypes.AUTHORIZATION_CODE, code: authorizationCode, }; return this.getTokens(params, options); }; /** * Acquires token info using the client credentials grant. * * @param {TokenRequestOptions} [options] - Sets optional behavior for the token grant * @returns {Promise<TokenInfo>} Promise resolving to the token info */ TokenManager.prototype.getTokensClientCredentialsGrant = function (options) { var params = { grant_type: grantTypes.CLIENT_CREDENTIALS, box_subject_type: this.config.boxSubjectType, box_subject_id: this.config.boxSubjectId, }; return this.getTokens(params, options); }; /** * Refreshes the access and refresh tokens for a given refresh token. * * @param {string} refreshToken - A valid OAuth refresh token * @param {TokenRequestOptions} [options] - Sets optional behavior for the token grant * @returns {Promise<TokenInfo>} Promise resolving to the token info */ TokenManager.prototype.getTokensRefreshGrant = function (refreshToken, options) { if (!isValidCodeOrToken(refreshToken)) { return bluebird_1.default.reject(new Error('Invalid refresh token.')); } var params = { grant_type: grantTypes.REFRESH_TOKEN, refresh_token: refreshToken, }; return this.getTokens(params, options); }; /** * Gets tokens for enterprise administration of app users * @param {string} type The type of token to create, "user" or "enterprise" * @param {string} id The ID of the enterprise to generate a token for * @param {TokenRequestOptions} [options] - Sets optional behavior for the token grant * @returns {Promise<TokenInfo>} Promise resolving to the token info */ TokenManager.prototype.getTokensJWTGrant = function (type, id, options) { var _this = this; if (!this.config.appAuth || !this.config.appAuth.keyID) { return bluebird_1.default.reject(new Error('Must provide app auth configuration to use JWT Grant')); } var claims = { exp: Math.floor(Date.now() / 1000) + this.config.appAuth.expirationTime, box_sub_type: type, }; var jwtOptions = { algorithm: this.config.appAuth.algorithm, audience: BOX_JWT_AUDIENCE, subject: id, issuer: this.config.clientID, jwtid: (0, uuid_1.v4)(), noTimestamp: !this.config.appAuth.verifyTimestamp, keyid: this.config.appAuth.keyID, }; var keyParams = { key: this.config.appAuth.privateKey, passphrase: this.config.appAuth.passphrase, }; var assertion; try { assertion = jsonwebtoken_1.default.sign(claims, keyParams, jwtOptions); } catch (jwtErr) { return bluebird_1.default.reject(jwtErr); } var params = { grant_type: grantTypes.JWT, assertion: assertion, }; // Start the request timer immediately before executing the async request asyncRequestTimer = process.hrtime(); return this.getTokens(params, options).catch(function (err) { return _this.retryJWTGrant(claims, jwtOptions, keyParams, params, options, err, 0); }); }; /** * Attempt a retry if possible and create a new JTI claim. If the request hasn't exceeded it's maximum number of retries, * re-execute the request (after the retry interval). Otherwise, propagate a new error. * * @param {Object} claims - JTI claims object * @param {Object} [jwtOptions] - JWT options for the signature * @param {Object} keyParams - Key JWT parameters object that contains the private key and the passphrase * @param {Object} params - Should contain all params expected by Box OAuth2 token endpoint * @param {TokenRequestOptions} [options] - Sets optional behavior for the token grant * @param {Error} error - Error from the previous JWT request * @param {int} numRetries - Number of retries attempted * @returns {Promise<TokenInfo>} Promise resolving to the token info */ // eslint-disable-next-line max-params TokenManager.prototype.retryJWTGrant = function (claims /* FIXME */, jwtOptions /* FIXME */, keyParams /* FIXME */, params /* FIXME */, options, error /* FIXME */, numRetries) { var _this = this; if (numRetries < this.config.numMaxRetries && isJWTAuthErrorRetryable(error)) { var retryTimeoutinSeconds; numRetries += 1; // If the retry strategy is defined, then use it to determine the time (in ms) until the next retry or to // propagate an error to the user. if (this.config.retryStrategy) { // Get the total elapsed time so far since the request was executed var totalElapsedTime = process.hrtime(asyncRequestTimer); var totalElapsedTimeMS = totalElapsedTime[0] * 1000 + totalElapsedTime[1] / 1000000; var retryOptions = { error: error, numRetryAttempts: numRetries, numMaxRetries: this.config.numMaxRetries, retryIntervalMS: this.config.retryIntervalMS, totalElapsedTimeMS: totalElapsedTimeMS, }; retryTimeoutinSeconds = this.config.retryStrategy(retryOptions); // If the retry strategy doesn't return a number/time in ms, then propagate the response error to the user. // However, if the retry strategy returns its own error, this will be propagated to the user instead. if (typeof retryTimeoutinSeconds !== 'number') { if (retryTimeoutinSeconds instanceof Error) { error = retryTimeoutinSeconds; } throw error; } } else if (error.hasOwnProperty('response') && error.response.hasOwnProperty('headers') && error.response.headers.hasOwnProperty('retry-after')) { retryTimeoutinSeconds = error.response.headers['retry-after']; } else { retryTimeoutinSeconds = Math.ceil((0, exponential_backoff_1.default)(numRetries, this.config.retryIntervalMS) / 1000); } var time = Math.floor(Date.now() / 1000); if (error.response.headers.date) { time = Math.floor(Date.parse(error.response.headers.date) / 1000); } // Add length of retry timeout to current expiration time to calculate the expiration time for the JTI claim. claims.exp = Math.ceil(time + this.config.appAuth.expirationTime + retryTimeoutinSeconds); jwtOptions.jwtid = (0, uuid_1.v4)(); try { params.assertion = jsonwebtoken_1.default.sign(claims, keyParams, jwtOptions); } catch (jwtErr) { throw jwtErr; } return bluebird_1.default.delay(retryTimeoutinSeconds).then(function () { // Start the request timer immediately before executing the async request asyncRequestTimer = process.hrtime(); return _this.getTokens(params, options).catch(function (err) { return _this.retryJWTGrant(claims, jwtOptions, keyParams, params, options, err, numRetries); }); }); } else if (numRetries >= this.config.numMaxRetries) { error.maxRetriesExceeded = true; } throw error; }; /** * Exchange a valid access token for one with a lower scope, or delegated to * an external user identifier. * * @param {string} accessToken - The valid access token to exchange * @param {string|string[]} scopes - The scope(s) of the new access token * @param {string} [resource] - The absolute URL of an API resource to restrict the new token to * @param {Object} [options] - Optional parameters * @param {TokenRequestOptions} [options.tokenRequestOptions] - Sets optional behavior for the token grant * @param {ActorParams} [options.actor] - Optional actor parameters for creating annotator tokens * @param {SharedLinkParams} [options.sharedLink] - Optional shared link parameters for creating tokens using shared links * @returns {Promise<TokenInfo>} Promise resolving to the new token info */ TokenManager.prototype.exchangeToken = function (accessToken, scopes, resource, options) { var params = { grant_type: grantTypes.TOKEN_EXCHANGE, subject_token_type: ACCESS_TOKEN_TYPE, subject_token: accessToken, scope: typeof scopes === 'string' ? scopes : scopes.join(' '), }; if (resource) { params.resource = resource; } if (options && options.sharedLink) { params.box_shared_link = options.sharedLink.url; } if (options && options.actor) { var payload = { iss: this.config.clientID, sub: options.actor.id, aud: BOX_JWT_AUDIENCE, box_sub_type: 'external', name: options.actor.name, }; var jwtOptions = { algorithm: 'none', expiresIn: '1m', noTimestamp: true, jwtid: (0, uuid_1.v4)(), }; var token; try { token = jsonwebtoken_1.default.sign(payload, 'UNUSED', jwtOptions /* FIXME */); } catch (jwtError) { return bluebird_1.default.reject(jwtError); } params.actor_token = token; params.actor_token_type = ACTOR_TOKEN_TYPE; } return this.getTokens(params, options && options.tokenRequestOptions ? options.tokenRequestOptions : null); }; /** * Revokes a token pair associated with a given access or refresh token. * * @param {string} token - A valid access or refresh token to revoke * @param {TokenRequestOptions} [options] - Sets optional behavior for the token grant * @returns {Promise} Promise resolving if the revoke succeeds */ TokenManager.prototype.revokeTokens = function (token, options) { var params = { method: 'POST', url: this.oauthBaseURL + tokenPaths.REVOKE, form: { token: token, client_id: this.config.clientID, client_secret: this.config.clientSecret, }, }; if (options && options.ip) { params.headers = {}; params.headers[HEADER_XFF] = options.ip; } return this.requestManager.makeRequest(params); }; return TokenManager; }()); module.exports = TokenManager; //# sourceMappingURL=token-manager.js.map