@webex/webex-core
Version:
Plugin handling for Cisco Webex
565 lines (552 loc) • 23.5 kB
JavaScript
"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