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

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 +19 -20
  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,1017 +1,1017 @@
1
- /*!
2
- * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
- */
4
-
5
- import {set} from 'lodash';
6
- import {assert} from '@webex/test-helper-chai';
7
- import sinon from 'sinon';
8
- import MockWebex from '@webex/test-helper-mock-webex';
9
- import {Credentials, Token, grantErrors} from '@webex/webex-core';
10
- import {inBrowser} from '@webex/common';
11
- import FakeTimers from '@sinonjs/fake-timers';
12
- import {skipInBrowser} from '@webex/test-helper-mocha';
13
- import Logger from '@webex/plugin-logger';
14
-
15
- /* eslint camelcase: [0] */
16
-
17
- // eslint-disable-next-line no-empty-function
18
- function noop() {}
19
-
20
- function promiseTick(count) {
21
- let promise = Promise.resolve();
22
-
23
- while (count > 1) {
24
- promise = promise.then(() => promiseTick(1));
25
- count -= 1;
26
- }
27
-
28
- return promise;
29
- }
30
-
31
- const AUTHORIZATION_STRING =
32
- 'https://api.ciscospark.com/v1/authorize?client_id=MOCK_CLIENT_ID&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8000&scope=spark%3Arooms_read%20spark%3Ateams_read&state=set_state_here';
33
-
34
- describe('webex-core', () => {
35
- describe('Credentials', () => {
36
- let clock;
37
-
38
- beforeEach(() => {
39
- clock = FakeTimers.install({now: Date.now()});
40
- });
41
-
42
- afterEach(() => {
43
- clock.uninstall();
44
- });
45
-
46
- function makeToken(webex, options) {
47
- return new Token(options, {parent: webex});
48
- }
49
-
50
- describe('#calcRefreshTimeout', () => {
51
- it('generates a number between 60-90% of expiration', () => {
52
- const expiration = 1000;
53
- const webex = new MockWebex();
54
- const credentials = new Credentials(undefined, {parent: webex});
55
- const result = credentials.calcRefreshTimeout(expiration);
56
-
57
- assert.isTrue(result >= expiration * 0.6);
58
- assert.isTrue(result <= expiration * 0.9);
59
- });
60
- });
61
-
62
- describe('#canAuthorize', () => {
63
- it('indicates if the current state has enough information to populate an auth header, even if a token refresh or token downscope is required', () => {
64
- const webex = new MockWebex();
65
-
66
- webex.config.credentials.refreshCallback = inBrowser && noop;
67
- let credentials = new Credentials(undefined, {parent: webex});
68
-
69
- webex.trigger('change:config');
70
- assert.isFalse(credentials.canAuthorize);
71
-
72
- credentials.supertoken = makeToken(webex, {
73
- access_token: 'AT',
74
- });
75
- assert.isTrue(credentials.canAuthorize);
76
-
77
- credentials.supertoken.unset('access_token');
78
- assert.isFalse(credentials.canAuthorize);
79
-
80
- credentials.supertoken = makeToken(webex, {
81
- access_token: 'AT',
82
- });
83
- assert.isTrue(credentials.canAuthorize);
84
-
85
- credentials.supertoken = makeToken(webex, {
86
- access_token: 'AT',
87
- expires: Date.now() - 10000,
88
- });
89
- assert.isFalse(credentials.supertoken.canAuthorize);
90
- assert.isFalse(credentials.canRefresh);
91
- assert.isFalse(credentials.canAuthorize);
92
-
93
- webex.config.credentials.refreshCallback = inBrowser && noop;
94
- credentials = new Credentials(undefined, {parent: webex});
95
- webex.trigger('change:config');
96
- assert.isFalse(credentials.canAuthorize);
97
- credentials.supertoken = makeToken(webex, {
98
- access_token: 'AT',
99
- refresh_token: 'RT',
100
- });
101
- credentials.supertoken.unset('access_token');
102
- assert.isTrue(credentials.canAuthorize);
103
- });
104
- });
105
-
106
- describe('#canRefresh', () => {
107
- it('indicates if there is presently enough information to refresh', () => {
108
- const webex = new MockWebex();
109
- let credentials = new Credentials(undefined, {parent: webex});
110
-
111
- webex.trigger('change:config');
112
- assert.isFalse(credentials.canRefresh);
113
- credentials.supertoken = makeToken(
114
- webex,
115
- {
116
- access_token: 'AT',
117
- },
118
- {parent: true}
119
- );
120
- assert.isFalse(credentials.canRefresh);
121
-
122
- webex.config.credentials.refreshCallback = inBrowser && noop;
123
- credentials = new Credentials(undefined, {parent: webex});
124
- webex.trigger('change:config');
125
- assert.isFalse(credentials.canRefresh);
126
- credentials.supertoken = makeToken(webex, {
127
- access_token: 'AT',
128
- refresh_token: 'RT',
129
- });
130
- assert.isTrue(credentials.supertoken.canRefresh);
131
- assert.isTrue(credentials.canRefresh);
132
- });
133
- });
134
-
135
- describe('#buildLoginUrl()', () => {
136
- it('requires `state` to be an object', () => {
137
- const webex = new MockWebex({
138
- children: {
139
- credentials: Credentials,
140
- },
141
- });
142
-
143
- webex.trigger('change:config)');
144
- assert.doesNotThrow(() => {
145
- webex.credentials.buildLoginUrl();
146
- }, /if specified, `options.state` must be an object/);
147
-
148
- assert.doesNotThrow(() => {
149
- webex.credentials.buildLoginUrl({});
150
- }, /if specified, `options.state` must be an object/);
151
-
152
- assert.throws(() => {
153
- webex.credentials.buildLoginUrl({state: 'state'});
154
- }, /if specified, `options.state` must be an object/);
155
-
156
- assert.doesNotThrow(() => {
157
- webex.credentials.buildLoginUrl({state: {}});
158
- }, /if specified, `options.state` must be an object/);
159
- });
160
-
161
- it('prefers the hydra auth url, but falls back to idbroker', () => {
162
- const webex = new MockWebex();
163
- let credentials = new Credentials(undefined, {parent: webex});
164
-
165
- webex.trigger('change:config');
166
-
167
- assert.match(credentials.buildLoginUrl({state: {}}), /idbroker/);
168
- webex.config.credentials = {
169
- authorizationString: AUTHORIZATION_STRING,
170
- };
171
- credentials = new Credentials({}, {parent: webex});
172
- webex.trigger('change:config');
173
- assert.match(credentials.buildLoginUrl({state: {}}), /api.ciscospark.com/);
174
- });
175
-
176
- skipInBrowser(it)('generates the login url', () => {
177
- const webex = new MockWebex();
178
- const credentials = new Credentials(undefined, {parent: webex});
179
-
180
- webex.trigger('change:config');
181
-
182
- assert.equal(
183
- credentials.buildLoginUrl({state: {page: 'front'}}),
184
- `${
185
- process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
186
- }/idb/oauth2/v1/authorize?state=eyJwYWdlIjoiZnJvbnQifQ&client_id=fake&redirect_uri=http%3A%2F%2Fexample.com&scope=scope%3Aone&response_type=code`
187
- );
188
- });
189
-
190
- skipInBrowser(it)('generates the login url with empty state param', () => {
191
- const webex = new MockWebex();
192
- const credentials = new Credentials(undefined, {parent: webex});
193
-
194
- webex.trigger('change:config');
195
-
196
- assert.equal(
197
- credentials.buildLoginUrl({state: {}}),
198
- `${
199
- process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
200
- }/idb/oauth2/v1/authorize?client_id=fake&redirect_uri=http%3A%2F%2Fexample.com&scope=scope%3Aone&response_type=code`
201
- );
202
- });
203
- });
204
-
205
- describe('#buildLogoutUrl()', () => {
206
- skipInBrowser(it)('generates the logout url', () => {
207
- const webex = new MockWebex();
208
-
209
- webex.config.credentials.redirect_uri = 'ru';
210
- const credentials = new Credentials(undefined, {parent: webex});
211
-
212
- webex.trigger('change:config');
213
- assert.equal(
214
- credentials.buildLogoutUrl(),
215
- `${
216
- process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
217
- }/idb/oauth2/v1/logout?cisService=webex&goto=ru`
218
- );
219
- });
220
-
221
- skipInBrowser(it)('includes a token param if passed', () => {
222
- const webex = new MockWebex();
223
-
224
- webex.config.credentials.redirect_uri = 'ru';
225
- const credentials = new Credentials(undefined, {parent: webex});
226
-
227
- webex.trigger('change:config');
228
- assert.equal(
229
- credentials.buildLogoutUrl({token: 't'}),
230
- `${
231
- process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
232
- }/idb/oauth2/v1/logout?cisService=webex&goto=ru&token=t`
233
- );
234
- });
235
-
236
- it('always fallsback to idbroker', () => {
237
- const webex = new MockWebex();
238
-
239
- webex.config.credentials.redirect_uri = 'ru';
240
- const credentials = new Credentials(undefined, {parent: webex});
241
-
242
- webex.trigger('change:config');
243
- assert.match(credentials.buildLogoutUrl(), /idbroker.*?goto=ru/);
244
- });
245
-
246
- it('allows overriding the goto url', () => {
247
- const webex = new MockWebex();
248
- const credentials = new Credentials(undefined, {parent: webex});
249
-
250
- webex.trigger('change:config');
251
- assert.match(
252
- credentials.buildLogoutUrl({goto: 'http://example.com/'}),
253
- /goto=http%3A%2F%2Fexample.com%2F/
254
- );
255
- });
256
- });
257
-
258
- describe('#getOrgId()', () => {
259
- let credentials;
260
- let orgId;
261
- let webex;
262
-
263
- beforeEach(() => {
264
- webex = new MockWebex();
265
- credentials = new Credentials(undefined, {parent: webex});
266
- });
267
-
268
- it('should return the OrgId of JWT-authenticated user', () => {
269
- credentials.supertoken = {
270
- access_token:
271
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyZWFsbSI6Im15LXJlYWxtIn0.U16gzUsaRW1VVikJA2VeXRHPX716tG1_B42oxzy1UMk',
272
- };
273
-
274
- orgId = 'my-realm';
275
-
276
- assert.equal(credentials.getOrgId(), orgId);
277
- });
278
-
279
- it('should return the OrgId of a user-token-authenticated user', () => {
280
- orgId = 'this-is-an-org-id';
281
-
282
- credentials.supertoken = {
283
- access_token: `000_000_${orgId}`,
284
- };
285
-
286
- assert.equal(credentials.getOrgId(), orgId);
287
- });
288
-
289
- it('should throw if the OrgId was not determined', () =>
290
- expect(() => credentials.getOrgId()).toThrow('the provided token is not a valid format'));
291
- });
292
-
293
- describe('#extractOrgIdFromJWT()', () => {
294
- let credentials;
295
- let webex;
296
-
297
- beforeEach(() => {
298
- webex = new MockWebex();
299
- credentials = new Credentials(undefined, {parent: webex});
300
- });
301
-
302
- it('should return the OrgId of a provided JWT', () => {
303
- const jwt =
304
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyZWFsbSI6Im15LXJlYWxtIn0.U16gzUsaRW1VVikJA2VeXRHPX716tG1_B42oxzy1UMk';
305
- const realm = 'my-realm';
306
-
307
- assert.equal(credentials.extractOrgIdFromJWT(jwt), realm);
308
- });
309
-
310
- it('should throw if the provided JWT is not valid', () =>
311
- expect(() => credentials.extractOrgIdFromJWT('not-valid')).toThrow());
312
-
313
- it('should throw if the provided JWT does not contain an OrgId', () => {
314
- const jwtNoOrg =
315
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
316
-
317
- expect(() => credentials.extractOrgIdFromJWT(jwtNoOrg)).toThrow();
318
- });
319
-
320
- it('should throw if no JWT was provided', () =>
321
- expect(() => credentials.extractOrgIdFromJWT()).toThrow());
322
- });
323
-
324
- describe('#extractOrgIdFromUserToken()', () => {
325
- let credentials;
326
- let webex;
327
-
328
- beforeEach(() => {
329
- webex = new MockWebex();
330
- credentials = new Credentials(undefined, {parent: webex});
331
- });
332
-
333
- it('should return the OrgId of the provided user token', () => {
334
- const orgId = 'this-is-an-org-id';
335
- const userToken = `000_000_${orgId}`;
336
-
337
- assert.equal(credentials.extractOrgIdFromUserToken(userToken), orgId);
338
- });
339
-
340
- it('should throw when provided an invalid token', () =>
341
- expect(() => credentials.extractOrgIdFromUserToken()).toThrow('the provided token is not a valid format'));
342
-
343
- it('should throw when no token is provided', () =>
344
- expect(() => credentials.extractOrgIdFromUserToken()).toThrow());
345
- });
346
-
347
- describe('#initialize()', () => {
348
- it('turns a portal auth string into a configuration object', () => {
349
- const webex = new MockWebex();
350
-
351
- webex.config.credentials = {
352
- client_id: 'ci',
353
- redirect_uri: 'ru',
354
- scope: 's',
355
- };
356
-
357
- let credentials = new Credentials(undefined, {parent: webex});
358
-
359
- webex.trigger('change:config');
360
- webex.trigger('change:config');
361
- assert.equal(webex.config.credentials.client_id, 'ci');
362
- assert.equal(credentials.config.client_id, 'ci');
363
- assert.equal(webex.config.credentials.redirect_uri, 'ru');
364
- assert.equal(credentials.config.redirect_uri, 'ru');
365
- assert.equal(webex.config.credentials.scope, 's');
366
- assert.equal(credentials.config.scope, 's');
367
-
368
- // Accept a portal auth string via environment variables
369
- webex.config.credentials.authorizationString = AUTHORIZATION_STRING;
370
- credentials = new Credentials(undefined, {parent: webex});
371
- webex.trigger('change:config');
372
- webex.trigger('change:config');
373
- assert.equal(webex.config.credentials.client_id, 'MOCK_CLIENT_ID');
374
- assert.equal(credentials.config.client_id, 'MOCK_CLIENT_ID');
375
- assert.equal(webex.config.credentials.redirect_uri, 'http://localhost:8000');
376
- assert.equal(credentials.config.redirect_uri, 'http://localhost:8000');
377
- assert.equal(webex.config.credentials.scope, 'spark:rooms_read spark:teams_read');
378
- assert.equal(credentials.config.scope, 'spark:rooms_read spark:teams_read');
379
- });
380
-
381
- [
382
- 'data',
383
- 'data.access_token',
384
- 'data.supertoken',
385
- 'data.supertoken.access_token',
386
- 'data.authorization',
387
- 'data.authorization.supertoken',
388
- 'data.authorization.supertoken.access_token',
389
- ]
390
- .reduce(
391
- (acc, path) =>
392
- acc.concat(
393
- ['ST', 'Bearer ST'].map((str) => {
394
- const obj = {
395
- msg: `accepts token string "${str}" at path "${path
396
- .split('.')
397
- .slice(1)
398
- .join('.')}"`,
399
- };
400
-
401
- set(obj, path, str);
402
-
403
- return obj;
404
- })
405
- ),
406
- []
407
- )
408
- .forEach(({msg, data}) => {
409
- it(msg, () => {
410
- const webex = new MockWebex();
411
- const credentials = new Credentials(data, {parent: webex});
412
-
413
- assert.isTrue(credentials.canAuthorize);
414
- assert.equal(credentials.supertoken.access_token, 'ST');
415
- assert.equal(credentials.supertoken.token_type, 'Bearer');
416
- });
417
- });
418
-
419
- it('schedules a refreshTimer', () => {
420
- const webex = new MockWebex();
421
- const supertoken = makeToken(webex, {
422
- access_token: 'ST',
423
- refresh_token: 'RT',
424
- expires: Date.now() + 10000,
425
- });
426
- const supertoken2 = makeToken(webex, {
427
- access_token: 'ST2',
428
- refresh_token: 'RT2',
429
- expires: Date.now() + 20000,
430
- });
431
-
432
- sinon.stub(supertoken, 'refresh').returns(Promise.resolve(supertoken2));
433
- const credentials = new Credentials(supertoken, {parent: webex});
434
-
435
- webex.trigger('change:config');
436
-
437
- const firstTimer = credentials.refreshTimer;
438
-
439
- assert.isDefined(firstTimer);
440
- assert.notCalled(supertoken.refresh);
441
- clock.tick(10000);
442
-
443
- return promiseTick(8)
444
- .then(() => assert.called(supertoken.refresh))
445
- .then(() => assert.isDefined(credentials.refreshTimer))
446
- .then(() => assert.notEqual(credentials.refreshTimer, firstTimer));
447
- });
448
-
449
- it.skip('does not schedule a refreshTimer', () => {
450
- const webex = new MockWebex();
451
- const supertoken = makeToken(webex, {
452
- access_token: 'ST',
453
- refresh_token: 'RT',
454
- expires: Date.now() - 10000,
455
- });
456
-
457
- sinon.stub(supertoken, 'refresh').returns(Promise.reject());
458
- const credentials = new Credentials(supertoken, {parent: webex});
459
-
460
- webex.trigger('change:config');
461
-
462
- assert.isUndefined(credentials.refreshTimer);
463
- });
464
- });
465
-
466
- describe('#getUserToken()', () => {
467
- // it('resolves with the supertoken if the supertoken matches the requested scopes');
468
-
469
- it('resolves with the token identified by the specified scopes', () => {
470
- const webex = new MockWebex();
471
- const credentials = new Credentials(undefined, {parent: webex});
472
-
473
- webex.trigger('change:config');
474
- const st = makeToken(webex, {access_token: 'ST'});
475
- const t1 = makeToken(webex, {
476
- access_token: 'AT1',
477
- scope: 'scope1',
478
- });
479
- const t2 = makeToken(webex, {
480
- access_token: 'AT2',
481
- scope: 'scope2',
482
- });
483
-
484
- credentials.set({
485
- supertoken: st,
486
- userTokens: [t1, t2],
487
- });
488
-
489
- return Promise.all([
490
- credentials.getUserToken('scope1').then((result) => assert.deepEqual(result, t1)),
491
- credentials.getUserToken('scope2').then((result) => assert.deepEqual(result, t2)),
492
- ]);
493
- });
494
-
495
- describe('when no matching token is found', () => {
496
- it('downscopes the supertoken', () => {
497
- const webex = new MockWebex();
498
- const credentials = new Credentials(undefined, {parent: webex});
499
-
500
- webex.trigger('change:config');
501
-
502
- credentials.supertoken = makeToken(webex, {
503
- access_token: 'ST',
504
- });
505
-
506
- const t2 = makeToken(webex, {
507
- access_token: 'AT2',
508
- });
509
-
510
- sinon.stub(credentials.supertoken, 'downscope').returns(Promise.resolve(t2));
511
-
512
- const t1 = makeToken(webex, {
513
- access_token: 'AT1',
514
- scope: 'scope1',
515
- });
516
-
517
- credentials.set({
518
- userTokens: [t1],
519
- });
520
-
521
- return credentials
522
- .getUserToken('scope2')
523
- .then((result) => assert.deepEqual(result, t2))
524
- .then(() => assert.calledWith(credentials.supertoken.downscope, 'scope2'));
525
- });
526
- });
527
-
528
- describe('when no scope is specified', () => {
529
- it('resolves with a token containing all but the kms scopes', () => {
530
- const webex = new MockWebex();
531
-
532
- webex.config.credentials.scope = 'scope1 spark:kms';
533
- const credentials = new Credentials(undefined, {parent: webex});
534
-
535
- webex.trigger('change:config');
536
-
537
- credentials.supertoken = makeToken(webex, {
538
- access_token: 'ST',
539
- });
540
-
541
- // const t2 = makeToken(webex, {
542
- // access_token: `AT2`
543
- // });
544
-
545
- // sinon.stub(credentials.supertoken, `downscope`).returns(Promise.resolve(t2));
546
-
547
- const t1 = makeToken(webex, {
548
- access_token: 'AT1',
549
- scope: 'scope1',
550
- });
551
-
552
- credentials.set({
553
- userTokens: [t1],
554
- });
555
-
556
- return credentials.getUserToken().then((result) => assert.deepEqual(result, t1));
557
- });
558
- });
559
-
560
- describe('when the kms downscope request fails', () => {
561
- it('falls back to the supertoken', () => {
562
- const webex = new MockWebex({
563
- children: {
564
- logger: Logger,
565
- },
566
- });
567
-
568
- webex.config.credentials.scope = 'scope1 spark:kms';
569
- const credentials = new Credentials(undefined, {parent: webex});
570
-
571
- webex.trigger('change:config');
572
-
573
- credentials.supertoken = makeToken(webex, {
574
- access_token: 'ST',
575
- });
576
-
577
- sinon
578
- .stub(credentials.supertoken, 'downscope')
579
- .returns(Promise.reject(new Error('downscope failed')));
580
-
581
- const t1 = makeToken(webex, {
582
- access_token: 'AT1',
583
- scope: 'scope1',
584
- });
585
-
586
- credentials.set({
587
- userTokens: [t1],
588
- });
589
-
590
- return credentials
591
- .getUserToken('scope2')
592
- .then((t) => assert.equal(t.access_token, credentials.supertoken.access_token));
593
- });
594
- });
595
-
596
- it('is blocked while a token refresh is inflight', () => {
597
- const webex = new MockWebex();
598
-
599
- webex.config.credentials.scope = 'scope1 spark:kms';
600
- const credentials = new Credentials(undefined, {parent: webex});
601
-
602
- webex.trigger('change:config');
603
-
604
- const supertoken1 = makeToken(webex, {
605
- access_token: 'ST1',
606
- refresh_token: 'RT1',
607
- });
608
-
609
- credentials.set({supertoken: supertoken1});
610
-
611
- sinon
612
- .stub(supertoken1, 'downscope')
613
- .returns(Promise.resolve(new Token({access_token: 'ST1ATD'})));
614
- const supertoken2 = makeToken(webex, {
615
- access_token: 'ST2',
616
- });
617
-
618
- sinon.stub(supertoken1, 'refresh').returns(Promise.resolve(supertoken2));
619
-
620
- const at2 = makeToken(webex, {access_token: 'ST2ATD'});
621
-
622
- sinon.stub(supertoken2, 'downscope').returns(Promise.resolve(at2));
623
-
624
- return Promise.all([
625
- credentials.refresh(),
626
- credentials.getUserToken('scope2').then((result) => assert.deepEqual(result, at2)),
627
- ]);
628
- });
629
- });
630
-
631
- describe('#invalidate()', () => {
632
- it('clears the refreshTimer', () => {
633
- const webex = new MockWebex();
634
- const credentials = new Credentials(undefined, {parent: webex});
635
-
636
- webex.trigger('change:config');
637
- const st = makeToken(webex, {
638
- access_token: 'ST',
639
- refresh_token: 'RT',
640
- });
641
-
642
- const st2 = makeToken(webex, {
643
- access_token: 'ST2',
644
- refresh_token: 'RT2',
645
- });
646
-
647
- credentials.set({
648
- supertoken: st,
649
- });
650
-
651
- sinon.stub(credentials, 'refresh').returns(Promise.resolve(st2));
652
-
653
- credentials.scheduleRefresh(Date.now() + 10000);
654
- assert.isDefined(credentials.refreshTimer);
655
- assert.notCalled(credentials.refresh);
656
-
657
- return credentials.invalidate().then(() => {
658
- clock.tick(10000);
659
- assert.isUndefined(credentials.refreshTimer);
660
- assert.notCalled(credentials.refresh);
661
- });
662
- });
663
-
664
- it('clears the tokens from boundedStorage', () => {
665
- const webex = new MockWebex();
666
- const credentials = new Credentials(undefined, {parent: webex});
667
-
668
- webex.trigger('change:config');
669
- const st = makeToken(webex, {
670
- access_token: 'ST',
671
- });
672
-
673
- const t1 = makeToken(webex, {
674
- access_token: 'AT1',
675
- scope: 'scope1',
676
- });
677
-
678
- const t2 = makeToken(webex, {
679
- access_token: 'AT2',
680
- scope: 'scope2',
681
- });
682
-
683
- credentials.set({
684
- supertoken: st,
685
- userTokens: [t1, t2],
686
- });
687
-
688
- return new Promise((resolve) => {
689
- setTimeout(resolve, 1);
690
- clock.tick(1000);
691
- })
692
- .then(() => webex.boundedStorage.get('Credentials', '@'))
693
- .then((data) => {
694
- assert.equal(data.userTokens[0].access_token, t1.access_token);
695
- assert.equal(data.userTokens[1].access_token, t2.access_token);
696
-
697
- return credentials.invalidate();
698
- })
699
- .then(() => promiseTick(500))
700
- .then(
701
- () =>
702
- new Promise((resolve) => {
703
- setTimeout(resolve, 1);
704
- clock.tick(1000);
705
- })
706
- )
707
- .then(() => promiseTick(500))
708
- .then(
709
- () =>
710
- new Promise((resolve) => {
711
- setTimeout(resolve, 1);
712
- clock.tick(1000);
713
- })
714
- )
715
- .then(() => assert.isRejected(webex.boundedStorage.get('Credentials', '@'), /NotFound/));
716
- });
717
-
718
-
719
- // it('does not induce any token refreshes');
720
-
721
- it('prevents #getUserToken() from being invoked', () => {
722
- const webex = new MockWebex();
723
- const credentials = new Credentials(undefined, {parent: webex});
724
-
725
- webex.trigger('change:config');
726
- const st = makeToken(webex, {
727
- access_token: 'ST',
728
- refresh_token: 'RT',
729
- });
730
-
731
- const t1 = makeToken(webex, {
732
- access_token: 'AT1',
733
- scope: 'scope1',
734
- });
735
-
736
- credentials.set({
737
- supertoken: st,
738
- userTokens: [t1],
739
- });
740
-
741
- return credentials
742
- .invalidate()
743
- .then(() =>
744
- assert.isRejected(
745
- credentials.getUserToken(),
746
- /Current state cannot produce an access token/
747
- )
748
- );
749
- });
750
- });
751
-
752
- describe('#refresh()', () => {
753
- it('refreshes and downscopes the supertoken, and revokes previous tokens', () => {
754
- const webex = new MockWebex();
755
- const credentials = new Credentials(undefined, {parent: webex});
756
-
757
- webex.trigger('change:config');
758
- const st = makeToken(webex, {
759
- access_token: 'ST',
760
- refresh_token: 'RT',
761
- });
762
-
763
- const st2 = makeToken(webex, {
764
- access_token: 'ST2',
765
- refresh_token: 'RT2',
766
- });
767
-
768
- const t1 = makeToken(webex, {
769
- access_token: 'AT1',
770
- scope: 'scope1',
771
- });
772
-
773
- const t2 = makeToken(webex, {
774
- access_token: 'AT2',
775
- scope: 'scope2',
776
- });
777
-
778
- sinon.stub(st2, 'downscope').returns(Promise.resolve(t2));
779
- sinon.stub(st, 'refresh').returns(Promise.resolve(st2));
780
- sinon.stub(t1, 'revoke').returns(Promise.resolve());
781
- sinon.spy(credentials, 'scheduleRefresh');
782
-
783
- credentials.set({
784
- supertoken: st,
785
- userTokens: [t1],
786
- });
787
-
788
- assert.equal(credentials.userTokens.get(t1.scope), t1);
789
-
790
- return credentials
791
- .refresh()
792
- .then(() => assert.called(st.refresh))
793
- .then(() => assert.calledWith(st2.downscope, 'scope1'))
794
- .then(() => assert.called(t1.revoke))
795
- .then(() => assert.isUndefined(credentials.userTokens.get(t1.scope)))
796
- .then(() => assert.equal(credentials.userTokens.get(t2.scope), t2))
797
- .then(() => assert.calledWith(credentials.scheduleRefresh, st.expires));
798
- });
799
-
800
- it('refreshes and downscopes the supertoken even if revocation of previous token fails', () => {
801
- const webex = new MockWebex();
802
- const credentials = new Credentials(undefined, {parent: webex});
803
-
804
- webex.trigger('change:config');
805
- const st = makeToken(webex, {
806
- access_token: 'ST',
807
- refresh_token: 'RT',
808
- });
809
-
810
- const st2 = makeToken(webex, {
811
- access_token: 'ST2',
812
- refresh_token: 'RT2',
813
- });
814
-
815
- const t1 = makeToken(webex, {
816
- access_token: 'AT1',
817
- scope: 'scope1',
818
- });
819
-
820
- const t2 = makeToken(webex, {
821
- access_token: 'AT2',
822
- scope: 'scope2',
823
- });
824
-
825
- sinon.stub(st2, 'downscope').returns(Promise.resolve(t2));
826
- sinon.stub(st, 'refresh').returns(Promise.resolve(st2));
827
- sinon.stub(t1, 'revoke').returns(Promise.reject());
828
- sinon.spy(credentials, 'scheduleRefresh');
829
-
830
- credentials.set({
831
- supertoken: st,
832
- userTokens: [t1],
833
- });
834
-
835
- assert.equal(credentials.userTokens.get(t1.scope), t1);
836
-
837
- return credentials
838
- .refresh()
839
- .then(() => assert.called(st.refresh))
840
- .then(() => assert.calledWith(st2.downscope, 'scope1'))
841
- .then(() => assert.called(t1.revoke))
842
- .then(() => assert.isUndefined(credentials.userTokens.get(t1.scope)))
843
- .then(() => assert.equal(credentials.userTokens.get(t2.scope), t2))
844
- .then(() => assert.calledWith(credentials.scheduleRefresh, st.expires));
845
- });
846
-
847
- it('removes and revokes all child tokens', () => {
848
- const webex = new MockWebex({
849
- children: {
850
- logger: Logger,
851
- },
852
- });
853
- const credentials = new Credentials(undefined, {parent: webex});
854
-
855
- webex.trigger('change:config');
856
- const st = makeToken(webex, {
857
- access_token: 'ST',
858
- refresh_token: 'RT',
859
- });
860
-
861
- sinon.stub(st, 'refresh').returns(Promise.resolve(makeToken(webex, {access_token: 'ST2'})));
862
-
863
- const t1 = makeToken(webex, {
864
- access_token: 'AT1',
865
- scope: 'scope1',
866
- });
867
-
868
- credentials.set({
869
- supertoken: st,
870
- userTokens: [t1],
871
- });
872
-
873
- return credentials.refresh().then(() => assert.called(st.refresh));
874
- });
875
-
876
- it('allows #getUserToken() to be revoked, but #getUserToken() promises will not resolve until the suport token has been refreshed', () => {
877
- const webex = new MockWebex();
878
- const credentials = new Credentials(undefined, {parent: webex});
879
-
880
- webex.trigger('change:config');
881
- const st1 = makeToken(webex, {
882
- access_token: 'ST1',
883
- refresh_token: 'RT1',
884
- });
885
-
886
- const st2 = makeToken(webex, {
887
- access_token: 'ST2',
888
- refresh_token: 'RT1',
889
- });
890
-
891
- const t1 = makeToken(webex, {
892
- access_token: 'AT1',
893
- scope: 'scope1',
894
- });
895
-
896
- const t2 = makeToken(webex, {
897
- access_token: 'AT2',
898
- scope: 'scope1',
899
- });
900
-
901
- sinon.stub(st1, 'refresh').returns(Promise.resolve(st2));
902
- sinon.stub(st2, 'downscope').returns(Promise.resolve(t2));
903
-
904
- credentials.set({
905
- supertoken: st1,
906
- userTokens: [t1],
907
- });
908
-
909
- credentials.refresh();
910
-
911
- return credentials.getUserToken('scope1').then((result) => assert.deepEqual(result, t2));
912
- });
913
-
914
- it('emits InvalidRequestError when the refresh token and access token expire', () => {
915
- const webex = new MockWebex();
916
- const credentials = new Credentials(undefined, {parent: webex});
917
-
918
- webex.trigger('change:config');
919
- const st = makeToken(webex, {
920
- access_token: 'ST',
921
- refresh_token: 'RT',
922
- });
923
-
924
- const t1 = makeToken(webex, {
925
- access_token: 'AT1',
926
- scope: 'scope1',
927
- });
928
-
929
- const res = {
930
- body: {
931
- error: 'invalid_request',
932
- error_description:
933
- 'The refresh token provided is expired, revoked, malformed, or invalid.',
934
- trackingID: 'test123',
935
- },
936
- };
937
-
938
- const ErrorConstructor = grantErrors.select(res.body.error);
939
-
940
- sinon
941
- .stub(st, 'refresh')
942
- .returns(Promise.reject(new ErrorConstructor('InvalidRequestError')));
943
- sinon.stub(credentials, 'unset').returns(Promise.resolve());
944
- const triggerSpy = sinon.spy(webex, 'trigger');
945
-
946
- credentials.set({
947
- supertoken: st,
948
- userTokens: [t1],
949
- });
950
-
951
- return credentials
952
- .refresh()
953
- .then(() => assert.called(st.refresh))
954
- .catch(() => {
955
- assert.called(credentials.unset);
956
- assert.calledWith(triggerSpy, sinon.match('client:InvalidRequestError'));
957
- });
958
- });
959
- });
960
-
961
- describe('#scheduleRefresh()', () => {
962
- it('refreshes token immediately if token is expired', () => {
963
- const webex = new MockWebex();
964
- const credentials = new Credentials(undefined, {parent: webex});
965
-
966
- webex.trigger('change:config');
967
- const st = makeToken(webex, {
968
- access_token: 'ST',
969
- refresh_token: 'RT',
970
- });
971
-
972
- const st2 = makeToken(webex, {
973
- access_token: 'ST2',
974
- refresh_token: 'RT2',
975
- });
976
-
977
- credentials.set({
978
- supertoken: st,
979
- });
980
-
981
- sinon.stub(credentials, 'refresh').returns(Promise.resolve(st2));
982
-
983
- credentials.scheduleRefresh(Date.now() - 10000);
984
- assert.isUndefined(credentials.refreshTimer);
985
- assert.called(credentials.refresh);
986
- });
987
-
988
- it('schedules a token refresh', () => {
989
- const webex = new MockWebex();
990
- const credentials = new Credentials(undefined, {parent: webex});
991
-
992
- webex.trigger('change:config');
993
- const st = makeToken(webex, {
994
- access_token: 'ST',
995
- refresh_token: 'RT',
996
- });
997
-
998
- const st2 = makeToken(webex, {
999
- access_token: 'ST2',
1000
- refresh_token: 'RT2',
1001
- });
1002
-
1003
- credentials.set({
1004
- supertoken: st,
1005
- });
1006
-
1007
- sinon.stub(credentials, 'refresh').returns(Promise.resolve(st2));
1008
-
1009
- credentials.scheduleRefresh(Date.now() + 10000);
1010
- assert.isDefined(credentials.refreshTimer);
1011
- assert.notCalled(credentials.refresh);
1012
- clock.tick(10000);
1013
- assert.called(credentials.refresh);
1014
- });
1015
- });
1016
- });
1017
- });
1
+ /*!
2
+ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ import {set} from 'lodash';
6
+ import {assert} from '@webex/test-helper-chai';
7
+ import sinon from 'sinon';
8
+ import MockWebex from '@webex/test-helper-mock-webex';
9
+ import {Credentials, Token, grantErrors} from '@webex/webex-core';
10
+ import {inBrowser} from '@webex/common';
11
+ import FakeTimers from '@sinonjs/fake-timers';
12
+ import {skipInBrowser} from '@webex/test-helper-mocha';
13
+ import Logger from '@webex/plugin-logger';
14
+
15
+ /* eslint camelcase: [0] */
16
+
17
+ // eslint-disable-next-line no-empty-function
18
+ function noop() {}
19
+
20
+ function promiseTick(count) {
21
+ let promise = Promise.resolve();
22
+
23
+ while (count > 1) {
24
+ promise = promise.then(() => promiseTick(1));
25
+ count -= 1;
26
+ }
27
+
28
+ return promise;
29
+ }
30
+
31
+ const AUTHORIZATION_STRING =
32
+ 'https://api.ciscospark.com/v1/authorize?client_id=MOCK_CLIENT_ID&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8000&scope=spark%3Arooms_read%20spark%3Ateams_read&state=set_state_here';
33
+
34
+ describe('webex-core', () => {
35
+ describe('Credentials', () => {
36
+ let clock;
37
+
38
+ beforeEach(() => {
39
+ clock = FakeTimers.install({now: Date.now()});
40
+ });
41
+
42
+ afterEach(() => {
43
+ clock.uninstall();
44
+ });
45
+
46
+ function makeToken(webex, options) {
47
+ return new Token(options, {parent: webex});
48
+ }
49
+
50
+ describe('#calcRefreshTimeout', () => {
51
+ it('generates a number between 60-90% of expiration', () => {
52
+ const expiration = 1000;
53
+ const webex = new MockWebex();
54
+ const credentials = new Credentials(undefined, {parent: webex});
55
+ const result = credentials.calcRefreshTimeout(expiration);
56
+
57
+ assert.isTrue(result >= expiration * 0.6);
58
+ assert.isTrue(result <= expiration * 0.9);
59
+ });
60
+ });
61
+
62
+ describe('#canAuthorize', () => {
63
+ it('indicates if the current state has enough information to populate an auth header, even if a token refresh or token downscope is required', () => {
64
+ const webex = new MockWebex();
65
+
66
+ webex.config.credentials.refreshCallback = inBrowser && noop;
67
+ let credentials = new Credentials(undefined, {parent: webex});
68
+
69
+ webex.trigger('change:config');
70
+ assert.isFalse(credentials.canAuthorize);
71
+
72
+ credentials.supertoken = makeToken(webex, {
73
+ access_token: 'AT',
74
+ });
75
+ assert.isTrue(credentials.canAuthorize);
76
+
77
+ credentials.supertoken.unset('access_token');
78
+ assert.isFalse(credentials.canAuthorize);
79
+
80
+ credentials.supertoken = makeToken(webex, {
81
+ access_token: 'AT',
82
+ });
83
+ assert.isTrue(credentials.canAuthorize);
84
+
85
+ credentials.supertoken = makeToken(webex, {
86
+ access_token: 'AT',
87
+ expires: Date.now() - 10000,
88
+ });
89
+ assert.isFalse(credentials.supertoken.canAuthorize);
90
+ assert.isFalse(credentials.canRefresh);
91
+ assert.isFalse(credentials.canAuthorize);
92
+
93
+ webex.config.credentials.refreshCallback = inBrowser && noop;
94
+ credentials = new Credentials(undefined, {parent: webex});
95
+ webex.trigger('change:config');
96
+ assert.isFalse(credentials.canAuthorize);
97
+ credentials.supertoken = makeToken(webex, {
98
+ access_token: 'AT',
99
+ refresh_token: 'RT',
100
+ });
101
+ credentials.supertoken.unset('access_token');
102
+ assert.isTrue(credentials.canAuthorize);
103
+ });
104
+ });
105
+
106
+ describe('#canRefresh', () => {
107
+ it('indicates if there is presently enough information to refresh', () => {
108
+ const webex = new MockWebex();
109
+ let credentials = new Credentials(undefined, {parent: webex});
110
+
111
+ webex.trigger('change:config');
112
+ assert.isFalse(credentials.canRefresh);
113
+ credentials.supertoken = makeToken(
114
+ webex,
115
+ {
116
+ access_token: 'AT',
117
+ },
118
+ {parent: true}
119
+ );
120
+ assert.isFalse(credentials.canRefresh);
121
+
122
+ webex.config.credentials.refreshCallback = inBrowser && noop;
123
+ credentials = new Credentials(undefined, {parent: webex});
124
+ webex.trigger('change:config');
125
+ assert.isFalse(credentials.canRefresh);
126
+ credentials.supertoken = makeToken(webex, {
127
+ access_token: 'AT',
128
+ refresh_token: 'RT',
129
+ });
130
+ assert.isTrue(credentials.supertoken.canRefresh);
131
+ assert.isTrue(credentials.canRefresh);
132
+ });
133
+ });
134
+
135
+ describe('#buildLoginUrl()', () => {
136
+ it('requires `state` to be an object', () => {
137
+ const webex = new MockWebex({
138
+ children: {
139
+ credentials: Credentials,
140
+ },
141
+ });
142
+
143
+ webex.trigger('change:config)');
144
+ assert.doesNotThrow(() => {
145
+ webex.credentials.buildLoginUrl();
146
+ }, /if specified, `options.state` must be an object/);
147
+
148
+ assert.doesNotThrow(() => {
149
+ webex.credentials.buildLoginUrl({});
150
+ }, /if specified, `options.state` must be an object/);
151
+
152
+ assert.throws(() => {
153
+ webex.credentials.buildLoginUrl({state: 'state'});
154
+ }, /if specified, `options.state` must be an object/);
155
+
156
+ assert.doesNotThrow(() => {
157
+ webex.credentials.buildLoginUrl({state: {}});
158
+ }, /if specified, `options.state` must be an object/);
159
+ });
160
+
161
+ it('prefers the hydra auth url, but falls back to idbroker', () => {
162
+ const webex = new MockWebex();
163
+ let credentials = new Credentials(undefined, {parent: webex});
164
+
165
+ webex.trigger('change:config');
166
+
167
+ assert.match(credentials.buildLoginUrl({state: {}}), /idbroker/);
168
+ webex.config.credentials = {
169
+ authorizationString: AUTHORIZATION_STRING,
170
+ };
171
+ credentials = new Credentials({}, {parent: webex});
172
+ webex.trigger('change:config');
173
+ assert.match(credentials.buildLoginUrl({state: {}}), /api.ciscospark.com/);
174
+ });
175
+
176
+ skipInBrowser(it)('generates the login url', () => {
177
+ const webex = new MockWebex();
178
+ const credentials = new Credentials(undefined, {parent: webex});
179
+
180
+ webex.trigger('change:config');
181
+
182
+ assert.equal(
183
+ credentials.buildLoginUrl({state: {page: 'front'}}),
184
+ `${
185
+ process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
186
+ }/idb/oauth2/v1/authorize?state=eyJwYWdlIjoiZnJvbnQifQ&client_id=fake&redirect_uri=http%3A%2F%2Fexample.com&scope=scope%3Aone&response_type=code`
187
+ );
188
+ });
189
+
190
+ skipInBrowser(it)('generates the login url with empty state param', () => {
191
+ const webex = new MockWebex();
192
+ const credentials = new Credentials(undefined, {parent: webex});
193
+
194
+ webex.trigger('change:config');
195
+
196
+ assert.equal(
197
+ credentials.buildLoginUrl({state: {}}),
198
+ `${
199
+ process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
200
+ }/idb/oauth2/v1/authorize?client_id=fake&redirect_uri=http%3A%2F%2Fexample.com&scope=scope%3Aone&response_type=code`
201
+ );
202
+ });
203
+ });
204
+
205
+ describe('#buildLogoutUrl()', () => {
206
+ skipInBrowser(it)('generates the logout url', () => {
207
+ const webex = new MockWebex();
208
+
209
+ webex.config.credentials.redirect_uri = 'ru';
210
+ const credentials = new Credentials(undefined, {parent: webex});
211
+
212
+ webex.trigger('change:config');
213
+ assert.equal(
214
+ credentials.buildLogoutUrl(),
215
+ `${
216
+ process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
217
+ }/idb/oauth2/v1/logout?cisService=webex&goto=ru`
218
+ );
219
+ });
220
+
221
+ skipInBrowser(it)('includes a token param if passed', () => {
222
+ const webex = new MockWebex();
223
+
224
+ webex.config.credentials.redirect_uri = 'ru';
225
+ const credentials = new Credentials(undefined, {parent: webex});
226
+
227
+ webex.trigger('change:config');
228
+ assert.equal(
229
+ credentials.buildLogoutUrl({token: 't'}),
230
+ `${
231
+ process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
232
+ }/idb/oauth2/v1/logout?cisService=webex&goto=ru&token=t`
233
+ );
234
+ });
235
+
236
+ it('always fallsback to idbroker', () => {
237
+ const webex = new MockWebex();
238
+
239
+ webex.config.credentials.redirect_uri = 'ru';
240
+ const credentials = new Credentials(undefined, {parent: webex});
241
+
242
+ webex.trigger('change:config');
243
+ assert.match(credentials.buildLogoutUrl(), /idbroker.*?goto=ru/);
244
+ });
245
+
246
+ it('allows overriding the goto url', () => {
247
+ const webex = new MockWebex();
248
+ const credentials = new Credentials(undefined, {parent: webex});
249
+
250
+ webex.trigger('change:config');
251
+ assert.match(
252
+ credentials.buildLogoutUrl({goto: 'http://example.com/'}),
253
+ /goto=http%3A%2F%2Fexample.com%2F/
254
+ );
255
+ });
256
+ });
257
+
258
+ describe('#getOrgId()', () => {
259
+ let credentials;
260
+ let orgId;
261
+ let webex;
262
+
263
+ beforeEach(() => {
264
+ webex = new MockWebex();
265
+ credentials = new Credentials(undefined, {parent: webex});
266
+ });
267
+
268
+ it('should return the OrgId of JWT-authenticated user', () => {
269
+ credentials.supertoken = {
270
+ access_token:
271
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyZWFsbSI6Im15LXJlYWxtIn0.U16gzUsaRW1VVikJA2VeXRHPX716tG1_B42oxzy1UMk',
272
+ };
273
+
274
+ orgId = 'my-realm';
275
+
276
+ assert.equal(credentials.getOrgId(), orgId);
277
+ });
278
+
279
+ it('should return the OrgId of a user-token-authenticated user', () => {
280
+ orgId = 'this-is-an-org-id';
281
+
282
+ credentials.supertoken = {
283
+ access_token: `000_000_${orgId}`,
284
+ };
285
+
286
+ assert.equal(credentials.getOrgId(), orgId);
287
+ });
288
+
289
+ it('should throw if the OrgId was not determined', () =>
290
+ expect(() => credentials.getOrgId()).toThrow('the provided token is not a valid format'));
291
+ });
292
+
293
+ describe('#extractOrgIdFromJWT()', () => {
294
+ let credentials;
295
+ let webex;
296
+
297
+ beforeEach(() => {
298
+ webex = new MockWebex();
299
+ credentials = new Credentials(undefined, {parent: webex});
300
+ });
301
+
302
+ it('should return the OrgId of a provided JWT', () => {
303
+ const jwt =
304
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyZWFsbSI6Im15LXJlYWxtIn0.U16gzUsaRW1VVikJA2VeXRHPX716tG1_B42oxzy1UMk';
305
+ const realm = 'my-realm';
306
+
307
+ assert.equal(credentials.extractOrgIdFromJWT(jwt), realm);
308
+ });
309
+
310
+ it('should throw if the provided JWT is not valid', () =>
311
+ expect(() => credentials.extractOrgIdFromJWT('not-valid')).toThrow());
312
+
313
+ it('should throw if the provided JWT does not contain an OrgId', () => {
314
+ const jwtNoOrg =
315
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
316
+
317
+ expect(() => credentials.extractOrgIdFromJWT(jwtNoOrg)).toThrow();
318
+ });
319
+
320
+ it('should throw if no JWT was provided', () =>
321
+ expect(() => credentials.extractOrgIdFromJWT()).toThrow());
322
+ });
323
+
324
+ describe('#extractOrgIdFromUserToken()', () => {
325
+ let credentials;
326
+ let webex;
327
+
328
+ beforeEach(() => {
329
+ webex = new MockWebex();
330
+ credentials = new Credentials(undefined, {parent: webex});
331
+ });
332
+
333
+ it('should return the OrgId of the provided user token', () => {
334
+ const orgId = 'this-is-an-org-id';
335
+ const userToken = `000_000_${orgId}`;
336
+
337
+ assert.equal(credentials.extractOrgIdFromUserToken(userToken), orgId);
338
+ });
339
+
340
+ it('should throw when provided an invalid token', () =>
341
+ expect(() => credentials.extractOrgIdFromUserToken()).toThrow('the provided token is not a valid format'));
342
+
343
+ it('should throw when no token is provided', () =>
344
+ expect(() => credentials.extractOrgIdFromUserToken()).toThrow());
345
+ });
346
+
347
+ describe('#initialize()', () => {
348
+ it('turns a portal auth string into a configuration object', () => {
349
+ const webex = new MockWebex();
350
+
351
+ webex.config.credentials = {
352
+ client_id: 'ci',
353
+ redirect_uri: 'ru',
354
+ scope: 's',
355
+ };
356
+
357
+ let credentials = new Credentials(undefined, {parent: webex});
358
+
359
+ webex.trigger('change:config');
360
+ webex.trigger('change:config');
361
+ assert.equal(webex.config.credentials.client_id, 'ci');
362
+ assert.equal(credentials.config.client_id, 'ci');
363
+ assert.equal(webex.config.credentials.redirect_uri, 'ru');
364
+ assert.equal(credentials.config.redirect_uri, 'ru');
365
+ assert.equal(webex.config.credentials.scope, 's');
366
+ assert.equal(credentials.config.scope, 's');
367
+
368
+ // Accept a portal auth string via environment variables
369
+ webex.config.credentials.authorizationString = AUTHORIZATION_STRING;
370
+ credentials = new Credentials(undefined, {parent: webex});
371
+ webex.trigger('change:config');
372
+ webex.trigger('change:config');
373
+ assert.equal(webex.config.credentials.client_id, 'MOCK_CLIENT_ID');
374
+ assert.equal(credentials.config.client_id, 'MOCK_CLIENT_ID');
375
+ assert.equal(webex.config.credentials.redirect_uri, 'http://localhost:8000');
376
+ assert.equal(credentials.config.redirect_uri, 'http://localhost:8000');
377
+ assert.equal(webex.config.credentials.scope, 'spark:rooms_read spark:teams_read');
378
+ assert.equal(credentials.config.scope, 'spark:rooms_read spark:teams_read');
379
+ });
380
+
381
+ [
382
+ 'data',
383
+ 'data.access_token',
384
+ 'data.supertoken',
385
+ 'data.supertoken.access_token',
386
+ 'data.authorization',
387
+ 'data.authorization.supertoken',
388
+ 'data.authorization.supertoken.access_token',
389
+ ]
390
+ .reduce(
391
+ (acc, path) =>
392
+ acc.concat(
393
+ ['ST', 'Bearer ST'].map((str) => {
394
+ const obj = {
395
+ msg: `accepts token string "${str}" at path "${path
396
+ .split('.')
397
+ .slice(1)
398
+ .join('.')}"`,
399
+ };
400
+
401
+ set(obj, path, str);
402
+
403
+ return obj;
404
+ })
405
+ ),
406
+ []
407
+ )
408
+ .forEach(({msg, data}) => {
409
+ it(msg, () => {
410
+ const webex = new MockWebex();
411
+ const credentials = new Credentials(data, {parent: webex});
412
+
413
+ assert.isTrue(credentials.canAuthorize);
414
+ assert.equal(credentials.supertoken.access_token, 'ST');
415
+ assert.equal(credentials.supertoken.token_type, 'Bearer');
416
+ });
417
+ });
418
+
419
+ it('schedules a refreshTimer', () => {
420
+ const webex = new MockWebex();
421
+ const supertoken = makeToken(webex, {
422
+ access_token: 'ST',
423
+ refresh_token: 'RT',
424
+ expires: Date.now() + 10000,
425
+ });
426
+ const supertoken2 = makeToken(webex, {
427
+ access_token: 'ST2',
428
+ refresh_token: 'RT2',
429
+ expires: Date.now() + 20000,
430
+ });
431
+
432
+ sinon.stub(supertoken, 'refresh').returns(Promise.resolve(supertoken2));
433
+ const credentials = new Credentials(supertoken, {parent: webex});
434
+
435
+ webex.trigger('change:config');
436
+
437
+ const firstTimer = credentials.refreshTimer;
438
+
439
+ assert.isDefined(firstTimer);
440
+ assert.notCalled(supertoken.refresh);
441
+ clock.tick(10000);
442
+
443
+ return promiseTick(8)
444
+ .then(() => assert.called(supertoken.refresh))
445
+ .then(() => assert.isDefined(credentials.refreshTimer))
446
+ .then(() => assert.notEqual(credentials.refreshTimer, firstTimer));
447
+ });
448
+
449
+ it.skip('does not schedule a refreshTimer', () => {
450
+ const webex = new MockWebex();
451
+ const supertoken = makeToken(webex, {
452
+ access_token: 'ST',
453
+ refresh_token: 'RT',
454
+ expires: Date.now() - 10000,
455
+ });
456
+
457
+ sinon.stub(supertoken, 'refresh').returns(Promise.reject());
458
+ const credentials = new Credentials(supertoken, {parent: webex});
459
+
460
+ webex.trigger('change:config');
461
+
462
+ assert.isUndefined(credentials.refreshTimer);
463
+ });
464
+ });
465
+
466
+ describe('#getUserToken()', () => {
467
+ // it('resolves with the supertoken if the supertoken matches the requested scopes');
468
+
469
+ it('resolves with the token identified by the specified scopes', () => {
470
+ const webex = new MockWebex();
471
+ const credentials = new Credentials(undefined, {parent: webex});
472
+
473
+ webex.trigger('change:config');
474
+ const st = makeToken(webex, {access_token: 'ST'});
475
+ const t1 = makeToken(webex, {
476
+ access_token: 'AT1',
477
+ scope: 'scope1',
478
+ });
479
+ const t2 = makeToken(webex, {
480
+ access_token: 'AT2',
481
+ scope: 'scope2',
482
+ });
483
+
484
+ credentials.set({
485
+ supertoken: st,
486
+ userTokens: [t1, t2],
487
+ });
488
+
489
+ return Promise.all([
490
+ credentials.getUserToken('scope1').then((result) => assert.deepEqual(result, t1)),
491
+ credentials.getUserToken('scope2').then((result) => assert.deepEqual(result, t2)),
492
+ ]);
493
+ });
494
+
495
+ describe('when no matching token is found', () => {
496
+ it('downscopes the supertoken', () => {
497
+ const webex = new MockWebex();
498
+ const credentials = new Credentials(undefined, {parent: webex});
499
+
500
+ webex.trigger('change:config');
501
+
502
+ credentials.supertoken = makeToken(webex, {
503
+ access_token: 'ST',
504
+ });
505
+
506
+ const t2 = makeToken(webex, {
507
+ access_token: 'AT2',
508
+ });
509
+
510
+ sinon.stub(credentials.supertoken, 'downscope').returns(Promise.resolve(t2));
511
+
512
+ const t1 = makeToken(webex, {
513
+ access_token: 'AT1',
514
+ scope: 'scope1',
515
+ });
516
+
517
+ credentials.set({
518
+ userTokens: [t1],
519
+ });
520
+
521
+ return credentials
522
+ .getUserToken('scope2')
523
+ .then((result) => assert.deepEqual(result, t2))
524
+ .then(() => assert.calledWith(credentials.supertoken.downscope, 'scope2'));
525
+ });
526
+ });
527
+
528
+ describe('when no scope is specified', () => {
529
+ it('resolves with a token containing all but the kms scopes', () => {
530
+ const webex = new MockWebex();
531
+
532
+ webex.config.credentials.scope = 'scope1 spark:kms';
533
+ const credentials = new Credentials(undefined, {parent: webex});
534
+
535
+ webex.trigger('change:config');
536
+
537
+ credentials.supertoken = makeToken(webex, {
538
+ access_token: 'ST',
539
+ });
540
+
541
+ // const t2 = makeToken(webex, {
542
+ // access_token: `AT2`
543
+ // });
544
+
545
+ // sinon.stub(credentials.supertoken, `downscope`).returns(Promise.resolve(t2));
546
+
547
+ const t1 = makeToken(webex, {
548
+ access_token: 'AT1',
549
+ scope: 'scope1',
550
+ });
551
+
552
+ credentials.set({
553
+ userTokens: [t1],
554
+ });
555
+
556
+ return credentials.getUserToken().then((result) => assert.deepEqual(result, t1));
557
+ });
558
+ });
559
+
560
+ describe('when the kms downscope request fails', () => {
561
+ it('falls back to the supertoken', () => {
562
+ const webex = new MockWebex({
563
+ children: {
564
+ logger: Logger,
565
+ },
566
+ });
567
+
568
+ webex.config.credentials.scope = 'scope1 spark:kms';
569
+ const credentials = new Credentials(undefined, {parent: webex});
570
+
571
+ webex.trigger('change:config');
572
+
573
+ credentials.supertoken = makeToken(webex, {
574
+ access_token: 'ST',
575
+ });
576
+
577
+ sinon
578
+ .stub(credentials.supertoken, 'downscope')
579
+ .returns(Promise.reject(new Error('downscope failed')));
580
+
581
+ const t1 = makeToken(webex, {
582
+ access_token: 'AT1',
583
+ scope: 'scope1',
584
+ });
585
+
586
+ credentials.set({
587
+ userTokens: [t1],
588
+ });
589
+
590
+ return credentials
591
+ .getUserToken('scope2')
592
+ .then((t) => assert.equal(t.access_token, credentials.supertoken.access_token));
593
+ });
594
+ });
595
+
596
+ it('is blocked while a token refresh is inflight', () => {
597
+ const webex = new MockWebex();
598
+
599
+ webex.config.credentials.scope = 'scope1 spark:kms';
600
+ const credentials = new Credentials(undefined, {parent: webex});
601
+
602
+ webex.trigger('change:config');
603
+
604
+ const supertoken1 = makeToken(webex, {
605
+ access_token: 'ST1',
606
+ refresh_token: 'RT1',
607
+ });
608
+
609
+ credentials.set({supertoken: supertoken1});
610
+
611
+ sinon
612
+ .stub(supertoken1, 'downscope')
613
+ .returns(Promise.resolve(new Token({access_token: 'ST1ATD'})));
614
+ const supertoken2 = makeToken(webex, {
615
+ access_token: 'ST2',
616
+ });
617
+
618
+ sinon.stub(supertoken1, 'refresh').returns(Promise.resolve(supertoken2));
619
+
620
+ const at2 = makeToken(webex, {access_token: 'ST2ATD'});
621
+
622
+ sinon.stub(supertoken2, 'downscope').returns(Promise.resolve(at2));
623
+
624
+ return Promise.all([
625
+ credentials.refresh(),
626
+ credentials.getUserToken('scope2').then((result) => assert.deepEqual(result, at2)),
627
+ ]);
628
+ });
629
+ });
630
+
631
+ describe('#invalidate()', () => {
632
+ it('clears the refreshTimer', () => {
633
+ const webex = new MockWebex();
634
+ const credentials = new Credentials(undefined, {parent: webex});
635
+
636
+ webex.trigger('change:config');
637
+ const st = makeToken(webex, {
638
+ access_token: 'ST',
639
+ refresh_token: 'RT',
640
+ });
641
+
642
+ const st2 = makeToken(webex, {
643
+ access_token: 'ST2',
644
+ refresh_token: 'RT2',
645
+ });
646
+
647
+ credentials.set({
648
+ supertoken: st,
649
+ });
650
+
651
+ sinon.stub(credentials, 'refresh').returns(Promise.resolve(st2));
652
+
653
+ credentials.scheduleRefresh(Date.now() + 10000);
654
+ assert.isDefined(credentials.refreshTimer);
655
+ assert.notCalled(credentials.refresh);
656
+
657
+ return credentials.invalidate().then(() => {
658
+ clock.tick(10000);
659
+ assert.isUndefined(credentials.refreshTimer);
660
+ assert.notCalled(credentials.refresh);
661
+ });
662
+ });
663
+
664
+ it('clears the tokens from boundedStorage', () => {
665
+ const webex = new MockWebex();
666
+ const credentials = new Credentials(undefined, {parent: webex});
667
+
668
+ webex.trigger('change:config');
669
+ const st = makeToken(webex, {
670
+ access_token: 'ST',
671
+ });
672
+
673
+ const t1 = makeToken(webex, {
674
+ access_token: 'AT1',
675
+ scope: 'scope1',
676
+ });
677
+
678
+ const t2 = makeToken(webex, {
679
+ access_token: 'AT2',
680
+ scope: 'scope2',
681
+ });
682
+
683
+ credentials.set({
684
+ supertoken: st,
685
+ userTokens: [t1, t2],
686
+ });
687
+
688
+ return new Promise((resolve) => {
689
+ setTimeout(resolve, 1);
690
+ clock.tick(1000);
691
+ })
692
+ .then(() => webex.boundedStorage.get('Credentials', '@'))
693
+ .then((data) => {
694
+ assert.equal(data.userTokens[0].access_token, t1.access_token);
695
+ assert.equal(data.userTokens[1].access_token, t2.access_token);
696
+
697
+ return credentials.invalidate();
698
+ })
699
+ .then(() => promiseTick(500))
700
+ .then(
701
+ () =>
702
+ new Promise((resolve) => {
703
+ setTimeout(resolve, 1);
704
+ clock.tick(1000);
705
+ })
706
+ )
707
+ .then(() => promiseTick(500))
708
+ .then(
709
+ () =>
710
+ new Promise((resolve) => {
711
+ setTimeout(resolve, 1);
712
+ clock.tick(1000);
713
+ })
714
+ )
715
+ .then(() => assert.isRejected(webex.boundedStorage.get('Credentials', '@'), /NotFound/));
716
+ });
717
+
718
+
719
+ // it('does not induce any token refreshes');
720
+
721
+ it('prevents #getUserToken() from being invoked', () => {
722
+ const webex = new MockWebex();
723
+ const credentials = new Credentials(undefined, {parent: webex});
724
+
725
+ webex.trigger('change:config');
726
+ const st = makeToken(webex, {
727
+ access_token: 'ST',
728
+ refresh_token: 'RT',
729
+ });
730
+
731
+ const t1 = makeToken(webex, {
732
+ access_token: 'AT1',
733
+ scope: 'scope1',
734
+ });
735
+
736
+ credentials.set({
737
+ supertoken: st,
738
+ userTokens: [t1],
739
+ });
740
+
741
+ return credentials
742
+ .invalidate()
743
+ .then(() =>
744
+ assert.isRejected(
745
+ credentials.getUserToken(),
746
+ /Current state cannot produce an access token/
747
+ )
748
+ );
749
+ });
750
+ });
751
+
752
+ describe('#refresh()', () => {
753
+ it('refreshes and downscopes the supertoken, and revokes previous tokens', () => {
754
+ const webex = new MockWebex();
755
+ const credentials = new Credentials(undefined, {parent: webex});
756
+
757
+ webex.trigger('change:config');
758
+ const st = makeToken(webex, {
759
+ access_token: 'ST',
760
+ refresh_token: 'RT',
761
+ });
762
+
763
+ const st2 = makeToken(webex, {
764
+ access_token: 'ST2',
765
+ refresh_token: 'RT2',
766
+ });
767
+
768
+ const t1 = makeToken(webex, {
769
+ access_token: 'AT1',
770
+ scope: 'scope1',
771
+ });
772
+
773
+ const t2 = makeToken(webex, {
774
+ access_token: 'AT2',
775
+ scope: 'scope2',
776
+ });
777
+
778
+ sinon.stub(st2, 'downscope').returns(Promise.resolve(t2));
779
+ sinon.stub(st, 'refresh').returns(Promise.resolve(st2));
780
+ sinon.stub(t1, 'revoke').returns(Promise.resolve());
781
+ sinon.spy(credentials, 'scheduleRefresh');
782
+
783
+ credentials.set({
784
+ supertoken: st,
785
+ userTokens: [t1],
786
+ });
787
+
788
+ assert.equal(credentials.userTokens.get(t1.scope), t1);
789
+
790
+ return credentials
791
+ .refresh()
792
+ .then(() => assert.called(st.refresh))
793
+ .then(() => assert.calledWith(st2.downscope, 'scope1'))
794
+ .then(() => assert.called(t1.revoke))
795
+ .then(() => assert.isUndefined(credentials.userTokens.get(t1.scope)))
796
+ .then(() => assert.equal(credentials.userTokens.get(t2.scope), t2))
797
+ .then(() => assert.calledWith(credentials.scheduleRefresh, st.expires));
798
+ });
799
+
800
+ it('refreshes and downscopes the supertoken even if revocation of previous token fails', () => {
801
+ const webex = new MockWebex();
802
+ const credentials = new Credentials(undefined, {parent: webex});
803
+
804
+ webex.trigger('change:config');
805
+ const st = makeToken(webex, {
806
+ access_token: 'ST',
807
+ refresh_token: 'RT',
808
+ });
809
+
810
+ const st2 = makeToken(webex, {
811
+ access_token: 'ST2',
812
+ refresh_token: 'RT2',
813
+ });
814
+
815
+ const t1 = makeToken(webex, {
816
+ access_token: 'AT1',
817
+ scope: 'scope1',
818
+ });
819
+
820
+ const t2 = makeToken(webex, {
821
+ access_token: 'AT2',
822
+ scope: 'scope2',
823
+ });
824
+
825
+ sinon.stub(st2, 'downscope').returns(Promise.resolve(t2));
826
+ sinon.stub(st, 'refresh').returns(Promise.resolve(st2));
827
+ sinon.stub(t1, 'revoke').returns(Promise.reject());
828
+ sinon.spy(credentials, 'scheduleRefresh');
829
+
830
+ credentials.set({
831
+ supertoken: st,
832
+ userTokens: [t1],
833
+ });
834
+
835
+ assert.equal(credentials.userTokens.get(t1.scope), t1);
836
+
837
+ return credentials
838
+ .refresh()
839
+ .then(() => assert.called(st.refresh))
840
+ .then(() => assert.calledWith(st2.downscope, 'scope1'))
841
+ .then(() => assert.called(t1.revoke))
842
+ .then(() => assert.isUndefined(credentials.userTokens.get(t1.scope)))
843
+ .then(() => assert.equal(credentials.userTokens.get(t2.scope), t2))
844
+ .then(() => assert.calledWith(credentials.scheduleRefresh, st.expires));
845
+ });
846
+
847
+ it('removes and revokes all child tokens', () => {
848
+ const webex = new MockWebex({
849
+ children: {
850
+ logger: Logger,
851
+ },
852
+ });
853
+ const credentials = new Credentials(undefined, {parent: webex});
854
+
855
+ webex.trigger('change:config');
856
+ const st = makeToken(webex, {
857
+ access_token: 'ST',
858
+ refresh_token: 'RT',
859
+ });
860
+
861
+ sinon.stub(st, 'refresh').returns(Promise.resolve(makeToken(webex, {access_token: 'ST2'})));
862
+
863
+ const t1 = makeToken(webex, {
864
+ access_token: 'AT1',
865
+ scope: 'scope1',
866
+ });
867
+
868
+ credentials.set({
869
+ supertoken: st,
870
+ userTokens: [t1],
871
+ });
872
+
873
+ return credentials.refresh().then(() => assert.called(st.refresh));
874
+ });
875
+
876
+ it('allows #getUserToken() to be revoked, but #getUserToken() promises will not resolve until the suport token has been refreshed', () => {
877
+ const webex = new MockWebex();
878
+ const credentials = new Credentials(undefined, {parent: webex});
879
+
880
+ webex.trigger('change:config');
881
+ const st1 = makeToken(webex, {
882
+ access_token: 'ST1',
883
+ refresh_token: 'RT1',
884
+ });
885
+
886
+ const st2 = makeToken(webex, {
887
+ access_token: 'ST2',
888
+ refresh_token: 'RT1',
889
+ });
890
+
891
+ const t1 = makeToken(webex, {
892
+ access_token: 'AT1',
893
+ scope: 'scope1',
894
+ });
895
+
896
+ const t2 = makeToken(webex, {
897
+ access_token: 'AT2',
898
+ scope: 'scope1',
899
+ });
900
+
901
+ sinon.stub(st1, 'refresh').returns(Promise.resolve(st2));
902
+ sinon.stub(st2, 'downscope').returns(Promise.resolve(t2));
903
+
904
+ credentials.set({
905
+ supertoken: st1,
906
+ userTokens: [t1],
907
+ });
908
+
909
+ credentials.refresh();
910
+
911
+ return credentials.getUserToken('scope1').then((result) => assert.deepEqual(result, t2));
912
+ });
913
+
914
+ it('emits InvalidRequestError when the refresh token and access token expire', () => {
915
+ const webex = new MockWebex();
916
+ const credentials = new Credentials(undefined, {parent: webex});
917
+
918
+ webex.trigger('change:config');
919
+ const st = makeToken(webex, {
920
+ access_token: 'ST',
921
+ refresh_token: 'RT',
922
+ });
923
+
924
+ const t1 = makeToken(webex, {
925
+ access_token: 'AT1',
926
+ scope: 'scope1',
927
+ });
928
+
929
+ const res = {
930
+ body: {
931
+ error: 'invalid_request',
932
+ error_description:
933
+ 'The refresh token provided is expired, revoked, malformed, or invalid.',
934
+ trackingID: 'test123',
935
+ },
936
+ };
937
+
938
+ const ErrorConstructor = grantErrors.select(res.body.error);
939
+
940
+ sinon
941
+ .stub(st, 'refresh')
942
+ .returns(Promise.reject(new ErrorConstructor('InvalidRequestError')));
943
+ sinon.stub(credentials, 'unset').returns(Promise.resolve());
944
+ const triggerSpy = sinon.spy(webex, 'trigger');
945
+
946
+ credentials.set({
947
+ supertoken: st,
948
+ userTokens: [t1],
949
+ });
950
+
951
+ return credentials
952
+ .refresh()
953
+ .then(() => assert.called(st.refresh))
954
+ .catch(() => {
955
+ assert.called(credentials.unset);
956
+ assert.calledWith(triggerSpy, sinon.match('client:InvalidRequestError'));
957
+ });
958
+ });
959
+ });
960
+
961
+ describe('#scheduleRefresh()', () => {
962
+ it('refreshes token immediately if token is expired', () => {
963
+ const webex = new MockWebex();
964
+ const credentials = new Credentials(undefined, {parent: webex});
965
+
966
+ webex.trigger('change:config');
967
+ const st = makeToken(webex, {
968
+ access_token: 'ST',
969
+ refresh_token: 'RT',
970
+ });
971
+
972
+ const st2 = makeToken(webex, {
973
+ access_token: 'ST2',
974
+ refresh_token: 'RT2',
975
+ });
976
+
977
+ credentials.set({
978
+ supertoken: st,
979
+ });
980
+
981
+ sinon.stub(credentials, 'refresh').returns(Promise.resolve(st2));
982
+
983
+ credentials.scheduleRefresh(Date.now() - 10000);
984
+ assert.isUndefined(credentials.refreshTimer);
985
+ assert.called(credentials.refresh);
986
+ });
987
+
988
+ it('schedules a token refresh', () => {
989
+ const webex = new MockWebex();
990
+ const credentials = new Credentials(undefined, {parent: webex});
991
+
992
+ webex.trigger('change:config');
993
+ const st = makeToken(webex, {
994
+ access_token: 'ST',
995
+ refresh_token: 'RT',
996
+ });
997
+
998
+ const st2 = makeToken(webex, {
999
+ access_token: 'ST2',
1000
+ refresh_token: 'RT2',
1001
+ });
1002
+
1003
+ credentials.set({
1004
+ supertoken: st,
1005
+ });
1006
+
1007
+ sinon.stub(credentials, 'refresh').returns(Promise.resolve(st2));
1008
+
1009
+ credentials.scheduleRefresh(Date.now() + 10000);
1010
+ assert.isDefined(credentials.refreshTimer);
1011
+ assert.notCalled(credentials.refresh);
1012
+ clock.tick(10000);
1013
+ assert.called(credentials.refresh);
1014
+ });
1015
+ });
1016
+ });
1017
+ });