@webex/webex-core 3.0.0-beta.34 → 3.0.0-beta.341

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.
@@ -0,0 +1,6 @@
1
+ // Metric to do with WDM registration
2
+ export const METRICS = {
3
+ JS_SDK_CREDENTIALS_DOWNSCOPE_FAILED: 'JS_SDK_CREDENTIALS_DOWNSCOPE_FAILED',
4
+ JS_SDK_CREDENTIALS_TOKEN_REFRESH_SCOPE_MISMATCH:
5
+ 'JS_SDK_CREDENTIALS_TOKEN_REFRESH_SCOPE_MISMATCH',
6
+ };
@@ -13,10 +13,11 @@ import {clone, cloneDeep, isObject, isEmpty} from 'lodash';
13
13
  import WebexPlugin from '../webex-plugin';
14
14
  import {persist, waitForValue} from '../storage/decorators';
15
15
 
16
- import grantErrors from './grant-errors';
17
- import {filterScope, sortScope} from './scope';
16
+ import grantErrors, {OAuthError} from './grant-errors';
17
+ import {filterScope, diffScopes, sortScope} from './scope';
18
18
  import Token from './token';
19
19
  import TokenCollection from './token-collection';
20
+ import {METRICS} from '../constants';
20
21
 
21
22
  /**
22
23
  * @class
@@ -48,6 +49,25 @@ const Credentials = WebexPlugin.extend({
48
49
  return Boolean(this.supertoken && this.supertoken.canRefresh);
49
50
  },
50
51
  },
52
+ isUnverifiedGuest: {
53
+ deps: ['supertoken'],
54
+ /**
55
+ * Returns true if the user is an unverified guest
56
+ * @returns {boolean}
57
+ */
58
+ fn() {
59
+ let isGuest = false;
60
+ try {
61
+ isGuest =
62
+ JSON.parse(base64.decode(this.supertoken.access_token.split('.')[1])).user_type ===
63
+ 'guest';
64
+ } catch {
65
+ /* the non-guest token is formatted differently so catch is expected */
66
+ }
67
+
68
+ return isGuest;
69
+ },
70
+ },
51
71
  },
52
72
 
53
73
  props: {
@@ -240,8 +260,15 @@ const Credentials = WebexPlugin.extend({
240
260
  */
241
261
  downscope(scope) {
242
262
  return this.supertoken.downscope(scope).catch((reason) => {
243
- this.logger.trace(`credentials: failed to downscope supertoken to ${scope}`, reason);
263
+ const failReason = reason?.body ?? reason;
264
+ this.logger.warn(`credentials: failed to downscope supertoken to "${scope}"`, failReason);
244
265
  this.logger.trace(`credentials: falling back to supertoken for ${scope}`);
266
+ this.webex.internal.metrics.submitClientMetrics(METRICS.JS_SDK_CREDENTIALS_DOWNSCOPE_FAILED, {
267
+ fields: {
268
+ requestedScope: scope,
269
+ failReason,
270
+ },
271
+ });
245
272
 
246
273
  return Promise.resolve(new Token({scope, ...this.supertoken.serialize()}), {
247
274
  parent: this,
@@ -322,12 +349,12 @@ const Credentials = WebexPlugin.extend({
322
349
  }
323
350
 
324
351
  if (!scope) {
325
- scope = filterScope('spark:kms', this.config.scope);
352
+ scope = filterScope('spark:kms', this.supertoken.scope);
326
353
  }
327
354
 
328
355
  scope = sortScope(scope);
329
356
 
330
- if (scope === sortScope(this.config.scope)) {
357
+ if (scope === sortScope(this.supertoken.scope)) {
331
358
  return Promise.resolve(this.supertoken);
332
359
  }
333
360
 
@@ -477,42 +504,10 @@ const Credentials = WebexPlugin.extend({
477
504
 
478
505
  return supertoken
479
506
  .refresh()
480
- .then((st) => {
481
- // clear refresh timer
482
- if (this.refreshTimer) {
483
- clearTimeout(this.refreshTimer);
484
- this.unset('refreshTimer');
485
- }
486
- this.supertoken = st;
487
-
488
- return Promise.all(
489
- tokens.map((token) =>
490
- this.downscope(token.scope)
491
- // eslint-disable-next-line max-nested-callbacks
492
- .then((t) => {
493
- this.logger.info(`credentials: revoking token for ${token.scope}`);
494
-
495
- return token
496
- .revoke()
497
- .catch((err) => {
498
- this.logger.warn('credentials: failed to revoke user token', err);
499
- })
500
- .then(() => {
501
- this.userTokens.remove(token.scope);
502
- this.userTokens.add(t);
503
- });
504
- })
505
- )
506
- );
507
- })
508
- .then(() => {
509
- this.scheduleRefresh(this.supertoken.expires);
510
- })
511
507
  .catch((error) => {
512
- const {InvalidRequestError} = grantErrors;
513
-
514
- if (error instanceof InvalidRequestError) {
515
- // Error: The refresh token provided is expired, revoked, malformed, or invalid. Hence emit an event to the client, an opportunity to logout.
508
+ if (error instanceof OAuthError) {
509
+ // Error: super token refresh failed with 400 status code.
510
+ // Hence emit an event to the client, an opportunity to logout.
516
511
  this.unset('supertoken');
517
512
  while (this.userTokens.models.length) {
518
513
  try {
@@ -525,6 +520,53 @@ const Credentials = WebexPlugin.extend({
525
520
  }
526
521
 
527
522
  return Promise.reject(error);
523
+ })
524
+ .then((st) => {
525
+ // clear refresh timer
526
+ if (this.refreshTimer) {
527
+ clearTimeout(this.refreshTimer);
528
+ this.unset('refreshTimer');
529
+ }
530
+ this.supertoken = st;
531
+
532
+ const invalidScopes = diffScopes(this.config.scope, st.scope);
533
+
534
+ if (invalidScopes !== '') {
535
+ this.logger.warn(
536
+ `credentials: "${invalidScopes}" scope(s) are invalid because not listed in the supertoken, they will be excluded from user token requests.`
537
+ );
538
+ this.webex.internal.metrics.submitClientMetrics(
539
+ METRICS.JS_SDK_CREDENTIALS_TOKEN_REFRESH_SCOPE_MISMATCH,
540
+ {fields: {invalidScopes}}
541
+ );
542
+ }
543
+
544
+ return Promise.all(
545
+ tokens.map((token) => {
546
+ const tokenScope = filterScope(diffScopes(token.scope, st.scope), token.scope);
547
+
548
+ return (
549
+ this.downscope(tokenScope)
550
+ // eslint-disable-next-line max-nested-callbacks
551
+ .then((t) => {
552
+ this.logger.info(`credentials: revoking token for ${token.scope}`);
553
+
554
+ return token
555
+ .revoke()
556
+ .catch((err) => {
557
+ this.logger.warn('credentials: failed to revoke user token', err);
558
+ })
559
+ .then(() => {
560
+ this.userTokens.remove(token.scope);
561
+ this.userTokens.add(t);
562
+ });
563
+ })
564
+ );
565
+ })
566
+ );
567
+ })
568
+ .then(() => {
569
+ this.scheduleRefresh(this.supertoken.expires);
528
570
  });
529
571
  },
530
572
 
@@ -2,6 +2,8 @@
2
2
  * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
3
  */
4
4
 
5
+ import {difference} from 'lodash';
6
+
5
7
  /**
6
8
  * sorts a list of scopes
7
9
  * @param {string} scope
@@ -17,7 +19,7 @@ export function sortScope(scope) {
17
19
 
18
20
  /**
19
21
  * sorts a list of scopes and filters the specified scope
20
- * @param {string} toFilter
22
+ * @param {string|string[]} toFilter
21
23
  * @param {string} scope
22
24
  * @returns {string}
23
25
  */
@@ -25,10 +27,25 @@ export function filterScope(toFilter, scope) {
25
27
  if (!scope) {
26
28
  return '';
27
29
  }
30
+ const toFilterArr = Array.isArray(toFilter) ? toFilter : [toFilter];
28
31
 
29
32
  return scope
30
33
  .split(' ')
31
- .filter((item) => item !== toFilter)
34
+ .filter((item) => !toFilterArr.includes(item))
32
35
  .sort()
33
36
  .join(' ');
34
37
  }
38
+
39
+ /**
40
+ * Returns a string containing all items in scopeA that are not in scopeB, or an empty string if there are none.
41
+ *
42
+ * @param {string} scopeA
43
+ * @param {string} scopeB
44
+ * @returns {string}
45
+ */
46
+ export function diffScopes(scopeA, scopeB) {
47
+ const a = scopeA?.split(' ') ?? [];
48
+ const b = scopeB?.split(' ') ?? [];
49
+
50
+ return difference(a, b).sort().join(' ');
51
+ }
@@ -36,11 +36,11 @@ export default class ServiceInterceptor extends Interceptor {
36
36
 
37
37
  // Destructure commonly referenced namespaces.
38
38
  const {services} = this.webex.internal;
39
- const {service, resource} = options;
39
+ const {service, resource, waitForServiceTimeout} = options;
40
40
 
41
41
  // Attempt to collect the service url.
42
42
  return services
43
- .waitForService({name: service})
43
+ .waitForService({name: service, timeout: waitForServiceTimeout})
44
44
  .then((serviceUrl) => {
45
45
  // Generate the combined service url and resource.
46
46
  options.uri = this.generateUri(serviceUrl, resource);
@@ -413,6 +413,8 @@ const ServiceCatalog = AmpState.extend({
413
413
  resolve();
414
414
  }
415
415
 
416
+ const validatedTimeout = typeof timeout === 'number' && timeout >= 0 ? timeout : 60;
417
+
416
418
  const timeoutTimer = setTimeout(
417
419
  () =>
418
420
  reject(
@@ -420,7 +422,7 @@ const ServiceCatalog = AmpState.extend({
420
422
  `services: timeout occured while waiting for '${serviceGroup}' catalog to populate`
421
423
  )
422
424
  ),
423
- timeout ? timeout * 1000 : 60000
425
+ validatedTimeout * 1000
424
426
  );
425
427
 
426
428
  this.once(serviceGroup, () => {
@@ -49,6 +49,8 @@ const Services = WebexPlugin.extend({
49
49
 
50
50
  _serviceUrls: null,
51
51
 
52
+ _hostCatalog: null,
53
+
52
54
  /**
53
55
  * Get the registry associated with this webex instance.
54
56
  *
@@ -157,6 +159,15 @@ const Services = WebexPlugin.extend({
157
159
  this._serviceUrls = {...this._serviceUrls, ...serviceUrls};
158
160
  },
159
161
 
162
+ /**
163
+ * saves the hostCatalog object
164
+ * @param {Object} hostCatalog
165
+ * @returns {void}
166
+ */
167
+ _updateHostCatalog(hostCatalog) {
168
+ this._hostCatalog = {...this._hostCatalog, ...hostCatalog};
169
+ },
170
+
160
171
  /**
161
172
  * Update a list of `serviceUrls` to the most current
162
173
  * catalog via the defined `discoveryUrl` then returns the current
@@ -720,6 +731,7 @@ const Services = WebexPlugin.extend({
720
731
  // update all the service urls in the host catalog
721
732
 
722
733
  this._updateServiceUrls(serviceHostmap.serviceLinks);
734
+ this._updateHostCatalog(serviceHostmap.hostCatalog);
723
735
 
724
736
  return formattedHostmap;
725
737
  },
package/src/webex-core.js CHANGED
@@ -6,7 +6,12 @@ import {EventEmitter} from 'events';
6
6
  import util from 'util';
7
7
 
8
8
  import {proxyEvents, retry, transferEvents} from '@webex/common';
9
- import {HttpStatusInterceptor, defaults as requestDefaults} from '@webex/http-core';
9
+ import {
10
+ HttpStatusInterceptor,
11
+ defaults as requestDefaults,
12
+ protoprepareFetchOptions as prepareFetchOptions,
13
+ setTimingsAndFetch as _setTimingsAndFetch,
14
+ } from '@webex/http-core';
10
15
  import {defaultsDeep, get, isFunction, isString, last, merge, omit, set, unset} from 'lodash';
11
16
  import AmpState from 'ampersand-state';
12
17
  import uuid from 'uuid';
@@ -402,6 +407,13 @@ const WebexCore = AmpState.extend({
402
407
  interceptors: ints,
403
408
  });
404
409
 
410
+ this.prepareFetchOptions = prepareFetchOptions({
411
+ json: true,
412
+ interceptors: ints,
413
+ });
414
+
415
+ this.setTimingsAndFetch = _setTimingsAndFetch;
416
+
405
417
  let sessionId = `${get(this, 'config.trackingIdPrefix', 'webex-js-sdk')}_${get(
406
418
  this,
407
419
  'config.trackingIdBase',
@@ -11,6 +11,7 @@ import {inBrowser} from '@webex/common';
11
11
  import FakeTimers from '@sinonjs/fake-timers';
12
12
  import {skipInBrowser} from '@webex/test-helper-mocha';
13
13
  import Logger from '@webex/plugin-logger';
14
+ import Metrics, {config} from '@webex/internal-plugin-metrics';
14
15
 
15
16
  /* eslint camelcase: [0] */
16
17
 
@@ -59,6 +60,34 @@ describe('webex-core', () => {
59
60
  });
60
61
  });
61
62
 
63
+ describe('#isUnverifiedGuest', () => {
64
+ let credentials;
65
+ let webex;
66
+ beforeEach('generate the webex instance', () => {
67
+ webex = new MockWebex();
68
+ credentials = new Credentials(undefined, {parent: webex});
69
+ });
70
+
71
+ it('should have #isUnverifiedGuest', () => {
72
+ assert.exists(credentials.isUnverifiedGuest);
73
+ });
74
+
75
+ it('should get the user status and return as a boolean', () => {
76
+ credentials.set('supertoken', 'AT');
77
+ assert.isFalse(credentials.isUnverifiedGuest);
78
+ });
79
+
80
+ it('should get guest user ', () => {
81
+ credentials.set('supertoken', 'eyJhbGciOiJSUzI1NiJ9.eyJ1c2VyX3R5cGUiOiJndWVzdCJ9');
82
+ assert.isTrue(credentials.isUnverifiedGuest);
83
+ });
84
+
85
+ it('should get login user ', () => {
86
+ credentials.set('supertoken', 'dGhpc2lzbm90YXJlYWx1c2VydG9rZW4=');
87
+ assert.isFalse(credentials.isUnverifiedGuest);
88
+ });
89
+ });
90
+
62
91
  describe('#canAuthorize', () => {
63
92
  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
93
  const webex = new MockWebex();
@@ -417,7 +446,11 @@ describe('webex-core', () => {
417
446
  });
418
447
 
419
448
  it('schedules a refreshTimer', () => {
420
- const webex = new MockWebex();
449
+ const webex = new MockWebex({
450
+ children: {
451
+ metrics: Metrics,
452
+ },
453
+ });
421
454
  const supertoken = makeToken(webex, {
422
455
  access_token: 'ST',
423
456
  refresh_token: 'RT',
@@ -430,6 +463,7 @@ describe('webex-core', () => {
430
463
  });
431
464
 
432
465
  sinon.stub(supertoken, 'refresh').returns(Promise.resolve(supertoken2));
466
+ sinon.stub(webex.internal.metrics, 'submitClientMetrics').callsFake(() => {});
433
467
  const credentials = new Credentials(supertoken, {parent: webex});
434
468
 
435
469
  webex.trigger('change:config');
@@ -464,7 +498,19 @@ describe('webex-core', () => {
464
498
  });
465
499
 
466
500
  describe('#getUserToken()', () => {
467
- it('resolves with the supertoken if the supertoken matches the requested scopes');
501
+ it('resolves with the supertoken if the supertoken matches the requested scopes', () => {
502
+ const webex = new MockWebex();
503
+ const credentials = new Credentials(undefined, {parent: webex});
504
+
505
+ webex.trigger('change:config');
506
+ const st = makeToken(webex, {access_token: 'ST', scope: 'scope1'});
507
+
508
+ credentials.set({
509
+ supertoken: st,
510
+ });
511
+
512
+ return credentials.getUserToken('scope1').then((result) => assert.deepEqual(result, st));
513
+ });
468
514
 
469
515
  it('resolves with the token identified by the specified scopes', () => {
470
516
  const webex = new MockWebex();
@@ -492,6 +538,25 @@ describe('webex-core', () => {
492
538
  ]);
493
539
  });
494
540
 
541
+ it('uses the supertoken.scope instead of the config.scope for downscope', () => {
542
+ const webex = new MockWebex();
543
+ const credentials = new Credentials(undefined, {parent: webex});
544
+
545
+ webex.trigger('change:config');
546
+ const st = makeToken(webex, {access_token: 'ST', scope: 'scope1 spark:kms'});
547
+
548
+ credentials.set({
549
+ supertoken: st,
550
+ scope: 'invalidScope scope1',
551
+ });
552
+
553
+ sinon.stub(credentials, 'downscope').returns(Promise.resolve());
554
+
555
+ return credentials.getUserToken().then(() => {
556
+ assert.calledWith(credentials.downscope, 'scope1');
557
+ });
558
+ });
559
+
495
560
  describe('when no matching token is found', () => {
496
561
  it('downscopes the supertoken', () => {
497
562
  const webex = new MockWebex();
@@ -529,13 +594,13 @@ describe('webex-core', () => {
529
594
  it('resolves with a token containing all but the kms scopes', () => {
530
595
  const webex = new MockWebex();
531
596
 
532
- webex.config.credentials.scope = 'scope1 spark:kms';
533
597
  const credentials = new Credentials(undefined, {parent: webex});
534
598
 
535
599
  webex.trigger('change:config');
536
600
 
537
601
  credentials.supertoken = makeToken(webex, {
538
602
  access_token: 'ST',
603
+ scope: 'scope1 spark:kms',
539
604
  });
540
605
 
541
606
  // const t2 = makeToken(webex, {
@@ -562,9 +627,11 @@ describe('webex-core', () => {
562
627
  const webex = new MockWebex({
563
628
  children: {
564
629
  logger: Logger,
630
+ metrics: Metrics,
565
631
  },
566
632
  });
567
633
 
634
+ webex.config.metrics = config.metrics;
568
635
  webex.config.credentials.scope = 'scope1 spark:kms';
569
636
  const credentials = new Credentials(undefined, {parent: webex});
570
637
 
@@ -574,9 +641,11 @@ describe('webex-core', () => {
574
641
  access_token: 'ST',
575
642
  });
576
643
 
577
- sinon
578
- .stub(credentials.supertoken, 'downscope')
579
- .returns(Promise.reject(new Error('downscope failed')));
644
+ const failReason = 'downscope failed';
645
+ sinon.stub(credentials.supertoken, 'downscope').returns(Promise.reject(failReason));
646
+
647
+ sinon.stub(credentials.logger, 'warn').callsFake(() => {});
648
+ sinon.stub(webex.internal.metrics, 'submitClientMetrics').callsFake(() => {});
580
649
 
581
650
  const t1 = makeToken(webex, {
582
651
  access_token: 'AT1',
@@ -587,14 +656,27 @@ describe('webex-core', () => {
587
656
  userTokens: [t1],
588
657
  });
589
658
 
590
- return credentials
591
- .getUserToken('scope2')
592
- .then((t) => assert.equal(t.access_token, credentials.supertoken.access_token));
659
+ return credentials.getUserToken('scope2').then((t) => {
660
+ assert.equal(t.access_token, credentials.supertoken.access_token);
661
+ assert.calledWith(
662
+ credentials.logger.warn,
663
+ 'credentials: failed to downscope supertoken to "scope2"'
664
+ );
665
+ assert.calledWith(
666
+ webex.internal.metrics.submitClientMetrics,
667
+ 'JS_SDK_CREDENTIALS_DOWNSCOPE_FAILED',
668
+ {fields: {failReason, requestedScope: 'scope2'}}
669
+ );
670
+ });
593
671
  });
594
672
  });
595
673
 
596
674
  it('is blocked while a token refresh is inflight', () => {
597
- const webex = new MockWebex();
675
+ const webex = new MockWebex({
676
+ children: {
677
+ metrics: Metrics,
678
+ },
679
+ });
598
680
 
599
681
  webex.config.credentials.scope = 'scope1 spark:kms';
600
682
  const credentials = new Credentials(undefined, {parent: webex});
@@ -620,6 +702,7 @@ describe('webex-core', () => {
620
702
  const at2 = makeToken(webex, {access_token: 'ST2ATD'});
621
703
 
622
704
  sinon.stub(supertoken2, 'downscope').returns(Promise.resolve(at2));
705
+ sinon.stub(webex.internal.metrics, 'submitClientMetrics').callsFake(() => {});
623
706
 
624
707
  return Promise.all([
625
708
  credentials.refresh(),
@@ -750,18 +833,24 @@ describe('webex-core', () => {
750
833
 
751
834
  describe('#refresh()', () => {
752
835
  it('refreshes and downscopes the supertoken, and revokes previous tokens', () => {
753
- const webex = new MockWebex();
836
+ const webex = new MockWebex({
837
+ children: {
838
+ metrics: Metrics,
839
+ },
840
+ });
754
841
  const credentials = new Credentials(undefined, {parent: webex});
755
842
 
756
843
  webex.trigger('change:config');
757
844
  const st = makeToken(webex, {
758
845
  access_token: 'ST',
759
846
  refresh_token: 'RT',
847
+ scope: 'scope1 scope2',
760
848
  });
761
849
 
762
850
  const st2 = makeToken(webex, {
763
851
  access_token: 'ST2',
764
852
  refresh_token: 'RT2',
853
+ scope: 'scope1 scope2',
765
854
  });
766
855
 
767
856
  const t1 = makeToken(webex, {
@@ -778,6 +867,7 @@ describe('webex-core', () => {
778
867
  sinon.stub(st, 'refresh').returns(Promise.resolve(st2));
779
868
  sinon.stub(t1, 'revoke').returns(Promise.resolve());
780
869
  sinon.spy(credentials, 'scheduleRefresh');
870
+ sinon.stub(webex.internal.metrics, 'submitClientMetrics').callsFake(() => {});
781
871
 
782
872
  credentials.set({
783
873
  supertoken: st,
@@ -797,7 +887,11 @@ describe('webex-core', () => {
797
887
  });
798
888
 
799
889
  it('refreshes and downscopes the supertoken even if revocation of previous token fails', () => {
800
- const webex = new MockWebex();
890
+ const webex = new MockWebex({
891
+ children: {
892
+ metrics: Metrics,
893
+ },
894
+ });
801
895
  const credentials = new Credentials(undefined, {parent: webex});
802
896
 
803
897
  webex.trigger('change:config');
@@ -809,6 +903,7 @@ describe('webex-core', () => {
809
903
  const st2 = makeToken(webex, {
810
904
  access_token: 'ST2',
811
905
  refresh_token: 'RT2',
906
+ scope: 'scope1 scope2',
812
907
  });
813
908
 
814
909
  const t1 = makeToken(webex, {
@@ -825,6 +920,7 @@ describe('webex-core', () => {
825
920
  sinon.stub(st, 'refresh').returns(Promise.resolve(st2));
826
921
  sinon.stub(t1, 'revoke').returns(Promise.reject());
827
922
  sinon.spy(credentials, 'scheduleRefresh');
923
+ sinon.stub(webex.internal.metrics, 'submitClientMetrics').callsFake(() => {});
828
924
 
829
925
  credentials.set({
830
926
  supertoken: st,
@@ -847,6 +943,7 @@ describe('webex-core', () => {
847
943
  const webex = new MockWebex({
848
944
  children: {
849
945
  logger: Logger,
946
+ metrics: Metrics,
850
947
  },
851
948
  });
852
949
  const credentials = new Credentials(undefined, {parent: webex});
@@ -855,9 +952,11 @@ describe('webex-core', () => {
855
952
  const st = makeToken(webex, {
856
953
  access_token: 'ST',
857
954
  refresh_token: 'RT',
955
+ scope: '',
858
956
  });
859
957
 
860
958
  sinon.stub(st, 'refresh').returns(Promise.resolve(makeToken(webex, {access_token: 'ST2'})));
959
+ sinon.stub(webex.internal.metrics, 'submitClientMetrics').callsFake(() => {});
861
960
 
862
961
  const t1 = makeToken(webex, {
863
962
  access_token: 'AT1',
@@ -873,7 +972,7 @@ describe('webex-core', () => {
873
972
  });
874
973
 
875
974
  it('allows #getUserToken() to be revoked, but #getUserToken() promises will not resolve until the suport token has been refreshed', () => {
876
- const webex = new MockWebex();
975
+ const webex = new MockWebex({children: {metrics: Metrics}});
877
976
  const credentials = new Credentials(undefined, {parent: webex});
878
977
 
879
978
  webex.trigger('change:config');
@@ -899,6 +998,7 @@ describe('webex-core', () => {
899
998
 
900
999
  sinon.stub(st1, 'refresh').returns(Promise.resolve(st2));
901
1000
  sinon.stub(st2, 'downscope').returns(Promise.resolve(t2));
1001
+ sinon.stub(webex.internal.metrics, 'submitClientMetrics').callsFake(() => {});
902
1002
 
903
1003
  credentials.set({
904
1004
  supertoken: st1,
@@ -955,6 +1055,61 @@ describe('webex-core', () => {
955
1055
  assert.calledWith(triggerSpy, sinon.match('client:InvalidRequestError'));
956
1056
  });
957
1057
  });
1058
+
1059
+ it('exclude invalid scopes from user token, log and call metrics when fetched supertoken scope mismatch with the configured scope', () => {
1060
+ const webex = new MockWebex({
1061
+ children: {
1062
+ logger: Logger,
1063
+ metrics: Metrics,
1064
+ },
1065
+ });
1066
+ const credentials = new Credentials(undefined, {parent: webex});
1067
+
1068
+ webex.trigger('change:config');
1069
+ const st = makeToken(webex, {
1070
+ access_token: 'ST',
1071
+ refresh_token: 'RT',
1072
+ });
1073
+
1074
+ const st2 = makeToken(webex, {
1075
+ access_token: 'ST2',
1076
+ refresh_token: 'RT2',
1077
+ scope: 'scope1',
1078
+ });
1079
+
1080
+ const userToken = makeToken(webex, {
1081
+ access_token: 'AT1',
1082
+ scope: 'scope1 invalidScope1',
1083
+ });
1084
+
1085
+ credentials.set({
1086
+ supertoken: st,
1087
+ userTokens: [userToken],
1088
+ });
1089
+ const invalidScopes = 'invalidScope1 invalidScope2';
1090
+ credentials.config.scope = `scope1 ${invalidScopes}`;
1091
+
1092
+ sinon.stub(st2, 'downscope').returns(Promise.resolve());
1093
+ sinon.stub(st, 'refresh').returns(Promise.resolve(st2));
1094
+ sinon.spy(credentials, 'downscope');
1095
+ sinon.spy(credentials, 'scheduleRefresh');
1096
+
1097
+ sinon.stub(credentials.logger, 'warn').callsFake(() => {});
1098
+ sinon.stub(webex.internal.metrics, 'submitClientMetrics').callsFake(() => {});
1099
+
1100
+ return credentials.refresh().then(() => {
1101
+ assert.calledWith(
1102
+ credentials.logger.warn,
1103
+ `credentials: "${invalidScopes}" scope(s) are invalid because not listed in the supertoken, they will be excluded from user token requests.`
1104
+ );
1105
+ assert.calledWith(
1106
+ webex.internal.metrics.submitClientMetrics,
1107
+ 'JS_SDK_CREDENTIALS_TOKEN_REFRESH_SCOPE_MISMATCH',
1108
+ {fields: {invalidScopes}}
1109
+ );
1110
+ assert.calledWith(credentials.downscope, 'scope1');
1111
+ });
1112
+ });
958
1113
  });
959
1114
 
960
1115
  describe('#scheduleRefresh()', () => {