@webex/plugin-authorization-browser-first-party 3.11.0 → 3.12.0-llmrefactor.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/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"names":["_webexCore","require","_authorization","_interopRequireWildcard","_config","_interopRequireDefault","e","t","_WeakMap","r","n","__esModule","o","i","f","__proto__","default","_typeof","has","get","set","_t","hasOwnProperty","call","_Object$defineProperty","_Object$getOwnPropertyDescriptor","proxies","registerPlugin","Authorization","config"],"sources":["index.js"],"sourcesContent":["/*!\n * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.\n */\n\nimport {registerPlugin} from '@webex/webex-core';\n\nimport Authorization from './authorization';\nimport config from './config';\n\nconst proxies = ['isAuthorizing', 'isAuthenticating'];\n\nregisterPlugin('authorization', Authorization, {\n config,\n proxies,\n});\n\nexport {default, Events} from './authorization';\nexport {default as config} from './config';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAIA,IAAAA,UAAA,GAAAC,OAAA;AAEA,IAAAC,cAAA,GAAAC,uBAAA,CAAAF,OAAA;AACA,IAAAG,OAAA,GAAAC,sBAAA,CAAAJ,OAAA;AAA8B,SAAAE,wBAAAG,CAAA,EAAAC,CAAA,6BAAAC,QAAA,MAAAC,CAAA,OAAAD,QAAA,IAAAE,CAAA,OAAAF,QAAA,YAAAL,uBAAA,YAAAA,wBAAAG,CAAA,EAAAC,CAAA,SAAAA,CAAA,IAAAD,CAAA,IAAAA,CAAA,CAAAK,UAAA,SAAAL,CAAA,MAAAM,CAAA,EAAAC,CAAA,EAAAC,CAAA,KAAAC,SAAA,QAAAC,OAAA,EAAAV,CAAA,iBAAAA,CAAA,gBAAAW,OAAA,CAAAX,CAAA,0BAAAA,CAAA,SAAAQ,CAAA,MAAAF,CAAA,GAAAL,CAAA,GAAAG,CAAA,GAAAD,CAAA,QAAAG,CAAA,CAAAM,GAAA,CAAAZ,CAAA,UAAAM,CAAA,CAAAO,GAAA,CAAAb,CAAA,GAAAM,CAAA,CAAAQ,GAAA,CAAAd,CAAA,EAAAQ,CAAA,cAAAO,EAAA,IAAAf,CAAA,gBAAAe,EAAA,OAAAC,cAAA,CAAAC,IAAA,CAAAjB,CAAA,EAAAe,EAAA,OAAAR,CAAA,IAAAD,CAAA,GAAAY,sBAAA,KAAAC,gCAAA,CAAAnB,CAAA,EAAAe,EAAA,OAAAR,CAAA,CAAAM,GAAA,IAAAN,CAAA,CAAAO,GAAA,IAAAR,CAAA,CAAAE,CAAA,EAAAO,EAAA,EAAAR,CAAA,IAAAC,CAAA,CAAAO,EAAA,IAAAf,CAAA,CAAAe,EAAA,WAAAP,CAAA,KAAAR,CAAA,EAAAC,CAAA;AAP9B;AACA;AACA;;AAOA,IAAMmB,OAAO,GAAG,CAAC,eAAe,EAAE,kBAAkB,CAAC;AAErD,IAAAC,yBAAc,EAAC,eAAe,EAAEC,sBAAa,EAAE;EAC7CC,MAAM,EAANA,eAAM;EACNH,OAAO,EAAPA;AACF,CAAC,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["_webexCore","require","_authorization","_interopRequireWildcard","_config","_interopRequireDefault","e","t","_WeakMap","r","n","__esModule","o","i","f","__proto__","default","_typeof","has","get","set","_t","hasOwnProperty","call","_Object$defineProperty","_Object$getOwnPropertyDescriptor","proxies","registerPlugin","Authorization","config"],"sources":["index.js"],"sourcesContent":["/*!\n * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.\n */\n\nimport {registerPlugin} from '@webex/webex-core';\n\nimport Authorization from './authorization';\nimport config from './config';\n\nconst proxies = ['isAuthorizing', 'isAuthenticating'];\n\nregisterPlugin('authorization', Authorization, {\n config,\n proxies,\n});\n\nexport {default, Events, InitialAuthorizationCodeGrantOutcomes} from './authorization';\nexport {default as config} from './config';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAIA,IAAAA,UAAA,GAAAC,OAAA;AAEA,IAAAC,cAAA,GAAAC,uBAAA,CAAAF,OAAA;AACA,IAAAG,OAAA,GAAAC,sBAAA,CAAAJ,OAAA;AAA8B,SAAAE,wBAAAG,CAAA,EAAAC,CAAA,6BAAAC,QAAA,MAAAC,CAAA,OAAAD,QAAA,IAAAE,CAAA,OAAAF,QAAA,YAAAL,uBAAA,YAAAA,wBAAAG,CAAA,EAAAC,CAAA,SAAAA,CAAA,IAAAD,CAAA,IAAAA,CAAA,CAAAK,UAAA,SAAAL,CAAA,MAAAM,CAAA,EAAAC,CAAA,EAAAC,CAAA,KAAAC,SAAA,QAAAC,OAAA,EAAAV,CAAA,iBAAAA,CAAA,gBAAAW,OAAA,CAAAX,CAAA,0BAAAA,CAAA,SAAAQ,CAAA,MAAAF,CAAA,GAAAL,CAAA,GAAAG,CAAA,GAAAD,CAAA,QAAAG,CAAA,CAAAM,GAAA,CAAAZ,CAAA,UAAAM,CAAA,CAAAO,GAAA,CAAAb,CAAA,GAAAM,CAAA,CAAAQ,GAAA,CAAAd,CAAA,EAAAQ,CAAA,cAAAO,EAAA,IAAAf,CAAA,gBAAAe,EAAA,OAAAC,cAAA,CAAAC,IAAA,CAAAjB,CAAA,EAAAe,EAAA,OAAAR,CAAA,IAAAD,CAAA,GAAAY,sBAAA,KAAAC,gCAAA,CAAAnB,CAAA,EAAAe,EAAA,OAAAR,CAAA,CAAAM,GAAA,IAAAN,CAAA,CAAAO,GAAA,IAAAR,CAAA,CAAAE,CAAA,EAAAO,EAAA,EAAAR,CAAA,IAAAC,CAAA,CAAAO,EAAA,IAAAf,CAAA,CAAAe,EAAA,WAAAP,CAAA,KAAAR,CAAA,EAAAC,CAAA;AAP9B;AACA;AACA;;AAOA,IAAMmB,OAAO,GAAG,CAAC,eAAe,EAAE,kBAAkB,CAAC;AAErD,IAAAC,yBAAc,EAAC,eAAe,EAAEC,sBAAa,EAAE;EAC7CC,MAAM,EAANA,eAAM;EACNH,OAAO,EAAPA;AACF,CAAC,CAAC","ignoreList":[]}
package/package.json CHANGED
@@ -26,23 +26,23 @@
26
26
  "@webex/eslint-config-legacy": "0.0.0",
27
27
  "@webex/jest-config-legacy": "0.0.0",
28
28
  "@webex/legacy-tools": "0.0.0",
29
- "@webex/test-helper-chai": "3.11.0",
30
- "@webex/test-helper-mocha": "3.11.0",
31
- "@webex/test-helper-mock-webex": "3.11.0",
32
- "@webex/test-helper-test-users": "3.11.0",
29
+ "@webex/test-helper-chai": "3.12.0-llmrefactor.1",
30
+ "@webex/test-helper-mocha": "3.12.0-llmrefactor.1",
31
+ "@webex/test-helper-mock-webex": "3.12.0-llmrefactor.1",
32
+ "@webex/test-helper-test-users": "3.12.0-llmrefactor.1",
33
33
  "eslint": "^8.24.0",
34
34
  "prettier": "^2.7.1",
35
35
  "sinon": "^9.2.4"
36
36
  },
37
37
  "dependencies": {
38
- "@webex/common": "3.11.0",
39
- "@webex/storage-adapter-local-storage": "3.11.0",
40
- "@webex/test-helper-automation": "3.11.0",
41
- "@webex/test-helper-chai": "3.11.0",
42
- "@webex/test-helper-mocha": "3.11.0",
43
- "@webex/test-helper-mock-webex": "3.11.0",
44
- "@webex/test-helper-test-users": "3.11.0",
45
- "@webex/webex-core": "3.11.0",
38
+ "@webex/common": "3.12.0-llmrefactor.1",
39
+ "@webex/storage-adapter-local-storage": "3.12.0-llmrefactor.1",
40
+ "@webex/test-helper-automation": "3.12.0-llmrefactor.1",
41
+ "@webex/test-helper-chai": "3.12.0-llmrefactor.1",
42
+ "@webex/test-helper-mocha": "3.12.0-llmrefactor.1",
43
+ "@webex/test-helper-mock-webex": "3.12.0-llmrefactor.1",
44
+ "@webex/test-helper-test-users": "3.12.0-llmrefactor.1",
45
+ "@webex/webex-core": "3.12.0-llmrefactor.1",
46
46
  "crypto-js": "^4.1.1",
47
47
  "lodash": "^4.17.21",
48
48
  "uuid": "^3.3.2"
@@ -56,5 +56,5 @@
56
56
  "test:style": "eslint ./src/**/*.*",
57
57
  "test:unit": "webex-legacy-tools test --unit --runner jest"
58
58
  },
59
- "version": "3.11.0"
59
+ "version": "3.12.0-llmrefactor.1"
60
60
  }
@@ -1,4 +1,4 @@
1
- // @ts-nocheck
1
+ // @ts-nocheck
2
2
  /* eslint-disable */
3
3
  /*!
4
4
  * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
@@ -14,9 +14,9 @@ import querystring from 'querystring';
14
14
  import url from 'url';
15
15
  import {EventEmitter} from 'events';
16
16
 
17
- import {base64, oneFlight, whileInFlight} from '@webex/common';
17
+ import {decodeState, encodeState, oneFlight, whileInFlight} from '@webex/common';
18
18
  import {grantErrors, WebexPlugin} from '@webex/webex-core';
19
- import {cloneDeep, isEmpty, omit} from 'lodash';
19
+ import {cloneDeep, isEmpty, omit, isObject} from 'lodash';
20
20
  import uuid from 'uuid';
21
21
  import base64url from 'crypto-js/enc-base64url';
22
22
  import CryptoJS from 'crypto-js';
@@ -39,6 +39,18 @@ export const Events = {
39
39
  qRCodeLogin: 'qRCodeLogin',
40
40
  };
41
41
 
42
+ /**
43
+ * Terminal outcomes for the automatic authorization-code exchange performed
44
+ * during authorization plugin initialization.
45
+ *
46
+ * @enum {string}
47
+ */
48
+ export const InitialAuthorizationCodeGrantOutcomes = {
49
+ failure: 'failure',
50
+ notAttempted: 'not_attempted',
51
+ success: 'success',
52
+ };
53
+
42
54
  /**
43
55
  * Browser support for OAuth2 for first-party (Webex Web Client) usage.
44
56
  *
@@ -69,6 +81,37 @@ export const Events = {
69
81
  */
70
82
  const Authorization = WebexPlugin.extend({
71
83
  derived: {
84
+ /**
85
+ * Retains the terminal outcome of the automatic authorization-code exchange
86
+ * performed during this authorization plugin instance's initialization.
87
+ *
88
+ * This historical value does not represent current authorization state,
89
+ * credentials hydrated from storage, guest authentication, or
90
+ * authorization-code exchanges requested later on the same SDK instance. It
91
+ * is not reset by logout. OAuth redirect errors and CSRF validation failures
92
+ * occur before the exchange, so the value remains not_attempted; these errors
93
+ * can throw before ready becomes true.
94
+ *
95
+ * Calling logout({noRedirect: true}) does not cancel an initialization
96
+ * exchange already in flight. If that exchange later settles, it can still
97
+ * update credentials and this retained outcome.
98
+ *
99
+ * Interpret only after authorization readiness:
100
+ * - not_attempted: initialization did not invoke requestAuthorizationCodeGrant()
101
+ * - success: the initialization exchange fulfilled
102
+ * - failure: the initialization exchange threw or rejected
103
+ *
104
+ * @instance
105
+ * @memberof AuthorizationBrowserFirstParty
106
+ * @readonly
107
+ * @type {string}
108
+ */
109
+ initialAuthorizationCodeGrantOutcome: {
110
+ deps: ['_initialAuthorizationCodeGrantOutcome'],
111
+ fn() {
112
+ return this._initialAuthorizationCodeGrantOutcome;
113
+ },
114
+ },
72
115
  /**
73
116
  * Alias of {@link AuthorizationBrowserFirstParty#isAuthorizing}
74
117
  * @instance
@@ -94,6 +137,14 @@ const Authorization = WebexPlugin.extend({
94
137
  default: false,
95
138
  type: 'boolean',
96
139
  },
140
+ /**
141
+ * Internal backing state for the initial authorization-code grant outcome.
142
+ * @private
143
+ */
144
+ _initialAuthorizationCodeGrantOutcome: {
145
+ default: InitialAuthorizationCodeGrantOutcomes.notAttempted,
146
+ type: 'string',
147
+ },
97
148
  /**
98
149
  * Indicates that the plugin has finished any automatic startup
99
150
  * processing (e.g., exchanging a returned authorization code)
@@ -199,7 +250,7 @@ const Authorization = WebexPlugin.extend({
199
250
 
200
251
  // Decode and parse state object (if present)
201
252
  if (location.query.state) {
202
- location.query.state = JSON.parse(base64.decode(location.query.state));
253
+ location.query.state = decodeState(location.query.state);
203
254
  } else {
204
255
  location.query.state = {};
205
256
  }
@@ -233,7 +284,13 @@ const Authorization = WebexPlugin.extend({
233
284
  .collectPreauthCatalog(preauthCatalogParams)
234
285
  .catch(() => Promise.resolve()) // Non-fatal if catalog collection fails
235
286
  .then(() => this.requestAuthorizationCodeGrant({code, codeVerifier}))
287
+ .then(() => {
288
+ this._initialAuthorizationCodeGrantOutcome =
289
+ InitialAuthorizationCodeGrantOutcomes.success;
290
+ })
236
291
  .catch((error) => {
292
+ this._initialAuthorizationCodeGrantOutcome =
293
+ InitialAuthorizationCodeGrantOutcomes.failure;
237
294
  this.logger.warn('authorization: failed initial authorization code grant request', error);
238
295
  })
239
296
  .then(() => {
@@ -267,7 +324,7 @@ const Authorization = WebexPlugin.extend({
267
324
  eventType: 'initiateLogin',
268
325
  data: {
269
326
  hasEmail: !!options.email,
270
- hasState: !!options.state
327
+ hasState: !!options.state,
271
328
  },
272
329
  });
273
330
 
@@ -316,7 +373,7 @@ const Authorization = WebexPlugin.extend({
316
373
 
317
374
  this.eventEmitter.emit(Events.login, {
318
375
  eventType: 'redirectToLoginUrl',
319
- data: { loginUrl },
376
+ data: {loginUrl},
320
377
  });
321
378
 
322
379
  if (options?.separateWindow) {
@@ -343,6 +400,97 @@ const Authorization = WebexPlugin.extend({
343
400
  return Promise.resolve();
344
401
  },
345
402
 
403
+ /**
404
+ * Initiates third-party (social provider) login. Generates a CSRF token,
405
+ * embeds it in `options.state.csrf_token`, and delegates to
406
+ * `initiateThirdPartyLoginRedirect` for navigation.
407
+ *
408
+ * @instance
409
+ * @memberof AuthorizationBrowserFirstParty
410
+ * @param {Object} options
411
+ * @param {string} options.oauth2provider
412
+ * @param {string} options.returnURL
413
+ * @param {Object} [options.state] - Caller-supplied state object. Merged
414
+ * with the generated `csrf_token`.
415
+ * @returns {Promise<void>}
416
+ */
417
+ initiateThirdPartyLogin(options = {}) {
418
+ options = cloneDeep(options);
419
+ if (options.state !== undefined && !isObject(options.state)) {
420
+ throw new Error('if specified, `options.state` must be an object');
421
+ }
422
+ options.state = options.state || {};
423
+ options.state.csrf_token = this._generateSecurityToken();
424
+
425
+ return this.initiateThirdPartyLoginRedirect(options);
426
+ },
427
+
428
+ /**
429
+ * Performs the navigation step of the third-party login flow. Builds the
430
+ * IdBroker URL via `Credentials#buildThirdPartyLoginUrl` and assigns it
431
+ * to `getWindow().location`.
432
+ *
433
+ * Mirrors `initiateAuthorizationCodeGrant` for the `/authorize` flow.
434
+ * Consumers may override this method for custom navigation handling
435
+ * (e.g. postMessage in iframed contexts).
436
+ *
437
+ * @instance
438
+ * @memberof AuthorizationBrowserFirstParty
439
+ * @param {Object} options
440
+ * @param {string} options.oauth2provider
441
+ * @param {string} options.returnURL
442
+ * @returns {Promise<void>}
443
+ */
444
+ initiateThirdPartyLoginRedirect(options = {}) {
445
+ this.logger.info('authorization: initiating third-party login redirect');
446
+
447
+ try {
448
+ const url = this.webex.credentials.buildThirdPartyLoginUrl(options);
449
+
450
+ this.webex.getWindow().location = url;
451
+ } catch (err) {
452
+ return Promise.reject(err);
453
+ }
454
+
455
+ return Promise.resolve();
456
+ },
457
+
458
+ /**
459
+ * Handles the third-party (social provider) login callback. Reads the
460
+ * current `window.location`, decodes `state`, validates the CSRF token
461
+ * (`state.csrf_token`), scrubs sensitive parameters from the URL via
462
+ * `_cleanUrl`, and returns the parsed payload.
463
+ *
464
+ * Mirrors `initialize()` in always operating on the live
465
+ * `window.location`
466
+ *
467
+ * `idToken` is single-use: it is parsed out of the URL exactly once and
468
+ * the calling client is expected to exchange it (or discard it)
469
+ * immediately. The returned `state` has `csrf_token` removed.
470
+ *
471
+ * @instance
472
+ * @memberof AuthorizationBrowserFirstParty
473
+ * @returns {{idToken: string|undefined, email: string|undefined,
474
+ * error: string|undefined, state: Object}}
475
+ */
476
+ handleThirdPartyCallback() {
477
+ const location = url.parse(this.webex.getWindow().location.href, true);
478
+
479
+ location.query.state = decodeState(location.query.state || 'e30');
480
+
481
+ this._verifySecurityToken(location.query, {requireMatch: true});
482
+ this._cleanUrl(location);
483
+
484
+ const {
485
+ id_token: idToken,
486
+ email,
487
+ error,
488
+ state: {csrf_token, ...state},
489
+ } = location.query;
490
+
491
+ return {idToken, email, error, state};
492
+ },
493
+
346
494
  /**
347
495
  * Called by {@link WebexCore#logout()}.
348
496
  * Constructs logout URL and (unless suppressed) navigates away to ensure
@@ -497,10 +645,11 @@ const Authorization = WebexPlugin.extend({
497
645
  })
498
646
  .then((res) => {
499
647
  const {user_code, verification_uri, verification_uri_complete} = res.body;
500
- const verificationUriComplete = this._generateQRCodeVerificationUrl(verification_uri_complete);
648
+ const verificationUriComplete =
649
+ this._generateQRCodeVerificationUrl(verification_uri_complete);
501
650
  this.eventEmitter.emit(Events.qRCodeLogin, {
502
651
  eventType: 'getUserCodeSuccess',
503
- userData: {
652
+ userData: {
504
653
  userCode: user_code,
505
654
  verificationUri: verification_uri,
506
655
  verificationUriComplete,
@@ -591,7 +740,7 @@ const Authorization = WebexPlugin.extend({
591
740
  // If polling canceled (id changed), ignore this response
592
741
  if (this.currentPollingId !== this.pollingId) return;
593
742
 
594
- this.eventEmitter.emit(Events.qRCodeLogin, {
743
+ this.eventEmitter.emit(Events.qRCodeLogin, {
595
744
  eventType: 'authorizationSuccess',
596
745
  data: res.body,
597
746
  });
@@ -703,8 +852,9 @@ const Authorization = WebexPlugin.extend({
703
852
  * - HTTP referrer headers to third-party content
704
853
  *
705
854
  * Approach:
706
- * - Remove 'code'.
707
- * - Remove 'state' entirely if only contained csrf_token.
855
+ * - Remove 'code' (OAuth code-grant), 'id_token', and 'email'
856
+ * (third-party callback).
857
+ * - Remove 'state' entirely if it only contained csrf_token.
708
858
  * - Else, re-encode remaining state fields (minus csrf_token).
709
859
  * - Replace current history entry (no page reload).
710
860
  *
@@ -718,12 +868,12 @@ const Authorization = WebexPlugin.extend({
718
868
  location = cloneDeep(location);
719
869
  if (this.webex.getWindow().history && this.webex.getWindow().history.replaceState) {
720
870
  Reflect.deleteProperty(location.query, 'code');
871
+ Reflect.deleteProperty(location.query, 'id_token');
872
+ Reflect.deleteProperty(location.query, 'email');
721
873
  if (isEmpty(omit(location.query.state, 'csrf_token'))) {
722
874
  Reflect.deleteProperty(location.query, 'state');
723
875
  } else {
724
- location.query.state = base64.encode(
725
- JSON.stringify(omit(location.query.state, 'csrf_token'))
726
- );
876
+ location.query.state = encodeState(omit(location.query.state, 'csrf_token'));
727
877
  }
728
878
  location.search = querystring.stringify(location.query);
729
879
  Reflect.deleteProperty(location, 'query');
@@ -792,27 +942,32 @@ const Authorization = WebexPlugin.extend({
792
942
  * - Ensure state + state.csrf_token exist.
793
943
  * - Compare values; throw descriptive errors on mismatch / absence.
794
944
  *
795
- * If no stored token (e.g., user navigated directly), silently returns.
945
+ * If no stored token (e.g., user navigated directly), silently returns
946
+ * unless `options.requireMatch` is `true`, in which case absence of a
947
+ * stored token is treated as a CSRF failure.
796
948
  *
797
949
  * @instance
798
950
  * @memberof AuthorizationBrowserFirstParty
799
951
  * @param {Object} query - Parsed query (location.query)
952
+ * @param {Object} [options]
953
+ * @param {boolean} [options.requireMatch=false] - When true, throws if
954
+ * no stored sessionToken is present.
800
955
  * @private
801
956
  * @returns {void}
802
957
  */
803
- _verifySecurityToken(query) {
958
+ _verifySecurityToken(query, options = {}) {
804
959
  const sessionToken = this.webex.getWindow().sessionStorage.getItem(OAUTH2_CSRF_TOKEN);
805
960
 
806
961
  this.webex.getWindow().sessionStorage.removeItem(OAUTH2_CSRF_TOKEN);
807
962
  if (!sessionToken) {
808
- return;
809
- }
963
+ if (options.requireMatch) {
964
+ throw new Error('CSRF token missing from session storage');
965
+ }
810
966
 
811
- if (!query.state) {
812
- throw new Error(`Expected CSRF token ${sessionToken}, but not found in redirect query`);
967
+ return;
813
968
  }
814
969
 
815
- if (!query.state.csrf_token) {
970
+ if (!query.state?.csrf_token) {
816
971
  throw new Error(`Expected CSRF token ${sessionToken}, but not found in redirect query`);
817
972
  }
818
973
 
package/src/index.js CHANGED
@@ -14,5 +14,5 @@ registerPlugin('authorization', Authorization, {
14
14
  proxies,
15
15
  });
16
16
 
17
- export {default, Events} from './authorization';
17
+ export {default, Events, InitialAuthorizationCodeGrantOutcomes} from './authorization';
18
18
  export {default as config} from './config';