UNPKG

@webex/webex-core

Version:

Plugin handling for Cisco Webex

565 lines (552 loc) • 23.5 kB
"use strict"; var _typeof = require("@babel/runtime-corejs2/helpers/typeof"); var _Object$keys2 = require("@babel/runtime-corejs2/core-js/object/keys"); var _Object$getOwnPropertySymbols = require("@babel/runtime-corejs2/core-js/object/get-own-property-symbols"); var _Object$getOwnPropertyDescriptor2 = require("@babel/runtime-corejs2/core-js/object/get-own-property-descriptor"); var _Object$getOwnPropertyDescriptors = require("@babel/runtime-corejs2/core-js/object/get-own-property-descriptors"); var _Object$defineProperties = require("@babel/runtime-corejs2/core-js/object/define-properties"); var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); var _WeakMap = require("@babel/runtime-corejs2/core-js/weak-map"); var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); _Object$defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _deleteProperty = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/reflect/delete-property")); var _stringify = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/json/stringify")); var _keys = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/keys")); var _apply = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/reflect/apply")); var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise")); var _now = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/date/now")); var _getOwnPropertyDescriptor = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/get-own-property-descriptor")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/defineProperty")); var _applyDecoratedDescriptor2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/applyDecoratedDescriptor")); var _querystring = _interopRequireDefault(require("querystring")); var _url = _interopRequireDefault(require("url")); var _jsonwebtoken = _interopRequireDefault(require("jsonwebtoken")); var _common = require("@webex/common"); var _commonTimers = require("@webex/common-timers"); var _lodash = require("lodash"); var _webexPlugin = _interopRequireDefault(require("../webex-plugin")); var _decorators = require("../storage/decorators"); var _grantErrors = _interopRequireWildcard(require("./grant-errors")); var _scope = require("./scope"); var _token = _interopRequireDefault(require("./token")); var _tokenCollection = _interopRequireDefault(require("./token-collection")); var _constants = require("../constants"); var _dec, _dec2, _dec3, _dec4, _dec5, _dec6, _obj; /*! * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file. */ function _getRequireWildcardCache(e) { if ("function" != typeof _WeakMap) return null; var r = new _WeakMap(), t = new _WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = _Object$defineProperty && _Object$getOwnPropertyDescriptor2; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? _Object$getOwnPropertyDescriptor2(e, u) : null; i && (i.get || i.set) ? _Object$defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } function ownKeys(e, r) { var t = _Object$keys2(e); if (_Object$getOwnPropertySymbols) { var o = _Object$getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return _Object$getOwnPropertyDescriptor2(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : _Object$getOwnPropertyDescriptors ? _Object$defineProperties(e, _Object$getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { _Object$defineProperty(e, r, _Object$getOwnPropertyDescriptor2(t, r)); }); } return e; } /** * @class */ var Credentials = _webexPlugin.default.extend((_dec = (0, _common.oneFlight)({ keyFactory: function keyFactory(scope) { return scope; } }), _dec2 = (0, _decorators.waitForValue)('@'), _dec3 = (0, _decorators.persist)('@'), _dec4 = (0, _decorators.waitForValue)('@'), _dec5 = (0, _common.whileInFlight)('isRefreshing'), _dec6 = (0, _decorators.waitForValue)('@'), (_obj = { collections: { userTokens: _tokenCollection.default }, dataTypes: { token: (0, _common.makeStateDataType)(_token.default, 'token').dataType }, derived: { canAuthorize: { deps: ['supertoken', 'supertoken.canAuthorize', 'canRefresh'], fn: function fn() { return Boolean(this.supertoken && this.supertoken.canAuthorize || this.canRefresh); } }, canRefresh: { deps: ['supertoken', 'supertoken.canRefresh'], fn: function fn() { // If we're operating in JWT mode, we have to delegate to the consumer if (this.config.jwtRefreshCallback) { return true; } return Boolean(this.supertoken && this.supertoken.canRefresh); } }, isUnverifiedGuest: { deps: ['supertoken'], /** * Returns true if the user is an unverified guest * @returns {boolean} */ fn: function fn() { var isGuest = false; try { isGuest = JSON.parse(_common.base64.decode(this.supertoken.access_token.split('.')[1])).user_type === 'guest'; } catch (_unused) { /* the non-guest token is formatted differently so catch is expected */ } return isGuest; } } }, props: { supertoken: (0, _common.makeStateDataType)(_token.default, 'token').prop }, namespace: 'Credentials', session: { isRefreshing: { default: false, type: 'boolean' }, /** * Becomes `true` once the {@link loaded} event fires. * @see {@link WebexPlugin#ready} * @instance * @memberof Credentials * @type {boolean} */ ready: { default: false, type: 'boolean' }, refreshTimer: { default: undefined, type: 'any' } }, /** * Generates an OAuth Login URL. Prefers the api.ciscospark.com proxy if the * instance is initialize with an authorizatUrl, but fallsback to idbroker * as the base otherwise. * @instance * @memberof Credentials * @param {Object} [options={}] * @returns {string} */ buildLoginUrl: function buildLoginUrl() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { clientType: 'public' }; /* eslint-disable camelcase */ if (options.state && !(0, _lodash.isObject)(options.state)) { throw new Error('if specified, `options.state` must be an object'); } options.client_id = this.config.client_id; options.redirect_uri = this.config.redirect_uri; options.scope = this.config.scope; options = (0, _lodash.cloneDeep)(options); if (!options.response_type) { options.response_type = options.clientType === 'public' ? 'token' : 'code'; } (0, _deleteProperty.default)(options, 'clientType'); if (options.state) { if (!(0, _lodash.isEmpty)(options.state)) { options.state = _common.base64.toBase64Url((0, _stringify.default)(options.state)); } else { delete options.state; } } return "".concat(this.config.authorizeUrl, "?").concat(_querystring.default.stringify(options)); /* eslint-enable camelcase */ }, /** * Get the determined OrgId. * * @throws {Error} - If the OrgId could not be determined. * @returns {string} - The OrgId. */ getOrgId: function getOrgId() { this.logger.info('credentials: attempting to retrieve the OrgId from token'); try { // Attempt to extract a client-authenticated token's OrgId. this.logger.info('credentials: trying to extract OrgId from JWT'); return this.extractOrgIdFromJWT(this.supertoken.access_token); } catch (e) { // Attempt to extract a user token's OrgId. this.logger.info('credentials: could not extract OrgId from JWT'); this.logger.info('credentials: attempting to extract OrgId from user token'); try { var _this$supertoken; return this.extractOrgIdFromUserToken((_this$supertoken = this.supertoken) === null || _this$supertoken === void 0 ? void 0 : _this$supertoken.access_token); } catch (f) { this.logger.info('credentials: could not extract OrgId from user token'); throw f; } } }, /** * Extract the OrgId [realm] from a provided JWT. * * @private * @param {string} token - The JWT to extract the OrgId from. * @throws {Error} - If the token does not pass JWT general/realm validation. * @returns {string} - The OrgId. */ extractOrgIdFromJWT: function extractOrgIdFromJWT() { var token = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; // Decoded the provided token. var decodedJWT = _jsonwebtoken.default.decode(token); // Validate that the provided token is a JWT. if (!decodedJWT) { throw new Error('unable to extract the OrgId from the provided JWT'); } if (!decodedJWT.realm) { throw new Error('the provided JWT does not contain an OrgId'); } // Return the OrgId [realm]. return decodedJWT.realm; }, /** * Extract the OrgId [realm] from a provided user token. * * @private * @param {string} token - The user token to extract the OrgId from. * @throws {Error} - Will throw an error if the provided token is invalid. * @returns {string} - The OrgId. */ extractOrgIdFromUserToken: function extractOrgIdFromUserToken() { var token = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; // Split the provided token into subsections. var fields = token.split('_'); // Validate that the provided token has the proper amount of sections. if (fields.length !== 3) { throw new Error("the provided token is not a valid format, token has ".concat(fields.length, " sections")); } // Return the token section that contains the OrgId. return fields[2]; }, /** * Generates a Logout URL * @instance * @memberof Credentials * @param {Object} [options={}] * @returns {[type]} */ buildLogoutUrl: function buildLogoutUrl() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; return "".concat(this.config.logoutUrl, "?").concat(_querystring.default.stringify(_objectSpread({ cisService: this.config.service, goto: this.config.redirect_uri }, options))); }, /** * Generates a number between 60% - 90% of expired value * @instance * @memberof Credentials * @param {number} expiration * @private * @returns {number} */ calcRefreshTimeout: function calcRefreshTimeout(expiration) { return Math.floor((Math.floor(Math.random() * 4) + 6) / 10 * expiration); }, constructor: function constructor() { var _this = this; // HACK to deal with the fact that AmpersandState#dataTypes#set is a pure // function. this._dataTypes = (0, _lodash.cloneDeep)(this._dataTypes); (0, _keys.default)(this._dataTypes).forEach(function (key) { if (_this._dataTypes[key].set) { _this._dataTypes[key].set = _this._dataTypes[key].set.bind(_this); } }); // END HACK for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } (0, _apply.default)(_webexPlugin.default, this, args); }, /** * Downscopes a token * @instance * @memberof Credentials * @param {string} scope * @private * @returns {Promise<Token>} */ downscope: function downscope(scope) { var _this2 = this; return this.supertoken.downscope(scope).catch(function (reason) { var _reason$body; var failReason = (_reason$body = reason === null || reason === void 0 ? void 0 : reason.body) !== null && _reason$body !== void 0 ? _reason$body : reason; _this2.logger.warn("credentials: failed to downscope supertoken to \"".concat(scope, "\""), failReason); _this2.logger.trace("credentials: falling back to supertoken for ".concat(scope)); _this2.webex.internal.metrics.submitClientMetrics(_constants.METRICS.JS_SDK_CREDENTIALS_DOWNSCOPE_FAILED, { fields: { requestedScope: scope, failReason: failReason } }); return _promise.default.resolve(new _token.default(_objectSpread({ scope: scope }, _this2.supertoken.serialize())), { parent: _this2 }); }); }, /** * Requests a client credentials grant and returns the token. Given the * limited use for such tokens as this time, this method does not cache its * token. * @instance * @memberof Credentials * @param {Object} options * @returns {Promise<Token>} */ getClientToken: function getClientToken() { var _this3 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; this.logger.info('credentials: requesting client credentials grant'); return this.webex.request({ /* eslint-disable camelcase */ method: 'POST', uri: options.uri || this.config.tokenUrl, form: { grant_type: 'client_credentials', scope: options.scope || 'webexsquare:admin', self_contained_token: true }, auth: { user: this.config.client_id, pass: this.config.client_secret, sendImmediately: true }, shouldRefreshAccessToken: false /* eslint-enable camelcase */ }).then(function (res) { return new _token.default(res.body, { parent: _this3 }); }).catch(function (res) { if (res.statusCode !== 400) { return _promise.default.reject(res); } var ErrorConstructor = _grantErrors.default.select(res.body.error); return _promise.default.reject(new ErrorConstructor(res._res || res)); }); }, /** * Resolves with a token with the specified scopes. If no scope is specified, * defaults to omit(webex.credentials.scope, 'spark:kms'). If no such token is * available, downscopes the supertoken to that scope. * @instance * @memberof Credentials * @param {string} scope * @returns {Promise<Token>} */ getUserToken: function getUserToken(scope) { var _this4 = this; return _promise.default.resolve(!this.isRefreshing || new _promise.default(function (resolve) { _this4.logger.info('credentials: token refresh inflight; delaying getUserToken until refresh completes'); _this4.once('change:isRefreshing', function () { _this4.logger.info('credentials: token refresh complete; reinvoking getUserToken'); resolve(); }); })).then(function () { if (!_this4.canAuthorize) { _this4.logger.info('credentials: cannot produce an access token from current state'); return _promise.default.reject(new Error('Current state cannot produce an access token')); } if (!scope) { scope = (0, _scope.filterScope)('spark:kms', _this4.supertoken.scope); } scope = (0, _scope.sortScope)(scope); if (scope === (0, _scope.sortScope)(_this4.supertoken.scope)) { return _promise.default.resolve(_this4.supertoken); } var token = _this4.userTokens.get(scope); // we should also check for the token.access_token since token object does // not get cleared on unsetting while logging out. if (!token || !token.access_token) { return _this4.downscope(scope).then((0, _common.tap)(function (t) { return _this4.userTokens.add(t); })); } return _promise.default.resolve(token); }); }, /** * Initializer * @instance * @memberof Credentials * @param {Object} attrs * @param {Object} options * @private * @returns {Credentials} */ initialize: function initialize(attrs, options) { var _this5 = this; if (attrs) { if (typeof attrs === 'string') { this.supertoken = attrs; } if (attrs.access_token) { this.supertoken = attrs; } if (attrs.authorization) { if (attrs.authorization.supertoken) { this.supertoken = attrs.authorization.supertoken; } else { this.supertoken = attrs.authorization; } } // schedule refresh if (this.supertoken && this.supertoken.expires) { this.scheduleRefresh(this.supertoken.expires); } } (0, _apply.default)(_webexPlugin.default.prototype.initialize, this, [attrs, options]); this.listenToOnce(this.parent, 'change:config', function () { if (_this5.config.authorizationString) { var parsed = _url.default.parse(_this5.config.authorizationString, true); /* eslint-disable camelcase */ _this5.config.client_id = parsed.query.client_id; _this5.config.redirect_uri = parsed.query.redirect_uri; _this5.config.scope = parsed.query.scope; _this5.config.authorizeUrl = parsed.href.substr(0, parsed.href.indexOf('?')); /* eslint-enable camelcase */ } }); this.webex.once('loaded', function () { _this5.ready = true; }); }, /** * Clears all tokens from store them from the stores. * * This is no longer quite the right name for this method, but all of the * alternatives I'm coming up with are already taken. * @instance * @memberof Credentials * @returns {Promise} */ invalidate: function invalidate() { this.logger.info('credentials: invalidating tokens'); // clear refresh timer if (this.refreshTimer) { clearTimeout(this.refreshTimer); this.unset('refreshTimer'); } try { this.unset('supertoken'); } catch (err) { this.logger.warn('credentials: failed to clear supertoken', err); } while (this.userTokens.models.length) { try { this.userTokens.remove(this.userTokens.models[0]); } catch (err) { this.logger.warn('credentials: failed to remove user token', err); } } this.logger.info('credentials: finished removing tokens'); // Return a promise to give the storage layer a tick or two to clear // localStorage return _promise.default.resolve(); }, /** * Removes the supertoken and child tokens, then refreshes the supertoken; * subsequent calls to {@link Credentials#getUserToken()} will re-downscope * child tokens. Enqueus revocation of previous previousTokens. Yes, that's * the correct number of "previous"es. * @instance * @memberof Credentials * @returns {Promise} */ refresh: function refresh() { var _this6 = this; this.logger.info('credentials: refresh requested'); var supertoken = this.supertoken; var tokens = (0, _lodash.clone)(this.userTokens.models); // This is kind of a leaky abstraction, since it relies on the authorization // plugin, but the only alternatives I see are // 1. put all JWT support in core // 2. have separate jwt and non-jwt auth plugins // while I like #2 from a code simplicity standpoint, the third-party DX // isn't great if (this.config.jwtRefreshCallback) { return this.config.jwtRefreshCallback(this.webex) // eslint-disable-next-line no-shadow .then(function (jwt) { return _this6.webex.authorization.requestAccessTokenFromJwt({ jwt: jwt }); }); } if (this.webex.internal.services) { this.webex.internal.services.updateCredentialsConfig(); } return supertoken.refresh().catch(function (error) { if (error instanceof _grantErrors.OAuthError) { // Error: super token refresh failed with 400 status code. // Hence emit an event to the client, an opportunity to logout. _this6.unset('supertoken'); while (_this6.userTokens.models.length) { try { _this6.userTokens.remove(_this6.userTokens.models[0]); } catch (err) { _this6.logger.warn('credentials: failed to remove user token', err); } } _this6.webex.trigger('client:InvalidRequestError'); } return _promise.default.reject(error); }).then(function (st) { // clear refresh timer if (_this6.refreshTimer) { clearTimeout(_this6.refreshTimer); _this6.unset('refreshTimer'); } _this6.supertoken = st; var invalidScopes = (0, _scope.diffScopes)(_this6.config.scope, st.scope); if (invalidScopes !== '') { _this6.logger.warn("credentials: \"".concat(invalidScopes, "\" scope(s) are invalid because not listed in the supertoken, they will be excluded from user token requests.")); _this6.webex.internal.metrics.submitClientMetrics(_constants.METRICS.JS_SDK_CREDENTIALS_TOKEN_REFRESH_SCOPE_MISMATCH, { fields: { invalidScopes: invalidScopes } }); } return _promise.default.all(tokens.map(function (token) { var tokenScope = (0, _scope.filterScope)((0, _scope.diffScopes)(token.scope, st.scope), token.scope); return _this6.downscope(tokenScope) // eslint-disable-next-line max-nested-callbacks .then(function (t) { _this6.logger.info("credentials: revoking token for ".concat(token.scope)); return token.revoke().catch(function (err) { _this6.logger.warn('credentials: failed to revoke user token', err); }).then(function () { _this6.userTokens.remove(token.scope); _this6.userTokens.add(t); }); }); })); }).then(function () { _this6.scheduleRefresh(_this6.supertoken.expires); }); }, /** * Schedules a token refresh or refreshes the token if token has expired * @instance * @memberof Credentials * @param {number} expires * @private * @returns {undefined} */ scheduleRefresh: function scheduleRefresh(expires) { var _this7 = this; var expiresIn = expires - (0, _now.default)(); if (expiresIn > 0) { var timeoutLength = this.calcRefreshTimeout(expiresIn); this.refreshTimer = (0, _commonTimers.safeSetTimeout)(function () { return _this7.refresh(); }, timeoutLength); } else { this.refresh(); } }, version: "3.9.0" }, ((0, _applyDecoratedDescriptor2.default)(_obj, "getUserToken", [_dec, _dec2], (0, _getOwnPropertyDescriptor.default)(_obj, "getUserToken"), _obj), (0, _applyDecoratedDescriptor2.default)(_obj, "initialize", [_dec3], (0, _getOwnPropertyDescriptor.default)(_obj, "initialize"), _obj), (0, _applyDecoratedDescriptor2.default)(_obj, "invalidate", [_common.oneFlight, _dec4], (0, _getOwnPropertyDescriptor.default)(_obj, "invalidate"), _obj), (0, _applyDecoratedDescriptor2.default)(_obj, "refresh", [_common.oneFlight, _dec5, _dec6], (0, _getOwnPropertyDescriptor.default)(_obj, "refresh"), _obj)), _obj))); var _default = exports.default = Credentials; //# sourceMappingURL=credentials.js.map