@webex/plugin-authorization-browser-first-party 3.12.0-next.5 → 3.12.0-next.50

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 CHANGED
@@ -178,13 +178,44 @@ webex.authorization.initQRCodeLogin();
178
178
 
179
179
  ### Properties
180
180
 
181
- | Property | Type | Description |
182
- | ------------------ | ---------------- | --------------------------------------------------------- |
183
- | `isAuthorizing` | boolean | True while a grant request is in flight |
184
- | `isAuthenticating` | boolean | Alias of `isAuthorizing` |
185
- | `ready` | boolean | Set true after initial redirect/code processing completes |
186
- | `eventEmitter` | EventEmitter | Emits QR/Device login lifecycle events |
187
- | `Events` | enum-like object | Accessible names for event types |
181
+ | Property | Type | Description |
182
+ | -------------------------------------- | ---------------- | --------------------------------------------------------- |
183
+ | `initialAuthorizationCodeGrantOutcome` | string | Retained outcome of the automatic initialization exchange |
184
+ | `isAuthorizing` | boolean | True while a grant request is in flight |
185
+ | `isAuthenticating` | boolean | Alias of `isAuthorizing` |
186
+ | `ready` | boolean | Set true after initial redirect/code processing completes |
187
+ | `eventEmitter` | EventEmitter | Emits QR/Device login lifecycle events |
188
+ | `Events` | enum-like object | Accessible names for event types |
189
+
190
+ #### Initial authorization-code grant outcome
191
+
192
+ ```javascript
193
+ import {InitialAuthorizationCodeGrantOutcomes} from '@webex/plugin-authorization-browser-first-party';
194
+
195
+ const didInitialExchangeSucceed =
196
+ webex.authorization.initialAuthorizationCodeGrantOutcome ===
197
+ InitialAuthorizationCodeGrantOutcomes.success;
198
+ ```
199
+
200
+ `initialAuthorizationCodeGrantOutcome` describes only the automatic
201
+ `requestAuthorizationCodeGrant()` call made while this authorization plugin
202
+ instance initializes. Read it after `ready`:
203
+
204
+ - `not_attempted`: initialization did not invoke the exchange.
205
+ - `success`: the initialization exchange fulfilled.
206
+ - `failure`: the initialization exchange threw or rejected.
207
+
208
+ The value is a historical result retained for the lifetime of the SDK instance
209
+ and is not reset by logout. It does not represent current authorization state
210
+ or credentials hydrated from storage. It also does not track guest or device
211
+ authentication, token refreshes, or authorization-code exchanges requested
212
+ later on the same SDK instance.
213
+
214
+ OAuth redirect errors and CSRF validation failures occur before the exchange,
215
+ so the value remains `not_attempted`; these errors can throw before `ready`
216
+ becomes `true`. A non-redirecting logout does not cancel an initialization
217
+ exchange already in flight. If that exchange later settles, it can still update
218
+ credentials and this retained outcome.
188
219
 
189
220
  ## Security Considerations
190
221
 
@@ -5,19 +5,20 @@ var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequ
5
5
  _Object$defineProperty(exports, "__esModule", {
6
6
  value: true
7
7
  });
8
- exports.default = exports.Events = void 0;
8
+ exports.default = exports.InitialAuthorizationCodeGrantOutcomes = exports.Events = void 0;
9
+ var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/objectWithoutProperties"));
9
10
  var _apply = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/reflect/apply"));
10
11
  var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));
11
12
  var _assign = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/assign"));
12
13
  var _entries = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/entries"));
13
14
  var _deleteProperty = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/reflect/delete-property"));
14
- var _stringify = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/json/stringify"));
15
+ var _from = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/array/from"));
15
16
  var _getOwnPropertyDescriptor = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/get-own-property-descriptor"));
16
17
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/slicedToArray"));
17
18
  var _typeof2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/typeof"));
18
19
  var _applyDecoratedDescriptor2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/applyDecoratedDescriptor"));
19
20
  var _querystring = _interopRequireDefault(require("querystring"));
20
- var _url = _interopRequireDefault(require("url"));
21
+ var _url2 = _interopRequireDefault(require("url"));
21
22
  var _events = require("events");
22
23
  var _common = require("@webex/common");
23
24
  var _webexCore = require("@webex/webex-core");
@@ -25,6 +26,7 @@ var _lodash = require("lodash");
25
26
  var _uuid = _interopRequireDefault(require("uuid"));
26
27
  var _encBase64url = _interopRequireDefault(require("crypto-js/enc-base64url"));
27
28
  var _cryptoJs = _interopRequireDefault(require("crypto-js"));
29
+ var _excluded = ["csrf_token"];
28
30
  var _dec, _dec2, _obj; // @ts-nocheck
29
31
  /* eslint-disable */
30
32
  /*!
@@ -35,9 +37,6 @@ var _dec, _dec2, _obj; // @ts-nocheck
35
37
  * TS checking disabled: file uses legacy decorator syntax inside an object literal
36
38
  * transformed by Babel. Safe to ignore for now.
37
39
  */
38
- // Necessary to require lodash this way in order to stub
39
- // methods in the unit test
40
- var lodash = require('lodash');
41
40
  var OAUTH2_CSRF_TOKEN = 'oauth2-csrf-token';
42
41
  var OAUTH2_CODE_VERIFIER = 'oauth2-code-verifier';
43
42
 
@@ -52,6 +51,18 @@ var Events = exports.Events = {
52
51
  qRCodeLogin: 'qRCodeLogin'
53
52
  };
54
53
 
54
+ /**
55
+ * Terminal outcomes for the automatic authorization-code exchange performed
56
+ * during authorization plugin initialization.
57
+ *
58
+ * @enum {string}
59
+ */
60
+ var InitialAuthorizationCodeGrantOutcomes = exports.InitialAuthorizationCodeGrantOutcomes = {
61
+ failure: 'failure',
62
+ notAttempted: 'not_attempted',
63
+ success: 'success'
64
+ };
65
+
55
66
  /**
56
67
  * Browser support for OAuth2 for first-party (Webex Web Client) usage.
57
68
  *
@@ -82,6 +93,37 @@ var Events = exports.Events = {
82
93
  */
83
94
  var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFlight)('isAuthorizing'), _dec2 = (0, _common.whileInFlight)('isAuthorizing'), _obj = {
84
95
  derived: {
96
+ /**
97
+ * Retains the terminal outcome of the automatic authorization-code exchange
98
+ * performed during this authorization plugin instance's initialization.
99
+ *
100
+ * This historical value does not represent current authorization state,
101
+ * credentials hydrated from storage, guest authentication, or
102
+ * authorization-code exchanges requested later on the same SDK instance. It
103
+ * is not reset by logout. OAuth redirect errors and CSRF validation failures
104
+ * occur before the exchange, so the value remains not_attempted; these errors
105
+ * can throw before ready becomes true.
106
+ *
107
+ * Calling logout({noRedirect: true}) does not cancel an initialization
108
+ * exchange already in flight. If that exchange later settles, it can still
109
+ * update credentials and this retained outcome.
110
+ *
111
+ * Interpret only after authorization readiness:
112
+ * - not_attempted: initialization did not invoke requestAuthorizationCodeGrant()
113
+ * - success: the initialization exchange fulfilled
114
+ * - failure: the initialization exchange threw or rejected
115
+ *
116
+ * @instance
117
+ * @memberof AuthorizationBrowserFirstParty
118
+ * @readonly
119
+ * @type {string}
120
+ */
121
+ initialAuthorizationCodeGrantOutcome: {
122
+ deps: ['_initialAuthorizationCodeGrantOutcome'],
123
+ fn: function fn() {
124
+ return this._initialAuthorizationCodeGrantOutcome;
125
+ }
126
+ },
85
127
  /**
86
128
  * Alias of {@link AuthorizationBrowserFirstParty#isAuthorizing}
87
129
  * @instance
@@ -106,6 +148,14 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
106
148
  default: false,
107
149
  type: 'boolean'
108
150
  },
151
+ /**
152
+ * Internal backing state for the initial authorization-code grant outcome.
153
+ * @private
154
+ */
155
+ _initialAuthorizationCodeGrantOutcome: {
156
+ default: InitialAuthorizationCodeGrantOutcomes.notAttempted,
157
+ type: 'string'
158
+ },
109
159
  /**
110
160
  * Indicates that the plugin has finished any automatic startup
111
161
  * processing (e.g., exchanging a returned authorization code)
@@ -194,7 +244,7 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
194
244
  attrs[_key] = arguments[_key];
195
245
  }
196
246
  var ret = (0, _apply.default)(_webexCore.WebexPlugin.prototype.initialize, this, attrs);
197
- var location = _url.default.parse(this.webex.getWindow().location.href, true);
247
+ var location = _url2.default.parse(this.webex.getWindow().location.href, true);
198
248
 
199
249
  // Check if redirect includes error
200
250
  this._checkForErrors(location);
@@ -208,7 +258,7 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
208
258
 
209
259
  // Decode and parse state object (if present)
210
260
  if (location.query.state) {
211
- location.query.state = JSON.parse(_common.base64.decode(location.query.state));
261
+ location.query.state = (0, _common.decodeState)(location.query.state);
212
262
  } else {
213
263
  location.query.state = {};
214
264
  }
@@ -247,7 +297,10 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
247
297
  code: code,
248
298
  codeVerifier: codeVerifier
249
299
  });
300
+ }).then(function () {
301
+ _this._initialAuthorizationCodeGrantOutcome = InitialAuthorizationCodeGrantOutcomes.success;
250
302
  }).catch(function (error) {
303
+ _this._initialAuthorizationCodeGrantOutcome = InitialAuthorizationCodeGrantOutcomes.failure;
251
304
  _this.logger.warn('authorization: failed initial authorization code grant request', error);
252
305
  }).then(function () {
253
306
  // Mark plugin ready regardless of success/failure of token exchange
@@ -347,6 +400,96 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
347
400
  }
348
401
  return _promise.default.resolve();
349
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: function initiateThirdPartyLogin() {
418
+ var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
419
+ options = (0, _lodash.cloneDeep)(options);
420
+ if (options.state !== undefined && !(0, _lodash.isObject)(options.state)) {
421
+ throw new Error('if specified, `options.state` must be an object');
422
+ }
423
+ options.state = options.state || {};
424
+ options.state.csrf_token = this._generateSecurityToken();
425
+ return this.initiateThirdPartyLoginRedirect(options);
426
+ },
427
+ /**
428
+ * Performs the navigation step of the third-party login flow. Builds the
429
+ * IdBroker URL via `Credentials#buildThirdPartyLoginUrl` and assigns it
430
+ * to `getWindow().location`.
431
+ *
432
+ * Mirrors `initiateAuthorizationCodeGrant` for the `/authorize` flow.
433
+ * Consumers may override this method for custom navigation handling
434
+ * (e.g. postMessage in iframed contexts).
435
+ *
436
+ * @instance
437
+ * @memberof AuthorizationBrowserFirstParty
438
+ * @param {Object} options
439
+ * @param {string} options.oauth2provider
440
+ * @param {string} options.returnURL
441
+ * @returns {Promise<void>}
442
+ */
443
+ initiateThirdPartyLoginRedirect: function initiateThirdPartyLoginRedirect() {
444
+ var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
445
+ this.logger.info('authorization: initiating third-party login redirect');
446
+ try {
447
+ var _url = this.webex.credentials.buildThirdPartyLoginUrl(options);
448
+ this.webex.getWindow().location = _url;
449
+ } catch (err) {
450
+ return _promise.default.reject(err);
451
+ }
452
+ return _promise.default.resolve();
453
+ },
454
+ /**
455
+ * Handles the third-party (social provider) login callback. Reads the
456
+ * current `window.location`, decodes `state`, validates the CSRF token
457
+ * (`state.csrf_token`), scrubs sensitive parameters from the URL via
458
+ * `_cleanUrl`, and returns the parsed payload.
459
+ *
460
+ * Mirrors `initialize()` in always operating on the live
461
+ * `window.location`
462
+ *
463
+ * `idToken` is single-use: it is parsed out of the URL exactly once and
464
+ * the calling client is expected to exchange it (or discard it)
465
+ * immediately. The returned `state` has `csrf_token` removed.
466
+ *
467
+ * @instance
468
+ * @memberof AuthorizationBrowserFirstParty
469
+ * @returns {{idToken: string|undefined, email: string|undefined,
470
+ * error: string|undefined, state: Object}}
471
+ */
472
+ handleThirdPartyCallback: function handleThirdPartyCallback() {
473
+ var location = _url2.default.parse(this.webex.getWindow().location.href, true);
474
+ location.query.state = (0, _common.decodeState)(location.query.state || 'e30');
475
+ this._verifySecurityToken(location.query, {
476
+ requireMatch: true
477
+ });
478
+ this._cleanUrl(location);
479
+ var _location$query = location.query,
480
+ idToken = _location$query.id_token,
481
+ email = _location$query.email,
482
+ error = _location$query.error,
483
+ _location$query$state = _location$query.state,
484
+ csrf_token = _location$query$state.csrf_token,
485
+ state = (0, _objectWithoutProperties2.default)(_location$query$state, _excluded);
486
+ return {
487
+ idToken: idToken,
488
+ email: email,
489
+ error: error,
490
+ state: state
491
+ };
492
+ },
350
493
  /**
351
494
  * Called by {@link WebexCore#logout()}.
352
495
  * Constructs logout URL and (unless suppressed) navigates away to ensure
@@ -696,8 +839,9 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
696
839
  * - HTTP referrer headers to third-party content
697
840
  *
698
841
  * Approach:
699
- * - Remove 'code'.
700
- * - Remove 'state' entirely if only contained csrf_token.
842
+ * - Remove 'code' (OAuth code-grant), 'id_token', and 'email'
843
+ * (third-party callback).
844
+ * - Remove 'state' entirely if it only contained csrf_token.
701
845
  * - Else, re-encode remaining state fields (minus csrf_token).
702
846
  * - Replace current history entry (no page reload).
703
847
  *
@@ -711,14 +855,16 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
711
855
  location = (0, _lodash.cloneDeep)(location);
712
856
  if (this.webex.getWindow().history && this.webex.getWindow().history.replaceState) {
713
857
  (0, _deleteProperty.default)(location.query, 'code');
858
+ (0, _deleteProperty.default)(location.query, 'id_token');
859
+ (0, _deleteProperty.default)(location.query, 'email');
714
860
  if ((0, _lodash.isEmpty)((0, _lodash.omit)(location.query.state, 'csrf_token'))) {
715
861
  (0, _deleteProperty.default)(location.query, 'state');
716
862
  } else {
717
- location.query.state = _common.base64.encode((0, _stringify.default)((0, _lodash.omit)(location.query.state, 'csrf_token')));
863
+ location.query.state = (0, _common.encodeState)((0, _lodash.omit)(location.query.state, 'csrf_token'));
718
864
  }
719
865
  location.search = _querystring.default.stringify(location.query);
720
866
  (0, _deleteProperty.default)(location, 'query');
721
- this.webex.getWindow().history.replaceState({}, null, _url.default.format(location));
867
+ this.webex.getWindow().history.replaceState({}, null, _url2.default.format(location));
722
868
  }
723
869
  },
724
870
  /**
@@ -727,7 +873,8 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
727
873
  * during authorization code exchange; removes it once consumed.
728
874
  *
729
875
  * Implementation details:
730
- * - Creates a 128 character string using base64url safe alphabet.
876
+ * - Creates a 128 character string using a cryptographically secure random
877
+ * source and the base64url safe alphabet.
731
878
  * - Computes SHA256 hash, encodes to base64url (no padding).
732
879
  *
733
880
  * @instance
@@ -740,8 +887,10 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
740
887
 
741
888
  // eslint-disable-next-line no-underscore-dangle
742
889
  var safeCharacterMap = _encBase64url.default._safe_map;
743
- var codeVerifier = lodash.times(128, function () {
744
- return safeCharacterMap[lodash.random(0, safeCharacterMap.length - 1)];
890
+ var randomValues = new Uint8Array(128);
891
+ this.webex.getWindow().crypto.getRandomValues(randomValues);
892
+ var codeVerifier = (0, _from.default)(randomValues, function (randomValue) {
893
+ return safeCharacterMap[randomValue & safeCharacterMap.length - 1];
745
894
  }).join('');
746
895
  var codeChallenge = _cryptoJs.default.SHA256(codeVerifier).toString(_encBase64url.default);
747
896
  this.webex.getWindow().sessionStorage.setItem(OAUTH2_CODE_VERIFIER, codeVerifier);
@@ -773,24 +922,31 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
773
922
  * - Ensure state + state.csrf_token exist.
774
923
  * - Compare values; throw descriptive errors on mismatch / absence.
775
924
  *
776
- * If no stored token (e.g., user navigated directly), silently returns.
925
+ * If no stored token (e.g., user navigated directly), silently returns
926
+ * unless `options.requireMatch` is `true`, in which case absence of a
927
+ * stored token is treated as a CSRF failure.
777
928
  *
778
929
  * @instance
779
930
  * @memberof AuthorizationBrowserFirstParty
780
931
  * @param {Object} query - Parsed query (location.query)
932
+ * @param {Object} [options]
933
+ * @param {boolean} [options.requireMatch=false] - When true, throws if
934
+ * no stored sessionToken is present.
781
935
  * @private
782
936
  * @returns {void}
783
937
  */
784
938
  _verifySecurityToken: function _verifySecurityToken(query) {
939
+ var _query$state;
940
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
785
941
  var sessionToken = this.webex.getWindow().sessionStorage.getItem(OAUTH2_CSRF_TOKEN);
786
942
  this.webex.getWindow().sessionStorage.removeItem(OAUTH2_CSRF_TOKEN);
787
943
  if (!sessionToken) {
944
+ if (options.requireMatch) {
945
+ throw new Error('CSRF token missing from session storage');
946
+ }
788
947
  return;
789
948
  }
790
- if (!query.state) {
791
- throw new Error("Expected CSRF token ".concat(sessionToken, ", but not found in redirect query"));
792
- }
793
- if (!query.state.csrf_token) {
949
+ if (!((_query$state = query.state) !== null && _query$state !== void 0 && _query$state.csrf_token)) {
794
950
  throw new Error("Expected CSRF token ".concat(sessionToken, ", but not found in redirect query"));
795
951
  }
796
952
  var token = query.state.csrf_token;
@@ -798,7 +954,7 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
798
954
  throw new Error("CSRF token ".concat(token, " does not match stored token ").concat(sessionToken));
799
955
  }
800
956
  },
801
- version: "3.12.0-next.5"
957
+ version: "3.12.0-next.50"
802
958
  }, (0, _applyDecoratedDescriptor2.default)(_obj, "initiateAuthorizationCodeGrant", [_dec], (0, _getOwnPropertyDescriptor.default)(_obj, "initiateAuthorizationCodeGrant"), _obj), (0, _applyDecoratedDescriptor2.default)(_obj, "requestAuthorizationCodeGrant", [_dec2, _common.oneFlight], (0, _getOwnPropertyDescriptor.default)(_obj, "requestAuthorizationCodeGrant"), _obj), _obj));
803
959
  var _default = exports.default = Authorization;
804
960
  //# sourceMappingURL=authorization.js.map