@webex/webex-core 3.11.0-webex-services-ready.1 → 3.12.0-auth-prejoin-fetch.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/config.js +24 -0
- package/dist/config.js.map +1 -1
- package/dist/credentials-config.js +12 -0
- package/dist/credentials-config.js.map +1 -1
- package/dist/interceptors/redirect.js +1 -1
- package/dist/interceptors/redirect.js.map +1 -1
- package/dist/lib/batcher.js +23 -7
- package/dist/lib/batcher.js.map +1 -1
- package/dist/lib/credentials/credentials.js +48 -4
- package/dist/lib/credentials/credentials.js.map +1 -1
- package/dist/lib/credentials/token.js +1 -1
- package/dist/lib/domains.js +90 -0
- package/dist/lib/domains.js.map +1 -0
- package/dist/lib/services/service-catalog.js +6 -10
- package/dist/lib/services/service-catalog.js.map +1 -1
- package/dist/lib/services/services.js +244 -57
- package/dist/lib/services/services.js.map +1 -1
- package/dist/lib/services-v2/service-catalog.js +6 -12
- package/dist/lib/services-v2/service-catalog.js.map +1 -1
- package/dist/lib/services-v2/services-v2.js +243 -52
- package/dist/lib/services-v2/services-v2.js.map +1 -1
- package/dist/plugins/logger.js +1 -1
- package/dist/webex-core.js +2 -2
- package/dist/webex-core.js.map +1 -1
- package/package.json +13 -13
- package/src/config.js +27 -0
- package/src/credentials-config.js +13 -0
- package/src/interceptors/redirect.js +4 -1
- package/src/lib/batcher.js +25 -10
- package/src/lib/credentials/credentials.js +50 -3
- package/src/lib/domains.ts +94 -0
- package/src/lib/services/service-catalog.js +6 -10
- package/src/lib/services/services.js +218 -46
- package/src/lib/services-v2/service-catalog.ts +6 -11
- package/src/lib/services-v2/services-v2.ts +218 -42
- package/test/fixtures/activation-email.ts +22 -0
- package/test/integration/spec/services/service-catalog.js +7 -6
- package/test/integration/spec/services/services.js +188 -128
- package/test/integration/spec/services-v2/services-v2.js +174 -114
- package/test/unit/spec/credentials/credentials.js +133 -2
- package/test/unit/spec/interceptors/auth.js +56 -0
- package/test/unit/spec/lib/batcher.js +56 -0
- package/test/unit/spec/services/service-catalog.js +93 -11
- package/test/unit/spec/services/services.js +591 -322
- package/test/unit/spec/services-v2/service-catalog.ts +93 -11
- package/test/unit/spec/services-v2/services-v2.ts +539 -214
- package/test/unit/spec/webex-core.js +0 -2
- package/test/unit/spec/webex-internal-core.js +0 -2
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import '@webex/internal-plugin-device';
|
|
6
6
|
|
|
7
7
|
import {assert} from '@webex/test-helper-chai';
|
|
8
|
-
import {flaky} from '@webex/test-helper-mocha';
|
|
8
|
+
import {flaky, retryOn429} from '@webex/test-helper-mocha';
|
|
9
9
|
import WebexCore, {
|
|
10
10
|
ServiceCatalogV2,
|
|
11
11
|
ServiceDetail,
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
formattedServiceHostmapEntryTest,
|
|
26
26
|
serviceHostmapV2,
|
|
27
27
|
} from '../../../fixtures/host-catalog-v2';
|
|
28
|
+
import {createActivationEmail} from '../../../fixtures/activation-email';
|
|
28
29
|
|
|
29
30
|
// /* eslint-disable no-underscore-dangle */
|
|
30
31
|
describe('webex-core', () => {
|
|
@@ -316,28 +317,19 @@ describe('webex-core', () => {
|
|
|
316
317
|
assert.isTrue(catalog.isReady);
|
|
317
318
|
});
|
|
318
319
|
|
|
319
|
-
it('should call services#initServiceCatalogs() on webex
|
|
320
|
+
it('should call services#initServiceCatalogs() on webex ready', async () => {
|
|
321
|
+
services._loadCatalogFromCache = sinon.stub().resolves(false);
|
|
320
322
|
services.initServiceCatalogs = sinon.stub().resolves();
|
|
321
323
|
services.initialize();
|
|
322
|
-
|
|
323
|
-
//
|
|
324
|
-
|
|
324
|
+
// The mode-specific ('ready'/'loaded') listener is registered inside the
|
|
325
|
+
// change:config handler, so fire change:config first, then 'ready'.
|
|
326
|
+
webex.trigger('change:config');
|
|
327
|
+
webex.trigger('ready');
|
|
328
|
+
// Wait for the async 'ready' handler to complete
|
|
329
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
325
330
|
assert.called(services.initServiceCatalogs);
|
|
326
331
|
});
|
|
327
332
|
|
|
328
|
-
it('should set services.ready to true after initialization completes', async () => {
|
|
329
|
-
// services.ready starts as false
|
|
330
|
-
const newWebex = new WebexCore({credentials: {supertoken: webexUser.token}});
|
|
331
|
-
const newServices = newWebex.internal.services;
|
|
332
|
-
|
|
333
|
-
// Wait for initialization to complete
|
|
334
|
-
await new Promise((resolve) => {
|
|
335
|
-
newServices.on('services:initialized', resolve);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
assert.isTrue(newServices.ready);
|
|
339
|
-
});
|
|
340
|
-
|
|
341
333
|
it('should collect different catalogs based on OrgId region', () =>
|
|
342
334
|
assert.notDeepEqual(catalog._getAllServiceDetails(), catalogEU._getAllServiceDetails()));
|
|
343
335
|
|
|
@@ -352,6 +344,45 @@ describe('webex-core', () => {
|
|
|
352
344
|
done();
|
|
353
345
|
}, 2000);
|
|
354
346
|
});
|
|
347
|
+
|
|
348
|
+
it('blocks webex.ready until services.ready flips when waitForCatalogInit is enabled', async () => {
|
|
349
|
+
const gatedWebex = new WebexCore({
|
|
350
|
+
credentials: {supertoken: webexUser.token},
|
|
351
|
+
config: {services: {waitForCatalogInit: true}},
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Before init settles, webex.ready must be false because services.ready
|
|
355
|
+
// is a dependency and starts false in the gated path.
|
|
356
|
+
assert.isFalse(gatedWebex.internal.services.ready, 'services.ready should start false');
|
|
357
|
+
assert.isFalse(
|
|
358
|
+
gatedWebex.ready,
|
|
359
|
+
'webex.ready should not fire while services.ready is false'
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// Wait up to 30s for services init to complete and flip ready.
|
|
363
|
+
await new Promise((resolve, reject) => {
|
|
364
|
+
if (gatedWebex.internal.services.ready) {
|
|
365
|
+
resolve();
|
|
366
|
+
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const timer = setTimeout(
|
|
370
|
+
() => reject(new Error('timed out waiting for services.ready')),
|
|
371
|
+
30_000
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
gatedWebex.internal.services.once('change:ready', () => {
|
|
375
|
+
clearTimeout(timer);
|
|
376
|
+
resolve();
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
assert.isTrue(
|
|
381
|
+
gatedWebex.internal.services.ready,
|
|
382
|
+
'services.ready should flip true after init settles'
|
|
383
|
+
);
|
|
384
|
+
assert.isTrue(gatedWebex.ready, 'webex.ready should fire once services.ready flips');
|
|
385
|
+
});
|
|
355
386
|
});
|
|
356
387
|
|
|
357
388
|
describe('#initServiceCatalogs()', () => {
|
|
@@ -801,18 +832,22 @@ describe('webex-core', () => {
|
|
|
801
832
|
}));
|
|
802
833
|
|
|
803
834
|
it('validates an authorized user and webex instance', () =>
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
835
|
+
retryOn429(() =>
|
|
836
|
+
services.validateUser({email: webexUser.email}).then((r) => {
|
|
837
|
+
assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
|
|
838
|
+
assert.equal(r.activated, true);
|
|
839
|
+
assert.equal(r.exists, true);
|
|
840
|
+
})
|
|
841
|
+
));
|
|
809
842
|
|
|
810
843
|
it('validates an authorized EU user and webex instance', () =>
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
844
|
+
retryOn429(() =>
|
|
845
|
+
servicesEU.validateUser({email: webexUserEU.email}).then((r) => {
|
|
846
|
+
assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
|
|
847
|
+
assert.equal(r.activated, true);
|
|
848
|
+
assert.equal(r.exists, true);
|
|
849
|
+
})
|
|
850
|
+
));
|
|
816
851
|
|
|
817
852
|
it("returns a rejected promise if the provided email isn't valid", () =>
|
|
818
853
|
unauthServices
|
|
@@ -825,113 +860,134 @@ describe('webex-core', () => {
|
|
|
825
860
|
}));
|
|
826
861
|
|
|
827
862
|
it('validates a non-existing user', () =>
|
|
828
|
-
|
|
829
|
-
.validateUser({email:
|
|
830
|
-
.then((r) => {
|
|
863
|
+
retryOn429(() =>
|
|
864
|
+
unauthServices.validateUser({email: createActivationEmail()}).then((r) => {
|
|
831
865
|
assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
|
|
832
866
|
assert.equal(r.activated, false);
|
|
833
867
|
assert.equal(r.exists, false);
|
|
834
|
-
})
|
|
868
|
+
})
|
|
869
|
+
));
|
|
835
870
|
|
|
836
871
|
it('validates new user with activationOptions suppressEmail false', () =>
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
872
|
+
retryOn429(() =>
|
|
873
|
+
unauthServices
|
|
874
|
+
.validateUser({
|
|
875
|
+
email: createActivationEmail(),
|
|
876
|
+
activationOptions: {suppressEmail: false},
|
|
877
|
+
})
|
|
878
|
+
.then((r) => {
|
|
879
|
+
assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
|
|
880
|
+
assert.equal(r.activated, false);
|
|
881
|
+
assert.equal(r.exists, false);
|
|
882
|
+
assert.equal(r.user.verificationEmailTriggered, true);
|
|
883
|
+
})
|
|
884
|
+
));
|
|
848
885
|
|
|
849
886
|
it('validates new user with activationOptions suppressEmail true', () =>
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
887
|
+
retryOn429(() =>
|
|
888
|
+
unauthServices
|
|
889
|
+
.validateUser({
|
|
890
|
+
email: createActivationEmail(),
|
|
891
|
+
activationOptions: {suppressEmail: true},
|
|
892
|
+
})
|
|
893
|
+
.then((r) => {
|
|
894
|
+
assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
|
|
895
|
+
assert.equal(r.activated, false);
|
|
896
|
+
assert.equal(r.exists, false);
|
|
897
|
+
assert.equal(r.user.verificationEmailTriggered, false);
|
|
898
|
+
})
|
|
899
|
+
));
|
|
861
900
|
|
|
862
901
|
it('validates an inactive user', () => {
|
|
863
902
|
const inactive = 'webex.web.client+nonactivated@gmail.com';
|
|
864
903
|
|
|
865
|
-
return
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
904
|
+
return retryOn429(() =>
|
|
905
|
+
unauthServices
|
|
906
|
+
.validateUser({email: inactive, activationOptions: {suppressEmail: true}})
|
|
907
|
+
.then((r) => {
|
|
908
|
+
assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
|
|
909
|
+
assert.equal(r.activated, false, 'activated');
|
|
910
|
+
assert.equal(r.exists, true, 'exists');
|
|
911
|
+
})
|
|
912
|
+
).catch(() => {
|
|
913
|
+
assert(true);
|
|
914
|
+
});
|
|
875
915
|
});
|
|
876
916
|
|
|
877
917
|
it('validates an existing user', () =>
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
918
|
+
retryOn429(() =>
|
|
919
|
+
unauthServices.validateUser({email: webexUser.email}).then((r) => {
|
|
920
|
+
assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
|
|
921
|
+
assert.equal(r.activated, true);
|
|
922
|
+
assert.equal(r.exists, true);
|
|
923
|
+
})
|
|
924
|
+
));
|
|
883
925
|
|
|
884
926
|
it('validates an existing EU user', () =>
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
927
|
+
retryOn429(() =>
|
|
928
|
+
unauthServices.validateUser({email: webexUserEU.email}).then((r) => {
|
|
929
|
+
assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
|
|
930
|
+
assert.equal(r.activated, true);
|
|
931
|
+
assert.equal(r.exists, true);
|
|
932
|
+
})
|
|
933
|
+
));
|
|
890
934
|
|
|
891
935
|
it('sends the prelogin user id as undefined when not specified', () => {
|
|
892
936
|
const requestStub = sandbox.spy(unauthServices, 'request');
|
|
893
937
|
|
|
894
|
-
return
|
|
895
|
-
.
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
938
|
+
return retryOn429(() => {
|
|
939
|
+
requestStub.resetHistory();
|
|
940
|
+
|
|
941
|
+
return unauthServices
|
|
942
|
+
.validateUser({
|
|
943
|
+
email: createActivationEmail(),
|
|
944
|
+
activationOptions: {suppressEmail: true},
|
|
945
|
+
})
|
|
946
|
+
.then(() => {
|
|
947
|
+
assert.isUndefined(getActivationRequest(requestStub).headers['x-prelogin-userid']);
|
|
948
|
+
});
|
|
949
|
+
});
|
|
902
950
|
});
|
|
903
951
|
|
|
904
952
|
it('sends the prelogin user id as provided when specified', () => {
|
|
905
953
|
const requestStub = sandbox.spy(unauthServices, 'request');
|
|
906
954
|
const preloginUserId = uuid.v4();
|
|
907
955
|
|
|
908
|
-
return
|
|
909
|
-
.
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
956
|
+
return retryOn429(() => {
|
|
957
|
+
requestStub.resetHistory();
|
|
958
|
+
|
|
959
|
+
return unauthServices
|
|
960
|
+
.validateUser({
|
|
961
|
+
email: createActivationEmail(),
|
|
962
|
+
activationOptions: {suppressEmail: true},
|
|
963
|
+
preloginUserId,
|
|
964
|
+
})
|
|
965
|
+
.then(() => {
|
|
966
|
+
assert.strictEqual(
|
|
967
|
+
getActivationRequest(requestStub).headers['x-prelogin-userid'],
|
|
968
|
+
preloginUserId
|
|
969
|
+
);
|
|
970
|
+
});
|
|
971
|
+
});
|
|
920
972
|
});
|
|
921
973
|
|
|
922
974
|
it('uses the license service by default', () => {
|
|
923
975
|
const requestStub = sandbox.spy(unauthServices, 'request');
|
|
924
976
|
|
|
925
|
-
return
|
|
926
|
-
.
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
977
|
+
return retryOn429(() => {
|
|
978
|
+
requestStub.resetHistory();
|
|
979
|
+
|
|
980
|
+
return unauthServices
|
|
981
|
+
.validateUser({
|
|
982
|
+
email: createActivationEmail(),
|
|
983
|
+
activationOptions: {suppressEmail: true},
|
|
984
|
+
})
|
|
985
|
+
.then(() => {
|
|
986
|
+
const request = getActivationRequest(requestStub, false);
|
|
987
|
+
assert.strictEqual(request.service, 'license');
|
|
988
|
+
assert.strictEqual(request.resource, 'users/activations');
|
|
989
|
+
});
|
|
990
|
+
});
|
|
935
991
|
});
|
|
936
992
|
|
|
937
993
|
it('uses the user-onboarding service when useUserOnboardingServiceForActivations config is true', () => {
|
|
@@ -945,16 +1001,20 @@ describe('webex-core', () => {
|
|
|
945
1001
|
const userOnboardingServices = userOnboardingWebex.internal.services;
|
|
946
1002
|
const requestStub = sandbox.spy(userOnboardingServices, 'request');
|
|
947
1003
|
|
|
948
|
-
return
|
|
949
|
-
.
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1004
|
+
return retryOn429(() => {
|
|
1005
|
+
requestStub.resetHistory();
|
|
1006
|
+
|
|
1007
|
+
return userOnboardingServices
|
|
1008
|
+
.validateUser({
|
|
1009
|
+
email: createActivationEmail(),
|
|
1010
|
+
activationOptions: {suppressEmail: true},
|
|
1011
|
+
})
|
|
1012
|
+
.then(() => {
|
|
1013
|
+
const request = getActivationRequest(requestStub, true);
|
|
1014
|
+
assert.strictEqual(request.service, 'user-onboarding');
|
|
1015
|
+
assert.strictEqual(request.resource, 'api/v1/users/activations');
|
|
1016
|
+
});
|
|
1017
|
+
});
|
|
958
1018
|
});
|
|
959
1019
|
});
|
|
960
1020
|
|
|
@@ -183,6 +183,10 @@ describe('webex-core', () => {
|
|
|
183
183
|
webex.credentials.buildLoginUrl({state: 'state'});
|
|
184
184
|
}, /if specified, `options.state` must be an object/);
|
|
185
185
|
|
|
186
|
+
assert.throws(() => {
|
|
187
|
+
webex.credentials.buildLoginUrl({state: null});
|
|
188
|
+
}, /if specified, `options.state` must be an object/);
|
|
189
|
+
|
|
186
190
|
assert.doesNotThrow(() => {
|
|
187
191
|
webex.credentials.buildLoginUrl({state: {}});
|
|
188
192
|
}, /if specified, `options.state` must be an object/);
|
|
@@ -232,6 +236,132 @@ describe('webex-core', () => {
|
|
|
232
236
|
});
|
|
233
237
|
});
|
|
234
238
|
|
|
239
|
+
describe('#buildThirdPartyLoginUrl()', () => {
|
|
240
|
+
it('throws if both `oauth2provider` and `returnURL` are missing', () => {
|
|
241
|
+
const webex = new MockWebex();
|
|
242
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
243
|
+
|
|
244
|
+
webex.trigger('change:config');
|
|
245
|
+
|
|
246
|
+
assert.throws(() => {
|
|
247
|
+
credentials.buildThirdPartyLoginUrl({});
|
|
248
|
+
}, /`options.oauth2provider` is required/);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('throws if `oauth2provider` is missing', () => {
|
|
252
|
+
const webex = new MockWebex();
|
|
253
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
254
|
+
|
|
255
|
+
webex.trigger('change:config');
|
|
256
|
+
|
|
257
|
+
assert.throws(() => {
|
|
258
|
+
credentials.buildThirdPartyLoginUrl({returnURL: 'https://web.webex.com'});
|
|
259
|
+
}, /`options.oauth2provider` is required/);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('throws if `returnURL` is missing', () => {
|
|
263
|
+
const webex = new MockWebex();
|
|
264
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
265
|
+
|
|
266
|
+
webex.trigger('change:config');
|
|
267
|
+
|
|
268
|
+
assert.throws(() => {
|
|
269
|
+
credentials.buildThirdPartyLoginUrl({oauth2provider: 'google'});
|
|
270
|
+
}, /`options.returnURL` is required/);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
skipInBrowser(it)('generates the third-party login url', () => {
|
|
274
|
+
const webex = new MockWebex();
|
|
275
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
276
|
+
|
|
277
|
+
webex.trigger('change:config');
|
|
278
|
+
|
|
279
|
+
assert.equal(
|
|
280
|
+
credentials.buildThirdPartyLoginUrl({
|
|
281
|
+
oauth2provider: 'google',
|
|
282
|
+
returnURL: 'https://web.webex.com',
|
|
283
|
+
}),
|
|
284
|
+
`${
|
|
285
|
+
process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
|
|
286
|
+
}/idb/ThirdPartyLogin?oauth2provider=google&returnURL=https%3A%2F%2Fweb.webex.com`
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
skipInBrowser(it)('generates the url with different parameter values', () => {
|
|
291
|
+
const webex = new MockWebex();
|
|
292
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
293
|
+
|
|
294
|
+
webex.trigger('change:config');
|
|
295
|
+
|
|
296
|
+
assert.equal(
|
|
297
|
+
credentials.buildThirdPartyLoginUrl({
|
|
298
|
+
oauth2provider: 'apple',
|
|
299
|
+
returnURL: 'https://example.com/callback',
|
|
300
|
+
}),
|
|
301
|
+
`${
|
|
302
|
+
process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
|
|
303
|
+
}/idb/ThirdPartyLogin?oauth2provider=apple&returnURL=https%3A%2F%2Fexample.com%2Fcallback`
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('throws if `state` is not an object', () => {
|
|
308
|
+
const webex = new MockWebex();
|
|
309
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
310
|
+
|
|
311
|
+
webex.trigger('change:config');
|
|
312
|
+
|
|
313
|
+
assert.throws(() => {
|
|
314
|
+
credentials.buildThirdPartyLoginUrl({
|
|
315
|
+
oauth2provider: 'google',
|
|
316
|
+
returnURL: 'https://web.webex.com',
|
|
317
|
+
state: 'not-an-object',
|
|
318
|
+
});
|
|
319
|
+
}, /`options.state` must be an object/);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
skipInBrowser(it)('omits `state` when an empty object is provided', () => {
|
|
323
|
+
const webex = new MockWebex();
|
|
324
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
325
|
+
|
|
326
|
+
webex.trigger('change:config');
|
|
327
|
+
|
|
328
|
+
const result = credentials.buildThirdPartyLoginUrl({
|
|
329
|
+
oauth2provider: 'google',
|
|
330
|
+
returnURL: 'https://web.webex.com',
|
|
331
|
+
state: {},
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const parsed = new URL(result);
|
|
335
|
+
|
|
336
|
+
assert.isFalse(parsed.searchParams.has('state'));
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
skipInBrowser(it)('base64url-encodes a non-empty `state` and emits it as a top-level query param', () => {
|
|
340
|
+
const webex = new MockWebex();
|
|
341
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
342
|
+
|
|
343
|
+
webex.trigger('change:config');
|
|
344
|
+
|
|
345
|
+
const result = credentials.buildThirdPartyLoginUrl({
|
|
346
|
+
oauth2provider: 'google',
|
|
347
|
+
returnURL: 'https://web.webex.com',
|
|
348
|
+
state: {csrf_token: 'abc', popUpSignIn: true},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Literal base64url of '{"csrf_token":"abc","popUpSignIn":true}'
|
|
352
|
+
const expectedState = 'eyJjc3JmX3Rva2VuIjoiYWJjIiwicG9wVXBTaWduSW4iOnRydWV9';
|
|
353
|
+
|
|
354
|
+
assert.equal(
|
|
355
|
+
result,
|
|
356
|
+
`${
|
|
357
|
+
process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
|
|
358
|
+
}/idb/ThirdPartyLogin?oauth2provider=google&returnURL=${encodeURIComponent(
|
|
359
|
+
'https://web.webex.com'
|
|
360
|
+
)}&state=${expectedState}`
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
235
365
|
describe('#buildLogoutUrl()', () => {
|
|
236
366
|
skipInBrowser(it)('generates the logout url', () => {
|
|
237
367
|
const webex = new MockWebex();
|
|
@@ -368,7 +498,9 @@ describe('webex-core', () => {
|
|
|
368
498
|
});
|
|
369
499
|
|
|
370
500
|
it('should throw when provided an invalid token', () =>
|
|
371
|
-
expect(() => credentials.extractOrgIdFromUserToken()).toThrow(
|
|
501
|
+
expect(() => credentials.extractOrgIdFromUserToken()).toThrow(
|
|
502
|
+
'the provided token is not a valid format, token has 1 sections'
|
|
503
|
+
));
|
|
372
504
|
|
|
373
505
|
it('should throw when no token is provided', () =>
|
|
374
506
|
expect(() => credentials.extractOrgIdFromUserToken()).toThrow());
|
|
@@ -799,7 +931,6 @@ describe('webex-core', () => {
|
|
|
799
931
|
.then(() => assert.isRejected(webex.boundedStorage.get('Credentials', '@'), /NotFound/));
|
|
800
932
|
});
|
|
801
933
|
|
|
802
|
-
|
|
803
934
|
// it('does not induce any token refreshes');
|
|
804
935
|
|
|
805
936
|
it('prevents #getUserToken() from being invoked', () => {
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
AuthInterceptor,
|
|
15
15
|
config,
|
|
16
16
|
Credentials,
|
|
17
|
+
Services,
|
|
18
|
+
ServicesV2,
|
|
17
19
|
WebexHttpError,
|
|
18
20
|
Token,
|
|
19
21
|
serviceConstants,
|
|
@@ -380,6 +382,60 @@ describe('webex-core', () => {
|
|
|
380
382
|
});
|
|
381
383
|
});
|
|
382
384
|
|
|
385
|
+
describe('#onRequest() against a real service catalog', () => {
|
|
386
|
+
// The cases above stub `isAllowedDomainUrl`, so they answer the
|
|
387
|
+
// allowed-domain question themselves and cannot show that the catalog
|
|
388
|
+
// matcher is reached by the code that attaches the token. These use a
|
|
389
|
+
// real catalog and real allowed-domain methods.
|
|
390
|
+
[
|
|
391
|
+
{name: 'Services', Constructor: Services},
|
|
392
|
+
{name: 'ServicesV2', Constructor: ServicesV2},
|
|
393
|
+
].forEach(({name, Constructor}) => {
|
|
394
|
+
describe(name, () => {
|
|
395
|
+
let getUserToken;
|
|
396
|
+
|
|
397
|
+
beforeEach(() => {
|
|
398
|
+
const services = new Constructor(undefined, {parent: webex});
|
|
399
|
+
|
|
400
|
+
services._getCatalog().setAllowedDomains(['webex.com']);
|
|
401
|
+
// the catalog holds no services, so a url that is not covered by
|
|
402
|
+
// an allowed domain has nothing else to authorize it
|
|
403
|
+
services.waitForService = sinon.stub().rejects(new Error('no such service'));
|
|
404
|
+
|
|
405
|
+
webex.internal.services = services;
|
|
406
|
+
getUserToken = sinon.spy(webex.credentials, 'getUserToken');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
afterEach(() => {
|
|
410
|
+
getUserToken.restore();
|
|
411
|
+
delete webex.internal.services;
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('adds the authorization header for a url under an allowed domain', () =>
|
|
415
|
+
interceptor
|
|
416
|
+
.onRequest({uri: 'https://api.webex.com/resource', headers: {}})
|
|
417
|
+
.then((options) => {
|
|
418
|
+
assert.equal(
|
|
419
|
+
options.headers.authorization,
|
|
420
|
+
webex.credentials.supertoken.toString()
|
|
421
|
+
);
|
|
422
|
+
assert.calledOnce(getUserToken);
|
|
423
|
+
}));
|
|
424
|
+
|
|
425
|
+
[
|
|
426
|
+
'https://notwebex.com/resource',
|
|
427
|
+
'https://webex.com.unrelated.example/resource',
|
|
428
|
+
].forEach((uri) => {
|
|
429
|
+
it(`does not add the authorization header for ${uri}`, () =>
|
|
430
|
+
interceptor.onRequest({uri, headers: {}}).then((options) => {
|
|
431
|
+
assert.notProperty(options.headers, 'authorization');
|
|
432
|
+
assert.notCalled(getUserToken);
|
|
433
|
+
}));
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
383
439
|
describe('#onResponseError()', () => {
|
|
384
440
|
describe('when the server responds with 401', () => {
|
|
385
441
|
nodeOnly(it)('refreshes the access token and replays the request', () => {
|