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