@webex/webex-core 2.59.2 → 2.59.3-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/.eslintrc.js +6 -6
  2. package/README.md +79 -79
  3. package/babel.config.js +3 -3
  4. package/dist/config.js +24 -24
  5. package/dist/config.js.map +1 -1
  6. package/dist/credentials-config.js +56 -56
  7. package/dist/credentials-config.js.map +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/interceptors/auth.js +28 -28
  10. package/dist/interceptors/auth.js.map +1 -1
  11. package/dist/interceptors/default-options.js +24 -24
  12. package/dist/interceptors/default-options.js.map +1 -1
  13. package/dist/interceptors/embargo.js +9 -9
  14. package/dist/interceptors/embargo.js.map +1 -1
  15. package/dist/interceptors/network-timing.js +19 -19
  16. package/dist/interceptors/network-timing.js.map +1 -1
  17. package/dist/interceptors/payload-transformer.js +19 -19
  18. package/dist/interceptors/payload-transformer.js.map +1 -1
  19. package/dist/interceptors/rate-limit.js +40 -40
  20. package/dist/interceptors/rate-limit.js.map +1 -1
  21. package/dist/interceptors/redirect.js +13 -13
  22. package/dist/interceptors/redirect.js.map +1 -1
  23. package/dist/interceptors/request-event.js +23 -23
  24. package/dist/interceptors/request-event.js.map +1 -1
  25. package/dist/interceptors/request-logger.js +13 -13
  26. package/dist/interceptors/request-logger.js.map +1 -1
  27. package/dist/interceptors/request-timing.js +23 -23
  28. package/dist/interceptors/request-timing.js.map +1 -1
  29. package/dist/interceptors/response-logger.js +19 -19
  30. package/dist/interceptors/response-logger.js.map +1 -1
  31. package/dist/interceptors/user-agent.js +29 -29
  32. package/dist/interceptors/user-agent.js.map +1 -1
  33. package/dist/interceptors/webex-tracking-id.js +15 -15
  34. package/dist/interceptors/webex-tracking-id.js.map +1 -1
  35. package/dist/interceptors/webex-user-agent.js +13 -13
  36. package/dist/interceptors/webex-user-agent.js.map +1 -1
  37. package/dist/lib/batcher.js +83 -83
  38. package/dist/lib/batcher.js.map +1 -1
  39. package/dist/lib/credentials/credentials.js +103 -103
  40. package/dist/lib/credentials/credentials.js.map +1 -1
  41. package/dist/lib/credentials/grant-errors.js +17 -17
  42. package/dist/lib/credentials/grant-errors.js.map +1 -1
  43. package/dist/lib/credentials/index.js +2 -2
  44. package/dist/lib/credentials/index.js.map +1 -1
  45. package/dist/lib/credentials/scope.js +11 -11
  46. package/dist/lib/credentials/scope.js.map +1 -1
  47. package/dist/lib/credentials/token-collection.js +2 -2
  48. package/dist/lib/credentials/token-collection.js.map +1 -1
  49. package/dist/lib/credentials/token.js +145 -145
  50. package/dist/lib/credentials/token.js.map +1 -1
  51. package/dist/lib/page.js +49 -49
  52. package/dist/lib/page.js.map +1 -1
  53. package/dist/lib/services/constants.js.map +1 -1
  54. package/dist/lib/services/index.js +2 -2
  55. package/dist/lib/services/index.js.map +1 -1
  56. package/dist/lib/services/interceptors/server-error.js +9 -9
  57. package/dist/lib/services/interceptors/server-error.js.map +1 -1
  58. package/dist/lib/services/interceptors/service.js +24 -24
  59. package/dist/lib/services/interceptors/service.js.map +1 -1
  60. package/dist/lib/services/metrics.js.map +1 -1
  61. package/dist/lib/services/service-catalog.js +104 -104
  62. package/dist/lib/services/service-catalog.js.map +1 -1
  63. package/dist/lib/services/service-fed-ramp.js.map +1 -1
  64. package/dist/lib/services/service-host.js +134 -134
  65. package/dist/lib/services/service-host.js.map +1 -1
  66. package/dist/lib/services/service-registry.js +175 -175
  67. package/dist/lib/services/service-registry.js.map +1 -1
  68. package/dist/lib/services/service-state.js +38 -38
  69. package/dist/lib/services/service-state.js.map +1 -1
  70. package/dist/lib/services/service-url.js +31 -31
  71. package/dist/lib/services/service-url.js.map +1 -1
  72. package/dist/lib/services/services.js +245 -245
  73. package/dist/lib/services/services.js.map +1 -1
  74. package/dist/lib/stateless-webex-plugin.js +28 -28
  75. package/dist/lib/stateless-webex-plugin.js.map +1 -1
  76. package/dist/lib/storage/decorators.js +27 -27
  77. package/dist/lib/storage/decorators.js.map +1 -1
  78. package/dist/lib/storage/errors.js +4 -4
  79. package/dist/lib/storage/errors.js.map +1 -1
  80. package/dist/lib/storage/index.js.map +1 -1
  81. package/dist/lib/storage/make-webex-plugin-store.js +44 -44
  82. package/dist/lib/storage/make-webex-plugin-store.js.map +1 -1
  83. package/dist/lib/storage/make-webex-store.js +40 -40
  84. package/dist/lib/storage/make-webex-store.js.map +1 -1
  85. package/dist/lib/storage/memory-store-adapter.js +9 -9
  86. package/dist/lib/storage/memory-store-adapter.js.map +1 -1
  87. package/dist/lib/webex-core-plugin-mixin.js +13 -13
  88. package/dist/lib/webex-core-plugin-mixin.js.map +1 -1
  89. package/dist/lib/webex-http-error.js +9 -9
  90. package/dist/lib/webex-http-error.js.map +1 -1
  91. package/dist/lib/webex-internal-core-plugin-mixin.js +13 -13
  92. package/dist/lib/webex-internal-core-plugin-mixin.js.map +1 -1
  93. package/dist/lib/webex-plugin.js +36 -36
  94. package/dist/lib/webex-plugin.js.map +1 -1
  95. package/dist/plugins/logger.js +9 -9
  96. package/dist/plugins/logger.js.map +1 -1
  97. package/dist/webex-core.js +104 -104
  98. package/dist/webex-core.js.map +1 -1
  99. package/dist/webex-internal-core.js +12 -12
  100. package/dist/webex-internal-core.js.map +1 -1
  101. package/jest.config.js +3 -3
  102. package/package.json +20 -19
  103. package/process +1 -1
  104. package/src/config.js +90 -90
  105. package/src/credentials-config.js +212 -212
  106. package/src/index.js +62 -62
  107. package/src/interceptors/auth.js +186 -186
  108. package/src/interceptors/default-options.js +55 -55
  109. package/src/interceptors/embargo.js +43 -43
  110. package/src/interceptors/network-timing.js +54 -54
  111. package/src/interceptors/payload-transformer.js +55 -55
  112. package/src/interceptors/rate-limit.js +169 -169
  113. package/src/interceptors/redirect.js +106 -106
  114. package/src/interceptors/request-event.js +93 -93
  115. package/src/interceptors/request-logger.js +78 -78
  116. package/src/interceptors/request-timing.js +65 -65
  117. package/src/interceptors/response-logger.js +98 -98
  118. package/src/interceptors/user-agent.js +77 -77
  119. package/src/interceptors/webex-tracking-id.js +73 -73
  120. package/src/interceptors/webex-user-agent.js +79 -79
  121. package/src/lib/batcher.js +307 -307
  122. package/src/lib/credentials/credentials.js +552 -552
  123. package/src/lib/credentials/grant-errors.js +92 -92
  124. package/src/lib/credentials/index.js +16 -16
  125. package/src/lib/credentials/scope.js +34 -34
  126. package/src/lib/credentials/token-collection.js +17 -17
  127. package/src/lib/credentials/token.js +559 -559
  128. package/src/lib/page.js +159 -159
  129. package/src/lib/services/constants.js +9 -9
  130. package/src/lib/services/index.js +26 -26
  131. package/src/lib/services/interceptors/server-error.js +48 -48
  132. package/src/lib/services/interceptors/service.js +101 -101
  133. package/src/lib/services/metrics.js +4 -4
  134. package/src/lib/services/service-catalog.js +435 -435
  135. package/src/lib/services/service-fed-ramp.js +4 -4
  136. package/src/lib/services/service-host.js +267 -267
  137. package/src/lib/services/service-registry.js +465 -465
  138. package/src/lib/services/service-state.js +78 -78
  139. package/src/lib/services/service-url.js +124 -124
  140. package/src/lib/services/services.js +1018 -1018
  141. package/src/lib/stateless-webex-plugin.js +98 -98
  142. package/src/lib/storage/decorators.js +220 -220
  143. package/src/lib/storage/errors.js +15 -15
  144. package/src/lib/storage/index.js +10 -10
  145. package/src/lib/storage/make-webex-plugin-store.js +211 -211
  146. package/src/lib/storage/make-webex-store.js +140 -140
  147. package/src/lib/storage/memory-store-adapter.js +79 -79
  148. package/src/lib/webex-core-plugin-mixin.js +114 -114
  149. package/src/lib/webex-http-error.js +61 -61
  150. package/src/lib/webex-internal-core-plugin-mixin.js +107 -107
  151. package/src/lib/webex-plugin.js +222 -222
  152. package/src/plugins/logger.js +60 -60
  153. package/src/webex-core.js +745 -745
  154. package/src/webex-internal-core.js +46 -46
  155. package/test/integration/spec/credentials/credentials.js +139 -139
  156. package/test/integration/spec/credentials/token.js +102 -102
  157. package/test/integration/spec/services/service-catalog.js +838 -838
  158. package/test/integration/spec/services/services.js +1221 -1221
  159. package/test/integration/spec/webex-core.js +178 -178
  160. package/test/unit/spec/_setup.js +44 -44
  161. package/test/unit/spec/credentials/credentials.js +1017 -1017
  162. package/test/unit/spec/credentials/token.js +441 -441
  163. package/test/unit/spec/interceptors/auth.js +521 -521
  164. package/test/unit/spec/interceptors/default-options.js +84 -84
  165. package/test/unit/spec/interceptors/embargo.js +144 -144
  166. package/test/unit/spec/interceptors/network-timing.js +49 -49
  167. package/test/unit/spec/interceptors/payload-transformer.js +155 -155
  168. package/test/unit/spec/interceptors/rate-limit.js +302 -302
  169. package/test/unit/spec/interceptors/redirect.js +102 -102
  170. package/test/unit/spec/interceptors/request-timing.js +92 -92
  171. package/test/unit/spec/interceptors/user-agent.js +76 -76
  172. package/test/unit/spec/interceptors/webex-tracking-id.js +76 -76
  173. package/test/unit/spec/interceptors/webex-user-agent.js +159 -159
  174. package/test/unit/spec/lib/batcher.js +330 -330
  175. package/test/unit/spec/lib/page.js +148 -148
  176. package/test/unit/spec/lib/webex-plugin.js +48 -48
  177. package/test/unit/spec/services/interceptors/server-error.js +204 -204
  178. package/test/unit/spec/services/interceptors/service.js +188 -188
  179. package/test/unit/spec/services/service-catalog.js +194 -194
  180. package/test/unit/spec/services/service-host.js +260 -260
  181. package/test/unit/spec/services/service-registry.js +747 -747
  182. package/test/unit/spec/services/service-state.js +60 -60
  183. package/test/unit/spec/services/service-url.js +258 -258
  184. package/test/unit/spec/services/services.js +348 -348
  185. package/test/unit/spec/storage/persist.js +50 -50
  186. package/test/unit/spec/storage/storage-adapter.js +12 -12
  187. package/test/unit/spec/storage/wait-for-value.js +81 -81
  188. package/test/unit/spec/webex-core.js +253 -253
  189. package/test/unit/spec/webex-internal-core.js +91 -91
@@ -1,552 +1,552 @@
1
- /*!
2
- * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
- */
4
-
5
- import querystring from 'querystring';
6
- import url from 'url';
7
-
8
- import jwt from 'jsonwebtoken';
9
- import {base64, makeStateDataType, oneFlight, tap, whileInFlight} from '@webex/common';
10
- import {safeSetTimeout} from '@webex/common-timers';
11
- import {clone, cloneDeep, isObject, isEmpty} from 'lodash';
12
-
13
- import WebexPlugin from '../webex-plugin';
14
- import {persist, waitForValue} from '../storage/decorators';
15
-
16
- import grantErrors from './grant-errors';
17
- import {filterScope, sortScope} from './scope';
18
- import Token from './token';
19
- import TokenCollection from './token-collection';
20
-
21
- /**
22
- * @class
23
- */
24
- const Credentials = WebexPlugin.extend({
25
- collections: {
26
- userTokens: TokenCollection,
27
- },
28
-
29
- dataTypes: {
30
- token: makeStateDataType(Token, 'token').dataType,
31
- },
32
-
33
- derived: {
34
- canAuthorize: {
35
- deps: ['supertoken', 'supertoken.canAuthorize', 'canRefresh'],
36
- fn() {
37
- return Boolean((this.supertoken && this.supertoken.canAuthorize) || this.canRefresh);
38
- },
39
- },
40
- canRefresh: {
41
- deps: ['supertoken', 'supertoken.canRefresh'],
42
- fn() {
43
- // If we're operating in JWT mode, we have to delegate to the consumer
44
- if (this.config.jwtRefreshCallback) {
45
- return true;
46
- }
47
-
48
- return Boolean(this.supertoken && this.supertoken.canRefresh);
49
- },
50
- },
51
- },
52
-
53
- props: {
54
- supertoken: makeStateDataType(Token, 'token').prop,
55
- },
56
-
57
- namespace: 'Credentials',
58
-
59
- session: {
60
- isRefreshing: {
61
- default: false,
62
- type: 'boolean',
63
- },
64
- /**
65
- * Becomes `true` once the {@link loaded} event fires.
66
- * @see {@link WebexPlugin#ready}
67
- * @instance
68
- * @memberof Credentials
69
- * @type {boolean}
70
- */
71
- ready: {
72
- default: false,
73
- type: 'boolean',
74
- },
75
- refreshTimer: {
76
- default: undefined,
77
- type: 'any',
78
- },
79
- },
80
-
81
- /**
82
- * Generates an OAuth Login URL. Prefers the api.ciscospark.com proxy if the
83
- * instance is initialize with an authorizatUrl, but fallsback to idbroker
84
- * as the base otherwise.
85
- * @instance
86
- * @memberof Credentials
87
- * @param {Object} [options={}]
88
- * @returns {string}
89
- */
90
- buildLoginUrl(options = {clientType: 'public'}) {
91
- /* eslint-disable camelcase */
92
- if (options.state && !isObject(options.state)) {
93
- throw new Error('if specified, `options.state` must be an object');
94
- }
95
-
96
- options.client_id = this.config.client_id;
97
- options.redirect_uri = this.config.redirect_uri;
98
- options.scope = this.config.scope;
99
-
100
- options = cloneDeep(options);
101
-
102
- if (!options.response_type) {
103
- options.response_type = options.clientType === 'public' ? 'token' : 'code';
104
- }
105
- Reflect.deleteProperty(options, 'clientType');
106
-
107
- if (options.state) {
108
- if (!isEmpty(options.state)) {
109
- options.state = base64.toBase64Url(JSON.stringify(options.state));
110
- } else {
111
- delete options.state;
112
- }
113
- }
114
-
115
- return `${this.config.authorizeUrl}?${querystring.stringify(options)}`;
116
- /* eslint-enable camelcase */
117
- },
118
-
119
- /**
120
- * Get the determined OrgId.
121
- *
122
- * @throws {Error} - If the OrgId could not be determined.
123
- * @returns {string} - The OrgId.
124
- */
125
- getOrgId() {
126
- this.logger.info('credentials: attempting to retrieve the OrgId from token');
127
-
128
- try {
129
- // Attempt to extract a client-authenticated token's OrgId.
130
- this.logger.info('credentials: trying to extract OrgId from JWT');
131
-
132
- return this.extractOrgIdFromJWT(this.supertoken.access_token);
133
- } catch (e) {
134
- // Attempt to extract a user token's OrgId.
135
- this.logger.info('credentials: could not extract OrgId from JWT');
136
- this.logger.info('credentials: attempting to extract OrgId from user token');
137
-
138
- try {
139
- return this.extractOrgIdFromUserToken(this.supertoken?.access_token);
140
- } catch (f) {
141
- this.logger.info('credentials: could not extract OrgId from user token');
142
- throw f;
143
- }
144
- }
145
- },
146
-
147
- /**
148
- * Extract the OrgId [realm] from a provided JWT.
149
- *
150
- * @private
151
- * @param {string} token - The JWT to extract the OrgId from.
152
- * @throws {Error} - If the token does not pass JWT general/realm validation.
153
- * @returns {string} - The OrgId.
154
- */
155
- extractOrgIdFromJWT(token = '') {
156
- // Decoded the provided token.
157
- const decodedJWT = jwt.decode(token);
158
-
159
- // Validate that the provided token is a JWT.
160
- if (!decodedJWT) {
161
- throw new Error('unable to extract the OrgId from the provided JWT');
162
- }
163
-
164
- if (!decodedJWT.realm) {
165
- throw new Error('the provided JWT does not contain an OrgId');
166
- }
167
-
168
- // Return the OrgId [realm].
169
- return decodedJWT.realm;
170
- },
171
-
172
- /**
173
- * Extract the OrgId [realm] from a provided user token.
174
- *
175
- * @private
176
- * @param {string} token - The user token to extract the OrgId from.
177
- * @throws {Error} - Will throw an error if the provided token is invalid.
178
- * @returns {string} - The OrgId.
179
- */
180
- extractOrgIdFromUserToken(token = '') {
181
- // Split the provided token into subsections.
182
- const fields = token.split('_');
183
-
184
- // Validate that the provided token has the proper amount of sections.
185
- if (fields.length !== 3) {
186
- throw new Error('the provided token is not a valid format');
187
- }
188
-
189
- // Return the token section that contains the OrgId.
190
- return fields[2];
191
- },
192
-
193
- /**
194
- * Generates a Logout URL
195
- * @instance
196
- * @memberof Credentials
197
- * @param {Object} [options={}]
198
- * @returns {[type]}
199
- */
200
- buildLogoutUrl(options = {}) {
201
- return `${this.config.logoutUrl}?${querystring.stringify({
202
- cisService: this.config.service,
203
- goto: this.config.redirect_uri,
204
- ...options,
205
- })}`;
206
- },
207
-
208
- /**
209
- * Generates a number between 60% - 90% of expired value
210
- * @instance
211
- * @memberof Credentials
212
- * @param {number} expiration
213
- * @private
214
- * @returns {number}
215
- */
216
- calcRefreshTimeout(expiration) {
217
- return Math.floor(((Math.floor(Math.random() * 4) + 6) / 10) * expiration);
218
- },
219
-
220
- constructor(...args) {
221
- // HACK to deal with the fact that AmpersandState#dataTypes#set is a pure
222
- // function.
223
- this._dataTypes = cloneDeep(this._dataTypes);
224
- Object.keys(this._dataTypes).forEach((key) => {
225
- if (this._dataTypes[key].set) {
226
- this._dataTypes[key].set = this._dataTypes[key].set.bind(this);
227
- }
228
- });
229
- // END HACK
230
- Reflect.apply(WebexPlugin, this, args);
231
- },
232
-
233
- /**
234
- * Downscopes a token
235
- * @instance
236
- * @memberof Credentials
237
- * @param {string} scope
238
- * @private
239
- * @returns {Promise<Token>}
240
- */
241
- downscope(scope) {
242
- return this.supertoken.downscope(scope).catch((reason) => {
243
- this.logger.trace(`credentials: failed to downscope supertoken to ${scope}`, reason);
244
- this.logger.trace(`credentials: falling back to supertoken for ${scope}`);
245
-
246
- return Promise.resolve(new Token({scope, ...this.supertoken.serialize()}), {
247
- parent: this,
248
- });
249
- });
250
- },
251
-
252
- /**
253
- * Requests a client credentials grant and returns the token. Given the
254
- * limited use for such tokens as this time, this method does not cache its
255
- * token.
256
- * @instance
257
- * @memberof Credentials
258
- * @param {Object} options
259
- * @returns {Promise<Token>}
260
- */
261
- getClientToken(options = {}) {
262
- this.logger.info('credentials: requesting client credentials grant');
263
-
264
- return this.webex
265
- .request({
266
- /* eslint-disable camelcase */
267
- method: 'POST',
268
- uri: options.uri || this.config.tokenUrl,
269
- form: {
270
- grant_type: 'client_credentials',
271
- scope: options.scope || 'webexsquare:admin',
272
- self_contained_token: true,
273
- },
274
- auth: {
275
- user: this.config.client_id,
276
- pass: this.config.client_secret,
277
- sendImmediately: true,
278
- },
279
- shouldRefreshAccessToken: false,
280
- /* eslint-enable camelcase */
281
- })
282
- .then((res) => new Token(res.body, {parent: this}))
283
- .catch((res) => {
284
- if (res.statusCode !== 400) {
285
- return Promise.reject(res);
286
- }
287
-
288
- const ErrorConstructor = grantErrors.select(res.body.error);
289
-
290
- return Promise.reject(new ErrorConstructor(res._res || res));
291
- });
292
- },
293
-
294
- @oneFlight({keyFactory: (scope) => scope})
295
- @waitForValue('@')
296
- /**
297
- * Resolves with a token with the specified scopes. If no scope is specified,
298
- * defaults to omit(webex.credentials.scope, 'spark:kms'). If no such token is
299
- * available, downscopes the supertoken to that scope.
300
- * @instance
301
- * @memberof Credentials
302
- * @param {string} scope
303
- * @returns {Promise<Token>}
304
- */
305
- getUserToken(scope) {
306
- return Promise.resolve(
307
- !this.isRefreshing ||
308
- new Promise((resolve) => {
309
- this.logger.info(
310
- 'credentials: token refresh inflight; delaying getUserToken until refresh completes'
311
- );
312
- this.once('change:isRefreshing', () => {
313
- this.logger.info('credentials: token refresh complete; reinvoking getUserToken');
314
- resolve();
315
- });
316
- })
317
- ).then(() => {
318
- if (!this.canAuthorize) {
319
- this.logger.info('credentials: cannot produce an access token from current state');
320
-
321
- return Promise.reject(new Error('Current state cannot produce an access token'));
322
- }
323
-
324
- if (!scope) {
325
- scope = filterScope('spark:kms', this.config.scope);
326
- }
327
-
328
- scope = sortScope(scope);
329
-
330
- if (scope === sortScope(this.config.scope)) {
331
- return Promise.resolve(this.supertoken);
332
- }
333
-
334
- const token = this.userTokens.get(scope);
335
-
336
- // we should also check for the token.access_token since token object does
337
- // not get cleared on unsetting while logging out.
338
- if (!token || !token.access_token) {
339
- return this.downscope(scope).then(tap((t) => this.userTokens.add(t)));
340
- }
341
-
342
- return Promise.resolve(token);
343
- });
344
- },
345
-
346
- @persist('@')
347
- /**
348
- * Initializer
349
- * @instance
350
- * @memberof Credentials
351
- * @param {Object} attrs
352
- * @param {Object} options
353
- * @private
354
- * @returns {Credentials}
355
- */
356
- initialize(attrs, options) {
357
- if (attrs) {
358
- if (typeof attrs === 'string') {
359
- this.supertoken = attrs;
360
- }
361
-
362
- if (attrs.access_token) {
363
- this.supertoken = attrs;
364
- }
365
-
366
- if (attrs.authorization) {
367
- if (attrs.authorization.supertoken) {
368
- this.supertoken = attrs.authorization.supertoken;
369
- } else {
370
- this.supertoken = attrs.authorization;
371
- }
372
- }
373
-
374
- // schedule refresh
375
- if (this.supertoken && this.supertoken.expires) {
376
- this.scheduleRefresh(this.supertoken.expires);
377
- }
378
- }
379
-
380
- Reflect.apply(WebexPlugin.prototype.initialize, this, [attrs, options]);
381
-
382
- this.listenToOnce(this.parent, 'change:config', () => {
383
- if (this.config.authorizationString) {
384
- const parsed = url.parse(this.config.authorizationString, true);
385
-
386
- /* eslint-disable camelcase */
387
- this.config.client_id = parsed.query.client_id;
388
- this.config.redirect_uri = parsed.query.redirect_uri;
389
- this.config.scope = parsed.query.scope;
390
- this.config.authorizeUrl = parsed.href.substr(0, parsed.href.indexOf('?'));
391
- /* eslint-enable camelcase */
392
- }
393
- });
394
-
395
- this.webex.once('loaded', () => {
396
- this.ready = true;
397
- });
398
- },
399
-
400
- @oneFlight
401
- @waitForValue('@')
402
- /**
403
- * Clears all tokens from store them from the stores.
404
- *
405
- * This is no longer quite the right name for this method, but all of the
406
- * alternatives I'm coming up with are already taken.
407
- * @instance
408
- * @memberof Credentials
409
- * @returns {Promise}
410
- */
411
- invalidate() {
412
- this.logger.info('credentials: invalidating tokens');
413
-
414
- // clear refresh timer
415
- if (this.refreshTimer) {
416
- clearTimeout(this.refreshTimer);
417
- this.unset('refreshTimer');
418
- }
419
-
420
- try {
421
- this.unset('supertoken');
422
- } catch (err) {
423
- this.logger.warn('credentials: failed to clear supertoken', err);
424
- }
425
-
426
- while (this.userTokens.models.length) {
427
- try {
428
- this.userTokens.remove(this.userTokens.models[0]);
429
- } catch (err) {
430
- this.logger.warn('credentials: failed to remove user token', err);
431
- }
432
- }
433
-
434
- this.logger.info('credentials: finished removing tokens');
435
-
436
- // Return a promise to give the storage layer a tick or two to clear
437
- // localStorage
438
- return Promise.resolve();
439
- },
440
-
441
- @oneFlight
442
- @whileInFlight('isRefreshing')
443
- @waitForValue('@')
444
- /**
445
- * Removes the supertoken and child tokens, then refreshes the supertoken;
446
- * subsequent calls to {@link Credentials#getUserToken()} will re-downscope
447
- * child tokens. Enqueus revocation of previous previousTokens. Yes, that's
448
- * the correct number of "previous"es.
449
- * @instance
450
- * @memberof Credentials
451
- * @returns {Promise}
452
- */
453
- refresh() {
454
- this.logger.info('credentials: refresh requested');
455
-
456
- const {supertoken} = this;
457
- const tokens = clone(this.userTokens.models);
458
-
459
- // This is kind of a leaky abstraction, since it relies on the authorization
460
- // plugin, but the only alternatives I see are
461
- // 1. put all JWT support in core
462
- // 2. have separate jwt and non-jwt auth plugins
463
- // while I like #2 from a code simplicity standpoint, the third-party DX
464
- // isn't great
465
- if (this.config.jwtRefreshCallback) {
466
- return (
467
- this.config
468
- .jwtRefreshCallback(this.webex)
469
- // eslint-disable-next-line no-shadow
470
- .then((jwt) => this.webex.authorization.requestAccessTokenFromJwt({jwt}))
471
- );
472
- }
473
-
474
- if (this.webex.internal.services) {
475
- this.webex.internal.services.updateCredentialsConfig();
476
- }
477
-
478
- return supertoken
479
- .refresh()
480
- .then((st) => {
481
- // clear refresh timer
482
- if (this.refreshTimer) {
483
- clearTimeout(this.refreshTimer);
484
- this.unset('refreshTimer');
485
- }
486
- this.supertoken = st;
487
-
488
- return Promise.all(
489
- tokens.map((token) =>
490
- this.downscope(token.scope)
491
- // eslint-disable-next-line max-nested-callbacks
492
- .then((t) => {
493
- this.logger.info(`credentials: revoking token for ${token.scope}`);
494
-
495
- return token
496
- .revoke()
497
- .catch((err) => {
498
- this.logger.warn('credentials: failed to revoke user token', err);
499
- })
500
- .then(() => {
501
- this.userTokens.remove(token.scope);
502
- this.userTokens.add(t);
503
- });
504
- })
505
- )
506
- );
507
- })
508
- .then(() => {
509
- this.scheduleRefresh(this.supertoken.expires);
510
- })
511
- .catch((error) => {
512
- const {InvalidRequestError} = grantErrors;
513
-
514
- if (error instanceof InvalidRequestError) {
515
- // Error: The refresh token provided is expired, revoked, malformed, or invalid. Hence emit an event to the client, an opportunity to logout.
516
- this.unset('supertoken');
517
- while (this.userTokens.models.length) {
518
- try {
519
- this.userTokens.remove(this.userTokens.models[0]);
520
- } catch (err) {
521
- this.logger.warn('credentials: failed to remove user token', err);
522
- }
523
- }
524
- this.webex.trigger('client:InvalidRequestError');
525
- }
526
-
527
- return Promise.reject(error);
528
- });
529
- },
530
-
531
- /**
532
- * Schedules a token refresh or refreshes the token if token has expired
533
- * @instance
534
- * @memberof Credentials
535
- * @param {number} expires
536
- * @private
537
- * @returns {undefined}
538
- */
539
- scheduleRefresh(expires) {
540
- const expiresIn = expires - Date.now();
541
-
542
- if (expiresIn > 0) {
543
- const timeoutLength = this.calcRefreshTimeout(expiresIn);
544
-
545
- this.refreshTimer = safeSetTimeout(() => this.refresh(), timeoutLength);
546
- } else {
547
- this.refresh();
548
- }
549
- },
550
- });
551
-
552
- export default Credentials;
1
+ /*!
2
+ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ import querystring from 'querystring';
6
+ import url from 'url';
7
+
8
+ import jwt from 'jsonwebtoken';
9
+ import {base64, makeStateDataType, oneFlight, tap, whileInFlight} from '@webex/common';
10
+ import {safeSetTimeout} from '@webex/common-timers';
11
+ import {clone, cloneDeep, isObject, isEmpty} from 'lodash';
12
+
13
+ import WebexPlugin from '../webex-plugin';
14
+ import {persist, waitForValue} from '../storage/decorators';
15
+
16
+ import grantErrors from './grant-errors';
17
+ import {filterScope, sortScope} from './scope';
18
+ import Token from './token';
19
+ import TokenCollection from './token-collection';
20
+
21
+ /**
22
+ * @class
23
+ */
24
+ const Credentials = WebexPlugin.extend({
25
+ collections: {
26
+ userTokens: TokenCollection,
27
+ },
28
+
29
+ dataTypes: {
30
+ token: makeStateDataType(Token, 'token').dataType,
31
+ },
32
+
33
+ derived: {
34
+ canAuthorize: {
35
+ deps: ['supertoken', 'supertoken.canAuthorize', 'canRefresh'],
36
+ fn() {
37
+ return Boolean((this.supertoken && this.supertoken.canAuthorize) || this.canRefresh);
38
+ },
39
+ },
40
+ canRefresh: {
41
+ deps: ['supertoken', 'supertoken.canRefresh'],
42
+ fn() {
43
+ // If we're operating in JWT mode, we have to delegate to the consumer
44
+ if (this.config.jwtRefreshCallback) {
45
+ return true;
46
+ }
47
+
48
+ return Boolean(this.supertoken && this.supertoken.canRefresh);
49
+ },
50
+ },
51
+ },
52
+
53
+ props: {
54
+ supertoken: makeStateDataType(Token, 'token').prop,
55
+ },
56
+
57
+ namespace: 'Credentials',
58
+
59
+ session: {
60
+ isRefreshing: {
61
+ default: false,
62
+ type: 'boolean',
63
+ },
64
+ /**
65
+ * Becomes `true` once the {@link loaded} event fires.
66
+ * @see {@link WebexPlugin#ready}
67
+ * @instance
68
+ * @memberof Credentials
69
+ * @type {boolean}
70
+ */
71
+ ready: {
72
+ default: false,
73
+ type: 'boolean',
74
+ },
75
+ refreshTimer: {
76
+ default: undefined,
77
+ type: 'any',
78
+ },
79
+ },
80
+
81
+ /**
82
+ * Generates an OAuth Login URL. Prefers the api.ciscospark.com proxy if the
83
+ * instance is initialize with an authorizatUrl, but fallsback to idbroker
84
+ * as the base otherwise.
85
+ * @instance
86
+ * @memberof Credentials
87
+ * @param {Object} [options={}]
88
+ * @returns {string}
89
+ */
90
+ buildLoginUrl(options = {clientType: 'public'}) {
91
+ /* eslint-disable camelcase */
92
+ if (options.state && !isObject(options.state)) {
93
+ throw new Error('if specified, `options.state` must be an object');
94
+ }
95
+
96
+ options.client_id = this.config.client_id;
97
+ options.redirect_uri = this.config.redirect_uri;
98
+ options.scope = this.config.scope;
99
+
100
+ options = cloneDeep(options);
101
+
102
+ if (!options.response_type) {
103
+ options.response_type = options.clientType === 'public' ? 'token' : 'code';
104
+ }
105
+ Reflect.deleteProperty(options, 'clientType');
106
+
107
+ if (options.state) {
108
+ if (!isEmpty(options.state)) {
109
+ options.state = base64.toBase64Url(JSON.stringify(options.state));
110
+ } else {
111
+ delete options.state;
112
+ }
113
+ }
114
+
115
+ return `${this.config.authorizeUrl}?${querystring.stringify(options)}`;
116
+ /* eslint-enable camelcase */
117
+ },
118
+
119
+ /**
120
+ * Get the determined OrgId.
121
+ *
122
+ * @throws {Error} - If the OrgId could not be determined.
123
+ * @returns {string} - The OrgId.
124
+ */
125
+ getOrgId() {
126
+ this.logger.info('credentials: attempting to retrieve the OrgId from token');
127
+
128
+ try {
129
+ // Attempt to extract a client-authenticated token's OrgId.
130
+ this.logger.info('credentials: trying to extract OrgId from JWT');
131
+
132
+ return this.extractOrgIdFromJWT(this.supertoken.access_token);
133
+ } catch (e) {
134
+ // Attempt to extract a user token's OrgId.
135
+ this.logger.info('credentials: could not extract OrgId from JWT');
136
+ this.logger.info('credentials: attempting to extract OrgId from user token');
137
+
138
+ try {
139
+ return this.extractOrgIdFromUserToken(this.supertoken?.access_token);
140
+ } catch (f) {
141
+ this.logger.info('credentials: could not extract OrgId from user token');
142
+ throw f;
143
+ }
144
+ }
145
+ },
146
+
147
+ /**
148
+ * Extract the OrgId [realm] from a provided JWT.
149
+ *
150
+ * @private
151
+ * @param {string} token - The JWT to extract the OrgId from.
152
+ * @throws {Error} - If the token does not pass JWT general/realm validation.
153
+ * @returns {string} - The OrgId.
154
+ */
155
+ extractOrgIdFromJWT(token = '') {
156
+ // Decoded the provided token.
157
+ const decodedJWT = jwt.decode(token);
158
+
159
+ // Validate that the provided token is a JWT.
160
+ if (!decodedJWT) {
161
+ throw new Error('unable to extract the OrgId from the provided JWT');
162
+ }
163
+
164
+ if (!decodedJWT.realm) {
165
+ throw new Error('the provided JWT does not contain an OrgId');
166
+ }
167
+
168
+ // Return the OrgId [realm].
169
+ return decodedJWT.realm;
170
+ },
171
+
172
+ /**
173
+ * Extract the OrgId [realm] from a provided user token.
174
+ *
175
+ * @private
176
+ * @param {string} token - The user token to extract the OrgId from.
177
+ * @throws {Error} - Will throw an error if the provided token is invalid.
178
+ * @returns {string} - The OrgId.
179
+ */
180
+ extractOrgIdFromUserToken(token = '') {
181
+ // Split the provided token into subsections.
182
+ const fields = token.split('_');
183
+
184
+ // Validate that the provided token has the proper amount of sections.
185
+ if (fields.length !== 3) {
186
+ throw new Error('the provided token is not a valid format');
187
+ }
188
+
189
+ // Return the token section that contains the OrgId.
190
+ return fields[2];
191
+ },
192
+
193
+ /**
194
+ * Generates a Logout URL
195
+ * @instance
196
+ * @memberof Credentials
197
+ * @param {Object} [options={}]
198
+ * @returns {[type]}
199
+ */
200
+ buildLogoutUrl(options = {}) {
201
+ return `${this.config.logoutUrl}?${querystring.stringify({
202
+ cisService: this.config.service,
203
+ goto: this.config.redirect_uri,
204
+ ...options,
205
+ })}`;
206
+ },
207
+
208
+ /**
209
+ * Generates a number between 60% - 90% of expired value
210
+ * @instance
211
+ * @memberof Credentials
212
+ * @param {number} expiration
213
+ * @private
214
+ * @returns {number}
215
+ */
216
+ calcRefreshTimeout(expiration) {
217
+ return Math.floor(((Math.floor(Math.random() * 4) + 6) / 10) * expiration);
218
+ },
219
+
220
+ constructor(...args) {
221
+ // HACK to deal with the fact that AmpersandState#dataTypes#set is a pure
222
+ // function.
223
+ this._dataTypes = cloneDeep(this._dataTypes);
224
+ Object.keys(this._dataTypes).forEach((key) => {
225
+ if (this._dataTypes[key].set) {
226
+ this._dataTypes[key].set = this._dataTypes[key].set.bind(this);
227
+ }
228
+ });
229
+ // END HACK
230
+ Reflect.apply(WebexPlugin, this, args);
231
+ },
232
+
233
+ /**
234
+ * Downscopes a token
235
+ * @instance
236
+ * @memberof Credentials
237
+ * @param {string} scope
238
+ * @private
239
+ * @returns {Promise<Token>}
240
+ */
241
+ downscope(scope) {
242
+ return this.supertoken.downscope(scope).catch((reason) => {
243
+ this.logger.trace(`credentials: failed to downscope supertoken to ${scope}`, reason);
244
+ this.logger.trace(`credentials: falling back to supertoken for ${scope}`);
245
+
246
+ return Promise.resolve(new Token({scope, ...this.supertoken.serialize()}), {
247
+ parent: this,
248
+ });
249
+ });
250
+ },
251
+
252
+ /**
253
+ * Requests a client credentials grant and returns the token. Given the
254
+ * limited use for such tokens as this time, this method does not cache its
255
+ * token.
256
+ * @instance
257
+ * @memberof Credentials
258
+ * @param {Object} options
259
+ * @returns {Promise<Token>}
260
+ */
261
+ getClientToken(options = {}) {
262
+ this.logger.info('credentials: requesting client credentials grant');
263
+
264
+ return this.webex
265
+ .request({
266
+ /* eslint-disable camelcase */
267
+ method: 'POST',
268
+ uri: options.uri || this.config.tokenUrl,
269
+ form: {
270
+ grant_type: 'client_credentials',
271
+ scope: options.scope || 'webexsquare:admin',
272
+ self_contained_token: true,
273
+ },
274
+ auth: {
275
+ user: this.config.client_id,
276
+ pass: this.config.client_secret,
277
+ sendImmediately: true,
278
+ },
279
+ shouldRefreshAccessToken: false,
280
+ /* eslint-enable camelcase */
281
+ })
282
+ .then((res) => new Token(res.body, {parent: this}))
283
+ .catch((res) => {
284
+ if (res.statusCode !== 400) {
285
+ return Promise.reject(res);
286
+ }
287
+
288
+ const ErrorConstructor = grantErrors.select(res.body.error);
289
+
290
+ return Promise.reject(new ErrorConstructor(res._res || res));
291
+ });
292
+ },
293
+
294
+ @oneFlight({keyFactory: (scope) => scope})
295
+ @waitForValue('@')
296
+ /**
297
+ * Resolves with a token with the specified scopes. If no scope is specified,
298
+ * defaults to omit(webex.credentials.scope, 'spark:kms'). If no such token is
299
+ * available, downscopes the supertoken to that scope.
300
+ * @instance
301
+ * @memberof Credentials
302
+ * @param {string} scope
303
+ * @returns {Promise<Token>}
304
+ */
305
+ getUserToken(scope) {
306
+ return Promise.resolve(
307
+ !this.isRefreshing ||
308
+ new Promise((resolve) => {
309
+ this.logger.info(
310
+ 'credentials: token refresh inflight; delaying getUserToken until refresh completes'
311
+ );
312
+ this.once('change:isRefreshing', () => {
313
+ this.logger.info('credentials: token refresh complete; reinvoking getUserToken');
314
+ resolve();
315
+ });
316
+ })
317
+ ).then(() => {
318
+ if (!this.canAuthorize) {
319
+ this.logger.info('credentials: cannot produce an access token from current state');
320
+
321
+ return Promise.reject(new Error('Current state cannot produce an access token'));
322
+ }
323
+
324
+ if (!scope) {
325
+ scope = filterScope('spark:kms', this.config.scope);
326
+ }
327
+
328
+ scope = sortScope(scope);
329
+
330
+ if (scope === sortScope(this.config.scope)) {
331
+ return Promise.resolve(this.supertoken);
332
+ }
333
+
334
+ const token = this.userTokens.get(scope);
335
+
336
+ // we should also check for the token.access_token since token object does
337
+ // not get cleared on unsetting while logging out.
338
+ if (!token || !token.access_token) {
339
+ return this.downscope(scope).then(tap((t) => this.userTokens.add(t)));
340
+ }
341
+
342
+ return Promise.resolve(token);
343
+ });
344
+ },
345
+
346
+ @persist('@')
347
+ /**
348
+ * Initializer
349
+ * @instance
350
+ * @memberof Credentials
351
+ * @param {Object} attrs
352
+ * @param {Object} options
353
+ * @private
354
+ * @returns {Credentials}
355
+ */
356
+ initialize(attrs, options) {
357
+ if (attrs) {
358
+ if (typeof attrs === 'string') {
359
+ this.supertoken = attrs;
360
+ }
361
+
362
+ if (attrs.access_token) {
363
+ this.supertoken = attrs;
364
+ }
365
+
366
+ if (attrs.authorization) {
367
+ if (attrs.authorization.supertoken) {
368
+ this.supertoken = attrs.authorization.supertoken;
369
+ } else {
370
+ this.supertoken = attrs.authorization;
371
+ }
372
+ }
373
+
374
+ // schedule refresh
375
+ if (this.supertoken && this.supertoken.expires) {
376
+ this.scheduleRefresh(this.supertoken.expires);
377
+ }
378
+ }
379
+
380
+ Reflect.apply(WebexPlugin.prototype.initialize, this, [attrs, options]);
381
+
382
+ this.listenToOnce(this.parent, 'change:config', () => {
383
+ if (this.config.authorizationString) {
384
+ const parsed = url.parse(this.config.authorizationString, true);
385
+
386
+ /* eslint-disable camelcase */
387
+ this.config.client_id = parsed.query.client_id;
388
+ this.config.redirect_uri = parsed.query.redirect_uri;
389
+ this.config.scope = parsed.query.scope;
390
+ this.config.authorizeUrl = parsed.href.substr(0, parsed.href.indexOf('?'));
391
+ /* eslint-enable camelcase */
392
+ }
393
+ });
394
+
395
+ this.webex.once('loaded', () => {
396
+ this.ready = true;
397
+ });
398
+ },
399
+
400
+ @oneFlight
401
+ @waitForValue('@')
402
+ /**
403
+ * Clears all tokens from store them from the stores.
404
+ *
405
+ * This is no longer quite the right name for this method, but all of the
406
+ * alternatives I'm coming up with are already taken.
407
+ * @instance
408
+ * @memberof Credentials
409
+ * @returns {Promise}
410
+ */
411
+ invalidate() {
412
+ this.logger.info('credentials: invalidating tokens');
413
+
414
+ // clear refresh timer
415
+ if (this.refreshTimer) {
416
+ clearTimeout(this.refreshTimer);
417
+ this.unset('refreshTimer');
418
+ }
419
+
420
+ try {
421
+ this.unset('supertoken');
422
+ } catch (err) {
423
+ this.logger.warn('credentials: failed to clear supertoken', err);
424
+ }
425
+
426
+ while (this.userTokens.models.length) {
427
+ try {
428
+ this.userTokens.remove(this.userTokens.models[0]);
429
+ } catch (err) {
430
+ this.logger.warn('credentials: failed to remove user token', err);
431
+ }
432
+ }
433
+
434
+ this.logger.info('credentials: finished removing tokens');
435
+
436
+ // Return a promise to give the storage layer a tick or two to clear
437
+ // localStorage
438
+ return Promise.resolve();
439
+ },
440
+
441
+ @oneFlight
442
+ @whileInFlight('isRefreshing')
443
+ @waitForValue('@')
444
+ /**
445
+ * Removes the supertoken and child tokens, then refreshes the supertoken;
446
+ * subsequent calls to {@link Credentials#getUserToken()} will re-downscope
447
+ * child tokens. Enqueus revocation of previous previousTokens. Yes, that's
448
+ * the correct number of "previous"es.
449
+ * @instance
450
+ * @memberof Credentials
451
+ * @returns {Promise}
452
+ */
453
+ refresh() {
454
+ this.logger.info('credentials: refresh requested');
455
+
456
+ const {supertoken} = this;
457
+ const tokens = clone(this.userTokens.models);
458
+
459
+ // This is kind of a leaky abstraction, since it relies on the authorization
460
+ // plugin, but the only alternatives I see are
461
+ // 1. put all JWT support in core
462
+ // 2. have separate jwt and non-jwt auth plugins
463
+ // while I like #2 from a code simplicity standpoint, the third-party DX
464
+ // isn't great
465
+ if (this.config.jwtRefreshCallback) {
466
+ return (
467
+ this.config
468
+ .jwtRefreshCallback(this.webex)
469
+ // eslint-disable-next-line no-shadow
470
+ .then((jwt) => this.webex.authorization.requestAccessTokenFromJwt({jwt}))
471
+ );
472
+ }
473
+
474
+ if (this.webex.internal.services) {
475
+ this.webex.internal.services.updateCredentialsConfig();
476
+ }
477
+
478
+ return supertoken
479
+ .refresh()
480
+ .then((st) => {
481
+ // clear refresh timer
482
+ if (this.refreshTimer) {
483
+ clearTimeout(this.refreshTimer);
484
+ this.unset('refreshTimer');
485
+ }
486
+ this.supertoken = st;
487
+
488
+ return Promise.all(
489
+ tokens.map((token) =>
490
+ this.downscope(token.scope)
491
+ // eslint-disable-next-line max-nested-callbacks
492
+ .then((t) => {
493
+ this.logger.info(`credentials: revoking token for ${token.scope}`);
494
+
495
+ return token
496
+ .revoke()
497
+ .catch((err) => {
498
+ this.logger.warn('credentials: failed to revoke user token', err);
499
+ })
500
+ .then(() => {
501
+ this.userTokens.remove(token.scope);
502
+ this.userTokens.add(t);
503
+ });
504
+ })
505
+ )
506
+ );
507
+ })
508
+ .then(() => {
509
+ this.scheduleRefresh(this.supertoken.expires);
510
+ })
511
+ .catch((error) => {
512
+ const {InvalidRequestError} = grantErrors;
513
+
514
+ if (error instanceof InvalidRequestError) {
515
+ // Error: The refresh token provided is expired, revoked, malformed, or invalid. Hence emit an event to the client, an opportunity to logout.
516
+ this.unset('supertoken');
517
+ while (this.userTokens.models.length) {
518
+ try {
519
+ this.userTokens.remove(this.userTokens.models[0]);
520
+ } catch (err) {
521
+ this.logger.warn('credentials: failed to remove user token', err);
522
+ }
523
+ }
524
+ this.webex.trigger('client:InvalidRequestError');
525
+ }
526
+
527
+ return Promise.reject(error);
528
+ });
529
+ },
530
+
531
+ /**
532
+ * Schedules a token refresh or refreshes the token if token has expired
533
+ * @instance
534
+ * @memberof Credentials
535
+ * @param {number} expires
536
+ * @private
537
+ * @returns {undefined}
538
+ */
539
+ scheduleRefresh(expires) {
540
+ const expiresIn = expires - Date.now();
541
+
542
+ if (expiresIn > 0) {
543
+ const timeoutLength = this.calcRefreshTimeout(expiresIn);
544
+
545
+ this.refreshTimer = safeSetTimeout(() => this.refresh(), timeoutLength);
546
+ } else {
547
+ this.refresh();
548
+ }
549
+ },
550
+ });
551
+
552
+ export default Credentials;