UNPKG

@webex/webex-core

Version:

Plugin handling for Cisco Webex

539 lines (530 loc) • 20.5 kB
"use strict"; var _typeof = require("@babel/runtime-corejs2/helpers/typeof"); var _Object$keys = 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 _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/defineProperty")); var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/slicedToArray")); var _applyDecoratedDescriptor2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/applyDecoratedDescriptor")); var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise")); var _assign = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/assign")); var _apply = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/reflect/apply")); 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 _lodash = require("lodash"); var _common = require("@webex/common"); var _commonTimers = require("@webex/common-timers"); var _webexHttpError = _interopRequireDefault(require("../webex-http-error")); var _webexPlugin = _interopRequireDefault(require("../webex-plugin")); var _scope = require("./scope"); var _grantErrors = _interopRequireWildcard(require("./grant-errors")); var _dec, _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$keys(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; } /* eslint-disable camelcase */ /** * Parse response from CI and converts to structured error when appropriate * @param {WebexHttpError} res * @private * @returns {GrantError} */ function processGrantError(res) { if (res.statusCode !== 400) { return _promise.default.reject(res); } var ErrorConstructor = _grantErrors.default.select(res.body.error); if (ErrorConstructor === _grantErrors.OAuthError && res instanceof _webexHttpError.default) { return _promise.default.reject(res); } if (!ErrorConstructor) { return _promise.default.reject(res); } return _promise.default.reject(new ErrorConstructor(res._res || res)); } /** * @class */ var Token = _webexPlugin.default.extend((_dec = (0, _common.oneFlight)({ keyFactory: function keyFactory(scope) { return scope; } }), (_obj = { derived: { /** * Indicates if this token can be used in an auth header. `true` iff * {@link Token#access_token} is defined and {@link Token#isExpired} is * false. * @instance * @memberof Token * @readonly * @type {boolean} */ canAuthorize: { deps: ['access_token', 'isExpired'], fn: function fn() { return !!this.access_token && !this.isExpired; } }, /** * Indicates that this token can be downscoped. `true` iff * {@link config.credentials.client_id} is defined and if * {@link Token#canAuthorize} is true * * Note: since {@link config} is not evented, we can't listen for changes to * {@link config.credentials.client_id}. As such, * {@link config.credentials.client_id} must always be set before * instantiating a {@link Token} * @instance * @memberof Token * @readonly * @type {boolean} */ canDownscope: { deps: ['canAuthorize'], fn: function fn() { return this.canAuthorize && !!this.config.client_id; } }, /** * Indicates if this token can be refreshed. `true` iff * {@link Token@refresh_token} is defined and * {@link config.credentials.refreshCallback()} is defined * * Note: since {@link config} is not evented, we can't listen for changes to * {@link config.credentials.refreshCallback()}. As such, * {@link config.credentials.refreshCallback()} must always be set before * instantiating a {@link Token} * @instance * @memberof Token * @readonly * @type {boolean} */ canRefresh: { deps: ['refresh_token'], fn: function fn() { if (_common.inBrowser) { return !!this.refresh_token && !!this.config.refreshCallback; } return !!this.refresh_token && !!this.config.client_secret; } }, /** * Indicates if this `Token` is expired. `true` iff {@link Token#expires} is * defined and is less than {@link Date.now()}. * @instance * @memberof Token * @readonly * @type {boolean} */ isExpired: { deps: ['expires', '_isExpired'], fn: function fn() { // in order to avoid setting `cache:false`, we'll use a private property // and a timer rather than comparing to `Date.now()`; return !!this.expires && this._isExpired; } }, /** * Cache for toString() * @instance * @memberof Token * @private * @readonly * @type {string} */ _string: { deps: ['access_token', 'token_type'], fn: function fn() { if (!this.access_token || !this.token_type) { return ''; } return "".concat(this.token_type, " ").concat(this.access_token); } } }, namespace: 'Credentials', props: { /** * Used for indexing in the credentials userTokens collection * @instance * @memberof Token * @private * @type {string} */ scope: 'string', /** * @instance * @memberof Token * @type {string} */ access_token: 'string', /** * @instance * @memberof Token * @type {number} */ expires: 'number', /** * @instance * @memberof Token * @type {number} */ expires_in: 'number', /** * @instance * @memberof Token * @type {string} */ refresh_token: 'string', /** * @instance * @memberof Token * @type {number} */ refresh_token_expires: 'number', /** * @instance * @memberof Token * @type {number} */ refresh_token_expires_in: 'number', /** * @default "Bearer" * @instance * @memberof Token * @type {string} */ token_type: { default: 'Bearer', type: 'string' } }, session: { /** * Used by {@link Token#isExpired} to avoid doing a Date comparison. * @instance * @memberof Token * @private * @type {boolean} */ _isExpired: { default: false, type: 'boolean' }, /** * Handle to the previous token that we'll revoke when we refresh this * token. The idea is to keep allow two valid tokens when a refresh occurs; * we don't want revoke a token that's in the middle of being used, so when * we do a token refresh, we won't revoke the token being refreshed, but * we'll revoke the previous one. * @instance * @memberof Token * @private * @type {Object} */ previousToken: { type: 'state' } }, /** * Uses this token to request a new Token with a subset of this Token's scopes * @instance * @memberof Token * @param {string} scope * @returns {Promise<Token>} */ downscope: function downscope(scope) { var _this = this; this.logger.info("token: downscoping token to ".concat(scope)); if (this.isExpired) { this.logger.info('token: request received to downscope expired access_token'); return _promise.default.reject(new Error('cannot downscope expired access token')); } if (!this.canDownscope) { if (this.config.client_id) { this.logger.info('token: request received to downscope invalid access_token'); } else { this.logger.trace('token: cannot downscope without client_id'); } return _promise.default.reject(new Error('cannot downscope access token')); } if ((0, _scope.diffScopes)(scope, this.config.scope) !== '') { return _promise.default.reject(new Error("new scope (".concat(scope, ") is not subset of the available scopes (").concat(this.config.scope, ")"))); } // Since we're going to use scope as the index in our token collection, it's // important scopes are always deterministically specified. if (scope) { scope = (0, _scope.sortScope)(scope); } // Ideally, we could depend on the service to communicate this error, but // all we get is "invalid scope", which, to the lay person, implies // something wrong with *one* of the scopes, not the whole thing. if (scope === (0, _scope.sortScope)(this.config.scope)) { return _promise.default.reject(new Error('token: scope reduction requires a reduced scope')); } return this.webex.request({ method: 'POST', uri: this.config.tokenUrl, addAuthHeader: false, form: { grant_type: 'urn:cisco:oauth:grant-type:scope-reduction', token: this.access_token, scope: scope, client_id: this.config.client_id, self_contained_token: true } }).then(function (res) { _this.logger.info("token: downscoped token to ".concat(scope)); return new Token((0, _assign.default)(res.body, { scope: scope }), { parent: _this.parent }); }); }, /** * Initializer * @instance * @memberof Token * @param {Object} [attrs={}] * @param {Object} [options={}] * @see {@link WebexPlugin#initialize()} * @returns {Token} */ initialize: function initialize() { var _this2 = this; var attrs = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; (0, _apply.default)(_webexPlugin.default.prototype.initialize, this, [attrs, options]); if (typeof attrs === 'string') { this.access_token = attrs; } if (!this.access_token) { throw new Error('`access_token` is required'); } // We don't want the derived property `isExpired` to need {cache:false}, so // we'll set up a timer the runs when this token should expire. if (this.expires) { if (this.expires < (0, _now.default)()) { this._isExpired = true; } else { (0, _commonTimers.safeSetTimeout)(function () { _this2._isExpired = true; }, this.expires - (0, _now.default)()); } } }, /** * Refreshes this Token. Relies on * {@link config.credentials.refreshCallback()} * @instance * @memberof Token * @returns {Promise<Token>} */ refresh: function refresh() { var _this3 = this; if (!this.canRefresh) { throw new Error('Not enough information available to refresh this access token'); } var promise; if (_common.inBrowser) { if (!this.config.refreshCallback) { throw new Error('Cannot refresh access token without refreshCallback'); } promise = _promise.default.resolve(this.config.refreshCallback(this.webex, this)); } return (promise || this.webex.request({ method: 'POST', uri: this.config.tokenUrl, form: { grant_type: 'refresh_token', redirect_uri: this.config.redirect_uri, refresh_token: this.refresh_token }, auth: { user: this.config.client_id, pass: this.config.client_secret, sendImmediately: true }, shouldRefreshAccessToken: false }).then(function (res) { return res.body; })).then(function (obj) { if (!obj) { throw new Error('token: refreshCallback() did not produce an object'); } // If the authentication server did not send back a refresh token, copy // the current refresh token and related values to the response (note: // at time of implementation, CI never sends a new refresh token) if (!obj.refresh_token) { (0, _assign.default)(obj, (0, _lodash.pick)(_this3, 'refresh_token', 'refresh_token_expires', 'refresh_token_expires_in')); } // If the new token is the same as the previous token, then we may have // found a bug in CI; log the details and reject the Promise if (_this3.access_token === obj.access_token) { _this3.logger.error('token: new token matches current token'); // log the tokens if it is not production if (process.env.NODE_ENV !== 'production') { _this3.logger.error('token: current token:', _this3.access_token); _this3.logger.error('token: new token:', obj.access_token); } return _promise.default.reject(new Error('new token matches current token')); } if (_this3.previousToken) { _this3.previousToken.revoke(); _this3.unset('previousToken'); } obj.previousToken = _this3; obj.scope = _this3.scope; return new Token(obj, { parent: _this3.parent }); }).catch(processGrantError); }, /** * Revokes this token and unsets its local properties * @instance * @memberof Token * @returns {Promise} */ revoke: function revoke() { var _this4 = this; if (this.isExpired) { this.logger.info('token: already expired, not making making revocation request'); return _promise.default.resolve(); } if (!this.canAuthorize) { this.logger.info('token: no longer valid, not making revocation request'); return _promise.default.resolve(); } // FIXME we need to use the user token revocation endpoint to revoke a token // without a client_secret, but it doesn't current support using a token to // revoke itself // Note: I'm not making a canRevoke property because there should be changes // coming to the user token revocation endpoint that allow us to do this // correctly. if (!this.config.client_secret) { this.logger.info('token: no client secret available, not making revocation request'); return _promise.default.resolve(); } this.logger.info('token: revoking access token'); return this.webex.request({ method: 'POST', uri: this.config.revokeUrl, form: { token: this.access_token, token_type_hint: 'access_token' }, auth: { user: this.config.client_id, pass: this.config.client_secret, sendImmediately: true }, shouldRefreshAccessToken: false }).then(function () { _this4.unset(['access_token', 'expires', 'expires_in', 'token_type']); _this4.logger.info('token: access token revoked'); }).catch(processGrantError); }, set: function set() { // eslint-disable-next-line prefer-const var _this$_filterSetParam = this._filterSetParameters.apply(this, arguments), _this$_filterSetParam2 = (0, _slicedToArray2.default)(_this$_filterSetParam, 2), attrs = _this$_filterSetParam2[0], options = _this$_filterSetParam2[1]; if (!attrs.token_type && attrs.access_token && attrs.access_token.includes(' ')) { var _attrs$access_token$s = attrs.access_token.split(' '), _attrs$access_token$s2 = (0, _slicedToArray2.default)(_attrs$access_token$s, 2), token_type = _attrs$access_token$s2[0], access_token = _attrs$access_token$s2[1]; attrs = _objectSpread(_objectSpread({}, attrs), {}, { access_token: access_token, token_type: token_type }); } var now = (0, _now.default)(); if (!attrs.expires && attrs.expires_in) { attrs.expires = now + attrs.expires_in * 1000; } if (!attrs.refresh_token_expires && attrs.refresh_token_expires_in) { attrs.refresh_token_expires = now + attrs.refresh_token_expires_in * 1000; } if (attrs.scope) { attrs.scope = (0, _scope.sortScope)(attrs.scope); } return (0, _apply.default)(_webexPlugin.default.prototype.set, this, [attrs, options]); }, /** * Renders the token object as an HTTP Header Value * @instance * @memberof Token * @returns {string} * @see {@link Object#toString()} */ toString: function toString() { if (!this._string) { throw new Error('cannot stringify Token'); } return this._string; }, /** * Uses a non-producation api to return information about this token. This * method is primarily for tests and will throw if NODE_ENV === production * @instance * @memberof Token * @private * @returns {Promise} */ validate: function validate() { var _this5 = this; if (process.env.NODE_ENV === 'production') { throw new Error('Token#validate() must not be used in production'); } return this.webex.request({ method: 'POST', service: 'conversation', resource: 'users/validateAuthToken', body: { token: this.access_token } }).catch(function (reason) { if ('statusCode' in reason) { return _promise.default.reject(reason); } _this5.logger.info("REMINDER: If you're investigating a network error here, it's normal"); // If we got an error that isn't a WebexHttpError, assume the problem is // that we don't have the wdm plugin loaded and service/resource isn't // a valid means of identifying a request. var convApi = process.env.CONVERSATION_SERVICE || process.env.CONVERSATION_SERVICE_URL || 'https://conv-a.wbx2.com/conversation/api/v1'; return _this5.webex.request({ method: 'POST', uri: "".concat(convApi, "/users/validateAuthToken"), body: { token: _this5.access_token }, headers: { authorization: "Bearer ".concat(_this5.access_token) } }); }).then(function (res) { return res.body; }); }, version: "3.9.0" }, ((0, _applyDecoratedDescriptor2.default)(_obj, "downscope", [_dec], (0, _getOwnPropertyDescriptor.default)(_obj, "downscope"), _obj), (0, _applyDecoratedDescriptor2.default)(_obj, "refresh", [_common.oneFlight], (0, _getOwnPropertyDescriptor.default)(_obj, "refresh"), _obj), (0, _applyDecoratedDescriptor2.default)(_obj, "revoke", [_common.oneFlight], (0, _getOwnPropertyDescriptor.default)(_obj, "revoke"), _obj)), _obj))); var _default = exports.default = Token; //# sourceMappingURL=token.js.map