@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,559 +1,559 @@
1
- /*!
2
- * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
- */
4
-
5
- import {pick} from 'lodash';
6
- import {inBrowser, oneFlight} from '@webex/common';
7
- import {safeSetTimeout} from '@webex/common-timers';
8
-
9
- import WebexHttpError from '../webex-http-error';
10
- import WebexPlugin from '../webex-plugin';
11
-
12
- import {sortScope} from './scope';
13
- import grantErrors, {OAuthError} from './grant-errors';
14
-
15
- /* eslint-disable camelcase */
16
-
17
- /**
18
- * Parse response from CI and converts to structured error when appropriate
19
- * @param {WebexHttpError} res
20
- * @private
21
- * @returns {GrantError}
22
- */
23
- function processGrantError(res) {
24
- if (res.statusCode !== 400) {
25
- return Promise.reject(res);
26
- }
27
-
28
- const ErrorConstructor = grantErrors.select(res.body.error);
29
-
30
- if (ErrorConstructor === OAuthError && res instanceof WebexHttpError) {
31
- return Promise.reject(res);
32
- }
33
- if (!ErrorConstructor) {
34
- return Promise.reject(res);
35
- }
36
-
37
- return Promise.reject(new ErrorConstructor(res._res || res));
38
- }
39
-
40
- /**
41
- * @class
42
- */
43
- const Token = WebexPlugin.extend({
44
- derived: {
45
- /**
46
- * Indicates if this token can be used in an auth header. `true` iff
47
- * {@link Token#access_token} is defined and {@link Token#isExpired} is
48
- * false.
49
- * @instance
50
- * @memberof Token
51
- * @readonly
52
- * @type {boolean}
53
- */
54
- canAuthorize: {
55
- deps: ['access_token', 'isExpired'],
56
- fn() {
57
- return !!this.access_token && !this.isExpired;
58
- },
59
- },
60
-
61
- /**
62
- * Indicates that this token can be downscoped. `true` iff
63
- * {@link config.credentials.client_id} is defined and if
64
- * {@link Token#canAuthorize} is true
65
- *
66
- * Note: since {@link config} is not evented, we can't listen for changes to
67
- * {@link config.credentials.client_id}. As such,
68
- * {@link config.credentials.client_id} must always be set before
69
- * instantiating a {@link Token}
70
- * @instance
71
- * @memberof Token
72
- * @readonly
73
- * @type {boolean}
74
- */
75
- canDownscope: {
76
- deps: ['canAuthorize'],
77
- fn() {
78
- return this.canAuthorize && !!this.config.client_id;
79
- },
80
- },
81
-
82
- /**
83
- * Indicates if this token can be refreshed. `true` iff
84
- * {@link Token@refresh_token} is defined and
85
- * {@link config.credentials.refreshCallback()} is defined
86
- *
87
- * Note: since {@link config} is not evented, we can't listen for changes to
88
- * {@link config.credentials.refreshCallback()}. As such,
89
- * {@link config.credentials.refreshCallback()} must always be set before
90
- * instantiating a {@link Token}
91
- * @instance
92
- * @memberof Token
93
- * @readonly
94
- * @type {boolean}
95
- */
96
- canRefresh: {
97
- deps: ['refresh_token'],
98
- fn() {
99
- if (inBrowser) {
100
- return !!this.refresh_token && !!this.config.refreshCallback;
101
- }
102
-
103
- return !!this.refresh_token && !!this.config.client_secret;
104
- },
105
- },
106
-
107
- /**
108
- * Indicates if this `Token` is expired. `true` iff {@link Token#expires} is
109
- * defined and is less than {@link Date.now()}.
110
- * @instance
111
- * @memberof Token
112
- * @readonly
113
- * @type {boolean}
114
- */
115
- isExpired: {
116
- deps: ['expires', '_isExpired'],
117
- fn() {
118
- // in order to avoid setting `cache:false`, we'll use a private property
119
- // and a timer rather than comparing to `Date.now()`;
120
- return !!this.expires && this._isExpired;
121
- },
122
- },
123
-
124
- /**
125
- * Cache for toString()
126
- * @instance
127
- * @memberof Token
128
- * @private
129
- * @readonly
130
- * @type {string}
131
- */
132
- _string: {
133
- deps: ['access_token', 'token_type'],
134
- fn() {
135
- if (!this.access_token || !this.token_type) {
136
- return '';
137
- }
138
-
139
- return `${this.token_type} ${this.access_token}`;
140
- },
141
- },
142
- },
143
-
144
- namespace: 'Credentials',
145
-
146
- props: {
147
- /**
148
- * Used for indexing in the credentials userTokens collection
149
- * @instance
150
- * @memberof Token
151
- * @private
152
- * @type {string}
153
- */
154
- scope: 'string',
155
- /**
156
- * @instance
157
- * @memberof Token
158
- * @type {string}
159
- */
160
- access_token: 'string',
161
- /**
162
- * @instance
163
- * @memberof Token
164
- * @type {number}
165
- */
166
- expires: 'number',
167
- /**
168
- * @instance
169
- * @memberof Token
170
- * @type {number}
171
- */
172
- expires_in: 'number',
173
- /**
174
- * @instance
175
- * @memberof Token
176
- * @type {string}
177
- */
178
- refresh_token: 'string',
179
- /**
180
- * @instance
181
- * @memberof Token
182
- * @type {number}
183
- */
184
- refresh_token_expires: 'number',
185
- /**
186
- * @instance
187
- * @memberof Token
188
- * @type {number}
189
- */
190
- refresh_token_expires_in: 'number',
191
- /**
192
- * @default "Bearer"
193
- * @instance
194
- * @memberof Token
195
- * @type {string}
196
- */
197
- token_type: {
198
- default: 'Bearer',
199
- type: 'string',
200
- },
201
- },
202
-
203
- session: {
204
- /**
205
- * Used by {@link Token#isExpired} to avoid doing a Date comparison.
206
- * @instance
207
- * @memberof Token
208
- * @private
209
- * @type {boolean}
210
- */
211
- _isExpired: {
212
- default: false,
213
- type: 'boolean',
214
- },
215
- /**
216
- * Handle to the previous token that we'll revoke when we refresh this
217
- * token. The idea is to keep allow two valid tokens when a refresh occurs;
218
- * we don't want revoke a token that's in the middle of being used, so when
219
- * we do a token refresh, we won't revoke the token being refreshed, but
220
- * we'll revoke the previous one.
221
- * @instance
222
- * @memberof Token
223
- * @private
224
- * @type {Object}
225
- */
226
- previousToken: {
227
- type: 'state',
228
- },
229
- },
230
-
231
- @oneFlight({
232
- keyFactory(scope) {
233
- return scope;
234
- },
235
- })
236
- /**
237
- * Uses this token to request a new Token with a subset of this Token's scopes
238
- * @instance
239
- * @memberof Token
240
- * @param {string} scope
241
- * @returns {Promise<Token>}
242
- */
243
- downscope(scope) {
244
- this.logger.info(`token: downscoping token to ${scope}`);
245
-
246
- if (this.isExpired) {
247
- this.logger.info('token: request received to downscope expired access_token');
248
-
249
- return Promise.reject(new Error('cannot downscope expired access token'));
250
- }
251
-
252
- if (!this.canDownscope) {
253
- if (this.config.client_id) {
254
- this.logger.info('token: request received to downscope invalid access_token');
255
- } else {
256
- this.logger.trace('token: cannot downscope without client_id');
257
- }
258
-
259
- return Promise.reject(new Error('cannot downscope access token'));
260
- }
261
-
262
- // Since we're going to use scope as the index in our token collection, it's
263
- // important scopes are always deterministically specified.
264
- if (scope) {
265
- scope = sortScope(scope);
266
- }
267
-
268
- // Ideally, we could depend on the service to communicate this error, but
269
- // all we get is "invalid scope", which, to the lay person, implies
270
- // something wrong with *one* of the scopes, not the whole thing.
271
- if (scope === sortScope(this.config.scope)) {
272
- return Promise.reject(new Error('token: scope reduction requires a reduced scope'));
273
- }
274
-
275
- return this.webex
276
- .request({
277
- method: 'POST',
278
- uri: this.config.tokenUrl,
279
- addAuthHeader: false,
280
- form: {
281
- grant_type: 'urn:cisco:oauth:grant-type:scope-reduction',
282
- token: this.access_token,
283
- scope,
284
- client_id: this.config.client_id,
285
- self_contained_token: true,
286
- },
287
- })
288
- .then((res) => {
289
- this.logger.info(`token: downscoped token to ${scope}`);
290
-
291
- return new Token(Object.assign(res.body, {scope}), {parent: this.parent});
292
- });
293
- },
294
-
295
- /**
296
- * Initializer
297
- * @instance
298
- * @memberof Token
299
- * @param {Object} [attrs={}]
300
- * @param {Object} [options={}]
301
- * @see {@link WebexPlugin#initialize()}
302
- * @returns {Token}
303
- */
304
- initialize(attrs = {}, options = {}) {
305
- Reflect.apply(WebexPlugin.prototype.initialize, this, [attrs, options]);
306
-
307
- if (typeof attrs === 'string') {
308
- this.access_token = attrs;
309
- }
310
-
311
- if (!this.access_token) {
312
- throw new Error('`access_token` is required');
313
- }
314
-
315
- // We don't want the derived property `isExpired` to need {cache:false}, so
316
- // we'll set up a timer the runs when this token should expire.
317
- if (this.expires) {
318
- if (this.expires < Date.now()) {
319
- this._isExpired = true;
320
- } else {
321
- safeSetTimeout(() => {
322
- this._isExpired = true;
323
- }, this.expires - Date.now());
324
- }
325
- }
326
- },
327
-
328
- @oneFlight
329
- /**
330
- * Refreshes this Token. Relies on
331
- * {@link config.credentials.refreshCallback()}
332
- * @instance
333
- * @memberof Token
334
- * @returns {Promise<Token>}
335
- */
336
- refresh() {
337
- if (!this.canRefresh) {
338
- throw new Error('Not enough information available to refresh this access token');
339
- }
340
-
341
- let promise;
342
-
343
- if (inBrowser) {
344
- if (!this.config.refreshCallback) {
345
- throw new Error('Cannot refresh access token without refreshCallback');
346
- }
347
-
348
- promise = Promise.resolve(this.config.refreshCallback(this.webex, this));
349
- }
350
-
351
- return (
352
- promise ||
353
- this.webex
354
- .request({
355
- method: 'POST',
356
- uri: this.config.tokenUrl,
357
- form: {
358
- grant_type: 'refresh_token',
359
- redirect_uri: this.config.redirect_uri,
360
- refresh_token: this.refresh_token,
361
- },
362
- auth: {
363
- user: this.config.client_id,
364
- pass: this.config.client_secret,
365
- sendImmediately: true,
366
- },
367
- shouldRefreshAccessToken: false,
368
- })
369
- .then((res) => res.body)
370
- )
371
- .then((obj) => {
372
- if (!obj) {
373
- throw new Error('token: refreshCallback() did not produce an object');
374
- }
375
- // If the authentication server did not send back a refresh token, copy
376
- // the current refresh token and related values to the response (note:
377
- // at time of implementation, CI never sends a new refresh token)
378
- if (!obj.refresh_token) {
379
- Object.assign(
380
- obj,
381
- pick(this, 'refresh_token', 'refresh_token_expires', 'refresh_token_expires_in')
382
- );
383
- }
384
-
385
- // If the new token is the same as the previous token, then we may have
386
- // found a bug in CI; log the details and reject the Promise
387
- if (this.access_token === obj.access_token) {
388
- this.logger.error('token: new token matches current token');
389
- // log the tokens if it is not production
390
- if (process.env.NODE_ENV !== 'production') {
391
- this.logger.error('token: current token:', this.access_token);
392
- this.logger.error('token: new token:', obj.access_token);
393
- }
394
-
395
- return Promise.reject(new Error('new token matches current token'));
396
- }
397
-
398
- if (this.previousToken) {
399
- this.previousToken.revoke();
400
- this.unset('previousToken');
401
- }
402
-
403
- obj.previousToken = this;
404
- obj.scope = this.scope;
405
-
406
- return new Token(obj, {parent: this.parent});
407
- })
408
- .catch(processGrantError);
409
- },
410
-
411
- @oneFlight
412
- /**
413
- * Revokes this token and unsets its local properties
414
- * @instance
415
- * @memberof Token
416
- * @returns {Promise}
417
- */
418
- revoke() {
419
- if (this.isExpired) {
420
- this.logger.info('token: already expired, not making making revocation request');
421
-
422
- return Promise.resolve();
423
- }
424
-
425
- if (!this.canAuthorize) {
426
- this.logger.info('token: no longer valid, not making revocation request');
427
-
428
- return Promise.resolve();
429
- }
430
-
431
- // FIXME we need to use the user token revocation endpoint to revoke a token
432
- // without a client_secret, but it doesn't current support using a token to
433
- // revoke itself
434
- // Note: I'm not making a canRevoke property because there should be changes
435
- // coming to the user token revocation endpoint that allow us to do this
436
- // correctly.
437
- if (!this.config.client_secret) {
438
- this.logger.info('token: no client secret available, not making revocation request');
439
-
440
- return Promise.resolve();
441
- }
442
-
443
- this.logger.info('token: revoking access token');
444
-
445
- return this.webex
446
- .request({
447
- method: 'POST',
448
- uri: this.config.revokeUrl,
449
- form: {
450
- token: this.access_token,
451
- token_type_hint: 'access_token',
452
- },
453
- auth: {
454
- user: this.config.client_id,
455
- pass: this.config.client_secret,
456
- sendImmediately: true,
457
- },
458
- shouldRefreshAccessToken: false,
459
- })
460
- .then(() => {
461
- this.unset(['access_token', 'expires', 'expires_in', 'token_type']);
462
- this.logger.info('token: access token revoked');
463
- })
464
- .catch(processGrantError);
465
- },
466
-
467
- set(...args) {
468
- // eslint-disable-next-line prefer-const
469
- let [attrs, options] = this._filterSetParameters(...args);
470
-
471
- if (!attrs.token_type && attrs.access_token && attrs.access_token.includes(' ')) {
472
- const [token_type, access_token] = attrs.access_token.split(' ');
473
-
474
- attrs = {...attrs, access_token, token_type};
475
- }
476
- const now = Date.now();
477
-
478
- if (!attrs.expires && attrs.expires_in) {
479
- attrs.expires = now + attrs.expires_in * 1000;
480
- }
481
-
482
- if (!attrs.refresh_token_expires && attrs.refresh_token_expires_in) {
483
- attrs.refresh_token_expires = now + attrs.refresh_token_expires_in * 1000;
484
- }
485
-
486
- if (attrs.scope) {
487
- attrs.scope = sortScope(attrs.scope);
488
- }
489
-
490
- return Reflect.apply(WebexPlugin.prototype.set, this, [attrs, options]);
491
- },
492
-
493
- /**
494
- * Renders the token object as an HTTP Header Value
495
- * @instance
496
- * @memberof Token
497
- * @returns {string}
498
- * @see {@link Object#toString()}
499
- */
500
- toString() {
501
- if (!this._string) {
502
- throw new Error('cannot stringify Token');
503
- }
504
-
505
- return this._string;
506
- },
507
-
508
- /**
509
- * Uses a non-producation api to return information about this token. This
510
- * method is primarily for tests and will throw if NODE_ENV === production
511
- * @instance
512
- * @memberof Token
513
- * @private
514
- * @returns {Promise}
515
- */
516
- validate() {
517
- if (process.env.NODE_ENV === 'production') {
518
- throw new Error('Token#validate() must not be used in production');
519
- }
520
-
521
- return this.webex
522
- .request({
523
- method: 'POST',
524
- service: 'conversation',
525
- resource: 'users/validateAuthToken',
526
- body: {
527
- token: this.access_token,
528
- },
529
- })
530
- .catch((reason) => {
531
- if ('statusCode' in reason) {
532
- return Promise.reject(reason);
533
- }
534
- this.logger.info("REMINDER: If you're investigating a network error here, it's normal");
535
-
536
- // If we got an error that isn't a WebexHttpError, assume the problem is
537
- // that we don't have the wdm plugin loaded and service/resource isn't
538
- // a valid means of identifying a request.
539
- const convApi =
540
- process.env.CONVERSATION_SERVICE ||
541
- process.env.CONVERSATION_SERVICE_URL ||
542
- 'https://conv-a.wbx2.com/conversation/api/v1';
543
-
544
- return this.webex.request({
545
- method: 'POST',
546
- uri: `${convApi}/users/validateAuthToken`,
547
- body: {
548
- token: this.access_token,
549
- },
550
- headers: {
551
- authorization: `Bearer ${this.access_token}`,
552
- },
553
- });
554
- })
555
- .then((res) => res.body);
556
- },
557
- });
558
-
559
- export default Token;
1
+ /*!
2
+ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ import {pick} from 'lodash';
6
+ import {inBrowser, oneFlight} from '@webex/common';
7
+ import {safeSetTimeout} from '@webex/common-timers';
8
+
9
+ import WebexHttpError from '../webex-http-error';
10
+ import WebexPlugin from '../webex-plugin';
11
+
12
+ import {sortScope} from './scope';
13
+ import grantErrors, {OAuthError} from './grant-errors';
14
+
15
+ /* eslint-disable camelcase */
16
+
17
+ /**
18
+ * Parse response from CI and converts to structured error when appropriate
19
+ * @param {WebexHttpError} res
20
+ * @private
21
+ * @returns {GrantError}
22
+ */
23
+ function processGrantError(res) {
24
+ if (res.statusCode !== 400) {
25
+ return Promise.reject(res);
26
+ }
27
+
28
+ const ErrorConstructor = grantErrors.select(res.body.error);
29
+
30
+ if (ErrorConstructor === OAuthError && res instanceof WebexHttpError) {
31
+ return Promise.reject(res);
32
+ }
33
+ if (!ErrorConstructor) {
34
+ return Promise.reject(res);
35
+ }
36
+
37
+ return Promise.reject(new ErrorConstructor(res._res || res));
38
+ }
39
+
40
+ /**
41
+ * @class
42
+ */
43
+ const Token = WebexPlugin.extend({
44
+ derived: {
45
+ /**
46
+ * Indicates if this token can be used in an auth header. `true` iff
47
+ * {@link Token#access_token} is defined and {@link Token#isExpired} is
48
+ * false.
49
+ * @instance
50
+ * @memberof Token
51
+ * @readonly
52
+ * @type {boolean}
53
+ */
54
+ canAuthorize: {
55
+ deps: ['access_token', 'isExpired'],
56
+ fn() {
57
+ return !!this.access_token && !this.isExpired;
58
+ },
59
+ },
60
+
61
+ /**
62
+ * Indicates that this token can be downscoped. `true` iff
63
+ * {@link config.credentials.client_id} is defined and if
64
+ * {@link Token#canAuthorize} is true
65
+ *
66
+ * Note: since {@link config} is not evented, we can't listen for changes to
67
+ * {@link config.credentials.client_id}. As such,
68
+ * {@link config.credentials.client_id} must always be set before
69
+ * instantiating a {@link Token}
70
+ * @instance
71
+ * @memberof Token
72
+ * @readonly
73
+ * @type {boolean}
74
+ */
75
+ canDownscope: {
76
+ deps: ['canAuthorize'],
77
+ fn() {
78
+ return this.canAuthorize && !!this.config.client_id;
79
+ },
80
+ },
81
+
82
+ /**
83
+ * Indicates if this token can be refreshed. `true` iff
84
+ * {@link Token@refresh_token} is defined and
85
+ * {@link config.credentials.refreshCallback()} is defined
86
+ *
87
+ * Note: since {@link config} is not evented, we can't listen for changes to
88
+ * {@link config.credentials.refreshCallback()}. As such,
89
+ * {@link config.credentials.refreshCallback()} must always be set before
90
+ * instantiating a {@link Token}
91
+ * @instance
92
+ * @memberof Token
93
+ * @readonly
94
+ * @type {boolean}
95
+ */
96
+ canRefresh: {
97
+ deps: ['refresh_token'],
98
+ fn() {
99
+ if (inBrowser) {
100
+ return !!this.refresh_token && !!this.config.refreshCallback;
101
+ }
102
+
103
+ return !!this.refresh_token && !!this.config.client_secret;
104
+ },
105
+ },
106
+
107
+ /**
108
+ * Indicates if this `Token` is expired. `true` iff {@link Token#expires} is
109
+ * defined and is less than {@link Date.now()}.
110
+ * @instance
111
+ * @memberof Token
112
+ * @readonly
113
+ * @type {boolean}
114
+ */
115
+ isExpired: {
116
+ deps: ['expires', '_isExpired'],
117
+ fn() {
118
+ // in order to avoid setting `cache:false`, we'll use a private property
119
+ // and a timer rather than comparing to `Date.now()`;
120
+ return !!this.expires && this._isExpired;
121
+ },
122
+ },
123
+
124
+ /**
125
+ * Cache for toString()
126
+ * @instance
127
+ * @memberof Token
128
+ * @private
129
+ * @readonly
130
+ * @type {string}
131
+ */
132
+ _string: {
133
+ deps: ['access_token', 'token_type'],
134
+ fn() {
135
+ if (!this.access_token || !this.token_type) {
136
+ return '';
137
+ }
138
+
139
+ return `${this.token_type} ${this.access_token}`;
140
+ },
141
+ },
142
+ },
143
+
144
+ namespace: 'Credentials',
145
+
146
+ props: {
147
+ /**
148
+ * Used for indexing in the credentials userTokens collection
149
+ * @instance
150
+ * @memberof Token
151
+ * @private
152
+ * @type {string}
153
+ */
154
+ scope: 'string',
155
+ /**
156
+ * @instance
157
+ * @memberof Token
158
+ * @type {string}
159
+ */
160
+ access_token: 'string',
161
+ /**
162
+ * @instance
163
+ * @memberof Token
164
+ * @type {number}
165
+ */
166
+ expires: 'number',
167
+ /**
168
+ * @instance
169
+ * @memberof Token
170
+ * @type {number}
171
+ */
172
+ expires_in: 'number',
173
+ /**
174
+ * @instance
175
+ * @memberof Token
176
+ * @type {string}
177
+ */
178
+ refresh_token: 'string',
179
+ /**
180
+ * @instance
181
+ * @memberof Token
182
+ * @type {number}
183
+ */
184
+ refresh_token_expires: 'number',
185
+ /**
186
+ * @instance
187
+ * @memberof Token
188
+ * @type {number}
189
+ */
190
+ refresh_token_expires_in: 'number',
191
+ /**
192
+ * @default "Bearer"
193
+ * @instance
194
+ * @memberof Token
195
+ * @type {string}
196
+ */
197
+ token_type: {
198
+ default: 'Bearer',
199
+ type: 'string',
200
+ },
201
+ },
202
+
203
+ session: {
204
+ /**
205
+ * Used by {@link Token#isExpired} to avoid doing a Date comparison.
206
+ * @instance
207
+ * @memberof Token
208
+ * @private
209
+ * @type {boolean}
210
+ */
211
+ _isExpired: {
212
+ default: false,
213
+ type: 'boolean',
214
+ },
215
+ /**
216
+ * Handle to the previous token that we'll revoke when we refresh this
217
+ * token. The idea is to keep allow two valid tokens when a refresh occurs;
218
+ * we don't want revoke a token that's in the middle of being used, so when
219
+ * we do a token refresh, we won't revoke the token being refreshed, but
220
+ * we'll revoke the previous one.
221
+ * @instance
222
+ * @memberof Token
223
+ * @private
224
+ * @type {Object}
225
+ */
226
+ previousToken: {
227
+ type: 'state',
228
+ },
229
+ },
230
+
231
+ @oneFlight({
232
+ keyFactory(scope) {
233
+ return scope;
234
+ },
235
+ })
236
+ /**
237
+ * Uses this token to request a new Token with a subset of this Token's scopes
238
+ * @instance
239
+ * @memberof Token
240
+ * @param {string} scope
241
+ * @returns {Promise<Token>}
242
+ */
243
+ downscope(scope) {
244
+ this.logger.info(`token: downscoping token to ${scope}`);
245
+
246
+ if (this.isExpired) {
247
+ this.logger.info('token: request received to downscope expired access_token');
248
+
249
+ return Promise.reject(new Error('cannot downscope expired access token'));
250
+ }
251
+
252
+ if (!this.canDownscope) {
253
+ if (this.config.client_id) {
254
+ this.logger.info('token: request received to downscope invalid access_token');
255
+ } else {
256
+ this.logger.trace('token: cannot downscope without client_id');
257
+ }
258
+
259
+ return Promise.reject(new Error('cannot downscope access token'));
260
+ }
261
+
262
+ // Since we're going to use scope as the index in our token collection, it's
263
+ // important scopes are always deterministically specified.
264
+ if (scope) {
265
+ scope = sortScope(scope);
266
+ }
267
+
268
+ // Ideally, we could depend on the service to communicate this error, but
269
+ // all we get is "invalid scope", which, to the lay person, implies
270
+ // something wrong with *one* of the scopes, not the whole thing.
271
+ if (scope === sortScope(this.config.scope)) {
272
+ return Promise.reject(new Error('token: scope reduction requires a reduced scope'));
273
+ }
274
+
275
+ return this.webex
276
+ .request({
277
+ method: 'POST',
278
+ uri: this.config.tokenUrl,
279
+ addAuthHeader: false,
280
+ form: {
281
+ grant_type: 'urn:cisco:oauth:grant-type:scope-reduction',
282
+ token: this.access_token,
283
+ scope,
284
+ client_id: this.config.client_id,
285
+ self_contained_token: true,
286
+ },
287
+ })
288
+ .then((res) => {
289
+ this.logger.info(`token: downscoped token to ${scope}`);
290
+
291
+ return new Token(Object.assign(res.body, {scope}), {parent: this.parent});
292
+ });
293
+ },
294
+
295
+ /**
296
+ * Initializer
297
+ * @instance
298
+ * @memberof Token
299
+ * @param {Object} [attrs={}]
300
+ * @param {Object} [options={}]
301
+ * @see {@link WebexPlugin#initialize()}
302
+ * @returns {Token}
303
+ */
304
+ initialize(attrs = {}, options = {}) {
305
+ Reflect.apply(WebexPlugin.prototype.initialize, this, [attrs, options]);
306
+
307
+ if (typeof attrs === 'string') {
308
+ this.access_token = attrs;
309
+ }
310
+
311
+ if (!this.access_token) {
312
+ throw new Error('`access_token` is required');
313
+ }
314
+
315
+ // We don't want the derived property `isExpired` to need {cache:false}, so
316
+ // we'll set up a timer the runs when this token should expire.
317
+ if (this.expires) {
318
+ if (this.expires < Date.now()) {
319
+ this._isExpired = true;
320
+ } else {
321
+ safeSetTimeout(() => {
322
+ this._isExpired = true;
323
+ }, this.expires - Date.now());
324
+ }
325
+ }
326
+ },
327
+
328
+ @oneFlight
329
+ /**
330
+ * Refreshes this Token. Relies on
331
+ * {@link config.credentials.refreshCallback()}
332
+ * @instance
333
+ * @memberof Token
334
+ * @returns {Promise<Token>}
335
+ */
336
+ refresh() {
337
+ if (!this.canRefresh) {
338
+ throw new Error('Not enough information available to refresh this access token');
339
+ }
340
+
341
+ let promise;
342
+
343
+ if (inBrowser) {
344
+ if (!this.config.refreshCallback) {
345
+ throw new Error('Cannot refresh access token without refreshCallback');
346
+ }
347
+
348
+ promise = Promise.resolve(this.config.refreshCallback(this.webex, this));
349
+ }
350
+
351
+ return (
352
+ promise ||
353
+ this.webex
354
+ .request({
355
+ method: 'POST',
356
+ uri: this.config.tokenUrl,
357
+ form: {
358
+ grant_type: 'refresh_token',
359
+ redirect_uri: this.config.redirect_uri,
360
+ refresh_token: this.refresh_token,
361
+ },
362
+ auth: {
363
+ user: this.config.client_id,
364
+ pass: this.config.client_secret,
365
+ sendImmediately: true,
366
+ },
367
+ shouldRefreshAccessToken: false,
368
+ })
369
+ .then((res) => res.body)
370
+ )
371
+ .then((obj) => {
372
+ if (!obj) {
373
+ throw new Error('token: refreshCallback() did not produce an object');
374
+ }
375
+ // If the authentication server did not send back a refresh token, copy
376
+ // the current refresh token and related values to the response (note:
377
+ // at time of implementation, CI never sends a new refresh token)
378
+ if (!obj.refresh_token) {
379
+ Object.assign(
380
+ obj,
381
+ pick(this, 'refresh_token', 'refresh_token_expires', 'refresh_token_expires_in')
382
+ );
383
+ }
384
+
385
+ // If the new token is the same as the previous token, then we may have
386
+ // found a bug in CI; log the details and reject the Promise
387
+ if (this.access_token === obj.access_token) {
388
+ this.logger.error('token: new token matches current token');
389
+ // log the tokens if it is not production
390
+ if (process.env.NODE_ENV !== 'production') {
391
+ this.logger.error('token: current token:', this.access_token);
392
+ this.logger.error('token: new token:', obj.access_token);
393
+ }
394
+
395
+ return Promise.reject(new Error('new token matches current token'));
396
+ }
397
+
398
+ if (this.previousToken) {
399
+ this.previousToken.revoke();
400
+ this.unset('previousToken');
401
+ }
402
+
403
+ obj.previousToken = this;
404
+ obj.scope = this.scope;
405
+
406
+ return new Token(obj, {parent: this.parent});
407
+ })
408
+ .catch(processGrantError);
409
+ },
410
+
411
+ @oneFlight
412
+ /**
413
+ * Revokes this token and unsets its local properties
414
+ * @instance
415
+ * @memberof Token
416
+ * @returns {Promise}
417
+ */
418
+ revoke() {
419
+ if (this.isExpired) {
420
+ this.logger.info('token: already expired, not making making revocation request');
421
+
422
+ return Promise.resolve();
423
+ }
424
+
425
+ if (!this.canAuthorize) {
426
+ this.logger.info('token: no longer valid, not making revocation request');
427
+
428
+ return Promise.resolve();
429
+ }
430
+
431
+ // FIXME we need to use the user token revocation endpoint to revoke a token
432
+ // without a client_secret, but it doesn't current support using a token to
433
+ // revoke itself
434
+ // Note: I'm not making a canRevoke property because there should be changes
435
+ // coming to the user token revocation endpoint that allow us to do this
436
+ // correctly.
437
+ if (!this.config.client_secret) {
438
+ this.logger.info('token: no client secret available, not making revocation request');
439
+
440
+ return Promise.resolve();
441
+ }
442
+
443
+ this.logger.info('token: revoking access token');
444
+
445
+ return this.webex
446
+ .request({
447
+ method: 'POST',
448
+ uri: this.config.revokeUrl,
449
+ form: {
450
+ token: this.access_token,
451
+ token_type_hint: 'access_token',
452
+ },
453
+ auth: {
454
+ user: this.config.client_id,
455
+ pass: this.config.client_secret,
456
+ sendImmediately: true,
457
+ },
458
+ shouldRefreshAccessToken: false,
459
+ })
460
+ .then(() => {
461
+ this.unset(['access_token', 'expires', 'expires_in', 'token_type']);
462
+ this.logger.info('token: access token revoked');
463
+ })
464
+ .catch(processGrantError);
465
+ },
466
+
467
+ set(...args) {
468
+ // eslint-disable-next-line prefer-const
469
+ let [attrs, options] = this._filterSetParameters(...args);
470
+
471
+ if (!attrs.token_type && attrs.access_token && attrs.access_token.includes(' ')) {
472
+ const [token_type, access_token] = attrs.access_token.split(' ');
473
+
474
+ attrs = {...attrs, access_token, token_type};
475
+ }
476
+ const now = Date.now();
477
+
478
+ if (!attrs.expires && attrs.expires_in) {
479
+ attrs.expires = now + attrs.expires_in * 1000;
480
+ }
481
+
482
+ if (!attrs.refresh_token_expires && attrs.refresh_token_expires_in) {
483
+ attrs.refresh_token_expires = now + attrs.refresh_token_expires_in * 1000;
484
+ }
485
+
486
+ if (attrs.scope) {
487
+ attrs.scope = sortScope(attrs.scope);
488
+ }
489
+
490
+ return Reflect.apply(WebexPlugin.prototype.set, this, [attrs, options]);
491
+ },
492
+
493
+ /**
494
+ * Renders the token object as an HTTP Header Value
495
+ * @instance
496
+ * @memberof Token
497
+ * @returns {string}
498
+ * @see {@link Object#toString()}
499
+ */
500
+ toString() {
501
+ if (!this._string) {
502
+ throw new Error('cannot stringify Token');
503
+ }
504
+
505
+ return this._string;
506
+ },
507
+
508
+ /**
509
+ * Uses a non-producation api to return information about this token. This
510
+ * method is primarily for tests and will throw if NODE_ENV === production
511
+ * @instance
512
+ * @memberof Token
513
+ * @private
514
+ * @returns {Promise}
515
+ */
516
+ validate() {
517
+ if (process.env.NODE_ENV === 'production') {
518
+ throw new Error('Token#validate() must not be used in production');
519
+ }
520
+
521
+ return this.webex
522
+ .request({
523
+ method: 'POST',
524
+ service: 'conversation',
525
+ resource: 'users/validateAuthToken',
526
+ body: {
527
+ token: this.access_token,
528
+ },
529
+ })
530
+ .catch((reason) => {
531
+ if ('statusCode' in reason) {
532
+ return Promise.reject(reason);
533
+ }
534
+ this.logger.info("REMINDER: If you're investigating a network error here, it's normal");
535
+
536
+ // If we got an error that isn't a WebexHttpError, assume the problem is
537
+ // that we don't have the wdm plugin loaded and service/resource isn't
538
+ // a valid means of identifying a request.
539
+ const convApi =
540
+ process.env.CONVERSATION_SERVICE ||
541
+ process.env.CONVERSATION_SERVICE_URL ||
542
+ 'https://conv-a.wbx2.com/conversation/api/v1';
543
+
544
+ return this.webex.request({
545
+ method: 'POST',
546
+ uri: `${convApi}/users/validateAuthToken`,
547
+ body: {
548
+ token: this.access_token,
549
+ },
550
+ headers: {
551
+ authorization: `Bearer ${this.access_token}`,
552
+ },
553
+ });
554
+ })
555
+ .then((res) => res.body);
556
+ },
557
+ });
558
+
559
+ export default Token;