@webex/plugin-authorization-browser-first-party 3.8.1 → 3.9.0-multiple-llm.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 +216 -26
- package/dist/authorization.js +240 -66
- package/dist/authorization.js.map +1 -1
- package/package.json +13 -13
- package/src/authorization.js +235 -75
package/dist/authorization.js
CHANGED
|
@@ -25,11 +25,16 @@ var _lodash = require("lodash");
|
|
|
25
25
|
var _uuid = _interopRequireDefault(require("uuid"));
|
|
26
26
|
var _encBase64url = _interopRequireDefault(require("crypto-js/enc-base64url"));
|
|
27
27
|
var _cryptoJs = _interopRequireDefault(require("crypto-js"));
|
|
28
|
-
var _dec, _dec2, _obj;
|
|
28
|
+
var _dec, _dec2, _obj; // @ts-nocheck
|
|
29
|
+
/* eslint-disable */
|
|
29
30
|
/*!
|
|
30
31
|
* Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
|
|
31
32
|
*/
|
|
32
33
|
/* eslint camelcase: [0] */
|
|
34
|
+
/**
|
|
35
|
+
* TS checking disabled: file uses legacy decorator syntax inside an object literal
|
|
36
|
+
* transformed by Babel. Safe to ignore for now.
|
|
37
|
+
*/
|
|
33
38
|
// Necessary to require lodash this way in order to stub
|
|
34
39
|
// methods in the unit test
|
|
35
40
|
var lodash = require('lodash');
|
|
@@ -47,11 +52,29 @@ var Events = exports.Events = {
|
|
|
47
52
|
};
|
|
48
53
|
|
|
49
54
|
/**
|
|
50
|
-
* Browser support for OAuth2
|
|
51
|
-
*
|
|
55
|
+
* Browser support for OAuth2 for first-party (Webex Web Client) usage.
|
|
56
|
+
*
|
|
57
|
+
* High-level flow handled by this module:
|
|
58
|
+
* 1. initiateLogin() constructs authorization request (adds CSRF + PKCE).
|
|
59
|
+
* 2. Browser navigates to IdBroker (login).
|
|
60
|
+
* 3. IdBroker redirects back with ?code=... (&state=...).
|
|
61
|
+
* 4. initialize() detects code, validates state/CSRF, cleans URL, optionally
|
|
62
|
+
* pre-fetches a preauth catalog, then exchanges the code via
|
|
63
|
+
* requestAuthorizationCodeGrant().
|
|
64
|
+
* 5. Sets resulting supertoken (access/refresh token bundle) on credentials.
|
|
65
|
+
*
|
|
66
|
+
* Additional supported flow:
|
|
67
|
+
* - Device Authorization (QR Code login):
|
|
68
|
+
* initQRCodeLogin() obtains device + user codes and begins polling
|
|
69
|
+
* _startQRCodePolling() until tokens are issued or timeout/cancel occurs.
|
|
70
|
+
*
|
|
71
|
+
* Security considerations implemented:
|
|
72
|
+
* - CSRF token (state.csrf_token) generation + verification.
|
|
73
|
+
* - PKCE (S256) code verifier + challenge generation and consumption.
|
|
74
|
+
* - URL cleanup after redirect (removes code & CSRF to prevent leakage).
|
|
75
|
+
*
|
|
76
|
+
* Use of this plugin for anything other than the Webex Web Client is discouraged.
|
|
52
77
|
*
|
|
53
|
-
* Use of this plugin for anything other than the Webex Web Client is strongly
|
|
54
|
-
* discouraged and may be broken at any time
|
|
55
78
|
* @class
|
|
56
79
|
* @name AuthorizationBrowserFirstParty
|
|
57
80
|
* @private
|
|
@@ -82,6 +105,10 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
82
105
|
default: false,
|
|
83
106
|
type: 'boolean'
|
|
84
107
|
},
|
|
108
|
+
/**
|
|
109
|
+
* Indicates that the plugin has finished any automatic startup
|
|
110
|
+
* processing (e.g., exchanging a returned authorization code)
|
|
111
|
+
*/
|
|
85
112
|
ready: {
|
|
86
113
|
default: false,
|
|
87
114
|
type: 'boolean'
|
|
@@ -89,7 +116,7 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
89
116
|
},
|
|
90
117
|
namespace: 'Credentials',
|
|
91
118
|
/**
|
|
92
|
-
* EventEmitter for authorization events
|
|
119
|
+
* EventEmitter for authorization events such as QR code login progress
|
|
93
120
|
* @instance
|
|
94
121
|
* @memberof AuthorizationBrowserFirstParty
|
|
95
122
|
* @type {EventEmitter}
|
|
@@ -97,7 +124,7 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
97
124
|
*/
|
|
98
125
|
eventEmitter: new _events.EventEmitter(),
|
|
99
126
|
/**
|
|
100
|
-
* Stores the timer ID for QR code polling
|
|
127
|
+
* Stores the timer ID for QR code polling (device authorization)
|
|
101
128
|
* @instance
|
|
102
129
|
* @memberof AuthorizationBrowserFirstParty
|
|
103
130
|
* @type {?number}
|
|
@@ -105,7 +132,7 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
105
132
|
*/
|
|
106
133
|
pollingTimer: null,
|
|
107
134
|
/**
|
|
108
|
-
* Stores the expiration timer ID for QR code polling
|
|
135
|
+
* Stores the expiration timer ID for QR code polling (overall timeout)
|
|
109
136
|
* @instance
|
|
110
137
|
* @memberof AuthorizationBrowserFirstParty
|
|
111
138
|
* @type {?number}
|
|
@@ -113,7 +140,8 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
113
140
|
*/
|
|
114
141
|
pollingExpirationTimer: null,
|
|
115
142
|
/**
|
|
116
|
-
* Monotonically increasing id to identify the current polling request
|
|
143
|
+
* Monotonically increasing id to identify the current polling request.
|
|
144
|
+
* Used to safely ignore late poll responses after a cancel/reset.
|
|
117
145
|
* @instance
|
|
118
146
|
* @memberof AuthorizationBrowserFirstParty
|
|
119
147
|
* @type {number}
|
|
@@ -121,7 +149,7 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
121
149
|
*/
|
|
122
150
|
pollingId: 0,
|
|
123
151
|
/**
|
|
124
|
-
* Identifier for the current polling request
|
|
152
|
+
* Identifier for the current polling request (snapshot of pollingId)
|
|
125
153
|
* @instance
|
|
126
154
|
* @memberof AuthorizationBrowserFirstParty
|
|
127
155
|
* @type {?number}
|
|
@@ -129,11 +157,34 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
129
157
|
*/
|
|
130
158
|
currentPollingId: null,
|
|
131
159
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
160
|
+
* Auto executes during Webex.init() – you do NOT call this yourself.
|
|
161
|
+
*
|
|
162
|
+
* Purpose: Seamless "redirect completion" of the OAuth Authorization Code (+ PKCE) flow.
|
|
163
|
+
*
|
|
164
|
+
* Simple summary:
|
|
165
|
+
* - You call initiateLogin() which redirects user to IdBroker.
|
|
166
|
+
* - User signs in; IdBroker redirects back to your redirect_uri with ?code=... (&state=...).
|
|
167
|
+
* - During SDK startup this initialize() runs automatically, sees the code, and
|
|
168
|
+
* silently finishes the login (validates state/CSRF + PKCE, scrubs URL, exchanges code).
|
|
169
|
+
* - When done, webex.credentials.supertoken holds access+refresh and ready=true.
|
|
170
|
+
*
|
|
171
|
+
* Step-by-step:
|
|
172
|
+
* 1. Inspect current window.location for ?code= (& state=).
|
|
173
|
+
* 2. If no code: set ready=true immediately (nothing to complete).
|
|
174
|
+
* 3. If code present:
|
|
175
|
+
* - Decode base64 state JSON.
|
|
176
|
+
* - Verify CSRF token matches sessionStorage value.
|
|
177
|
+
* - Retrieve then delete PKCE code_verifier (single use).
|
|
178
|
+
* - Optionally derive preauth hint (emailhash in state OR orgId parsed from code).
|
|
179
|
+
* - Clean the URL (history.replaceState) to remove code & csrf token data.
|
|
180
|
+
* - nextTick:
|
|
181
|
+
* a. Best‑effort preauth catalog fetch (non-blocking).
|
|
182
|
+
* b. Exchange authorization code (with code_verifier if any) for supertoken
|
|
183
|
+
* and store on webex.credentials.
|
|
184
|
+
* 4. Set ready=true after the async sequence finishes (or immediately if step 2).
|
|
185
|
+
*
|
|
186
|
+
* Result: If the redirect included a valid code the token exchange is completed
|
|
187
|
+
* automatically—no extra API call needed after Webex.init().
|
|
137
188
|
*/
|
|
138
189
|
// eslint-disable-next-line complexity
|
|
139
190
|
initialize: function initialize() {
|
|
@@ -143,23 +194,37 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
143
194
|
}
|
|
144
195
|
var ret = (0, _apply.default)(_webexCore.WebexPlugin.prototype.initialize, this, attrs);
|
|
145
196
|
var location = _url.default.parse(this.webex.getWindow().location.href, true);
|
|
197
|
+
|
|
198
|
+
// Check if redirect includes error
|
|
146
199
|
this._checkForErrors(location);
|
|
147
200
|
var code = location.query.code;
|
|
201
|
+
|
|
202
|
+
// If no authorization code returned, nothing to do
|
|
148
203
|
if (!code) {
|
|
149
204
|
this.ready = true;
|
|
150
205
|
return ret;
|
|
151
206
|
}
|
|
207
|
+
|
|
208
|
+
// Decode and parse state object (if present)
|
|
152
209
|
if (location.query.state) {
|
|
153
210
|
location.query.state = JSON.parse(_common.base64.decode(location.query.state));
|
|
154
211
|
} else {
|
|
155
212
|
location.query.state = {};
|
|
156
213
|
}
|
|
214
|
+
|
|
215
|
+
// Retrieve PKCE code verifier (if a PKCE flow was initiated)
|
|
157
216
|
var codeVerifier = this.webex.getWindow().sessionStorage.getItem(OAUTH2_CODE_VERIFIER);
|
|
217
|
+
// Immediately remove code verifier to minimize exposure
|
|
158
218
|
this.webex.getWindow().sessionStorage.removeItem(OAUTH2_CODE_VERIFIER);
|
|
159
219
|
var emailhash = location.query.state.emailhash;
|
|
220
|
+
|
|
221
|
+
// Validate CSRF token included in state
|
|
160
222
|
this._verifySecurityToken(location.query);
|
|
223
|
+
// Remove code + CSRF token remnants from URL (history replace)
|
|
161
224
|
this._cleanUrl(location);
|
|
162
225
|
var preauthCatalogParams;
|
|
226
|
+
|
|
227
|
+
// Attempt to extract orgId from structured authorization code (if present)
|
|
163
228
|
var orgId = this._extractOrgIdFromCode(code);
|
|
164
229
|
if (emailhash) {
|
|
165
230
|
preauthCatalogParams = {
|
|
@@ -171,11 +236,12 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
171
236
|
};
|
|
172
237
|
}
|
|
173
238
|
|
|
174
|
-
//
|
|
239
|
+
// Defer token exchange until next tick in case credentials plugin not ready yet
|
|
175
240
|
process.nextTick(function () {
|
|
176
241
|
_this.webex.internal.services.collectPreauthCatalog(preauthCatalogParams).catch(function () {
|
|
177
242
|
return _promise.default.resolve();
|
|
178
|
-
})
|
|
243
|
+
}) // Non-fatal if catalog collection fails
|
|
244
|
+
.then(function () {
|
|
179
245
|
return _this.requestAuthorizationCodeGrant({
|
|
180
246
|
code: code,
|
|
181
247
|
codeVerifier: codeVerifier
|
|
@@ -183,13 +249,24 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
183
249
|
}).catch(function (error) {
|
|
184
250
|
_this.logger.warn('authorization: failed initial authorization code grant request', error);
|
|
185
251
|
}).then(function () {
|
|
252
|
+
// Mark plugin ready regardless of success/failure of token exchange
|
|
186
253
|
_this.ready = true;
|
|
187
254
|
});
|
|
188
255
|
});
|
|
189
256
|
return ret;
|
|
190
257
|
},
|
|
191
258
|
/**
|
|
192
|
-
* Kicks off an
|
|
259
|
+
* Kicks off an OAuth authorization code flow (first party).
|
|
260
|
+
*
|
|
261
|
+
* Adds security + PKCE properties:
|
|
262
|
+
* - SHA256(email) (emailHash & emailhash) for preauth and redirect flows
|
|
263
|
+
* - state.csrf_token for CSRF protection
|
|
264
|
+
* - PKCE code_challenge (S256)
|
|
265
|
+
*
|
|
266
|
+
* NOTE: This does not itself perform the redirect; it calls
|
|
267
|
+
* initiateAuthorizationCodeGrant() which changes window location or opens
|
|
268
|
+
* a separate window as configured.
|
|
269
|
+
*
|
|
193
270
|
* @instance
|
|
194
271
|
* @memberof AuthorizationBrowserFirstParty
|
|
195
272
|
* @param {Object} options
|
|
@@ -198,25 +275,38 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
198
275
|
initiateLogin: function initiateLogin() {
|
|
199
276
|
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
200
277
|
options = (0, _lodash.cloneDeep)(options);
|
|
278
|
+
|
|
279
|
+
// Optionally compute heuristic email hash for preauth usage
|
|
201
280
|
if (options.email) {
|
|
202
281
|
options.emailHash = _cryptoJs.default.SHA256(options.email).toString();
|
|
203
282
|
}
|
|
204
|
-
delete options.email;
|
|
283
|
+
delete options.email; // Ensure raw email not propagated further
|
|
284
|
+
|
|
205
285
|
options.state = options.state || {};
|
|
286
|
+
// Embed CSRF token
|
|
206
287
|
options.state.csrf_token = this._generateSecurityToken();
|
|
207
|
-
//
|
|
288
|
+
// Provide email hash in lower-case key used by catalog service
|
|
289
|
+
// (Note: catalog uses emailhash and redirectCI uses emailHash)
|
|
208
290
|
options.state.emailhash = options.emailHash;
|
|
291
|
+
|
|
292
|
+
// PKCE - produce code_challenge (S256) and persist code_verifier
|
|
209
293
|
options.code_challenge = this._generateCodeChallenge();
|
|
210
294
|
options.code_challenge_method = 'S256';
|
|
211
295
|
return this.initiateAuthorizationCodeGrant(options);
|
|
212
296
|
},
|
|
213
297
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
298
|
+
* Performs the navigation step of the Authorization Code flow.
|
|
299
|
+
* Builds login URL and either:
|
|
300
|
+
* - Replaces current window location (default), or
|
|
301
|
+
* - Opens a separate window (popup) if options.separateWindow supplied.
|
|
302
|
+
*
|
|
303
|
+
* Decorated with whileInFlight('isAuthorizing') to set isAuthorizing=true
|
|
304
|
+
* during execution to prevent concurrent overlapping attempts.
|
|
305
|
+
*
|
|
216
306
|
* @instance
|
|
217
307
|
* @memberof AuthorizationBrowserFirstParty
|
|
218
|
-
* @param {Object} options
|
|
219
|
-
* @returns {Promise}
|
|
308
|
+
* @param {Object} options - Already augmented with state + PKCE info
|
|
309
|
+
* @returns {Promise<void>}
|
|
220
310
|
*/
|
|
221
311
|
initiateAuthorizationCodeGrant: function initiateAuthorizationCodeGrant(options) {
|
|
222
312
|
this.logger.info('authorization: initiating authorization code grant flow');
|
|
@@ -224,15 +314,12 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
224
314
|
response_type: 'code'
|
|
225
315
|
}, options));
|
|
226
316
|
if (options !== null && options !== void 0 && options.separateWindow) {
|
|
227
|
-
//
|
|
317
|
+
// If a separate popup window is requested, combine user supplied window features
|
|
228
318
|
var defaultWindowSettings = {
|
|
229
319
|
width: 600,
|
|
230
320
|
height: 800
|
|
231
321
|
};
|
|
232
|
-
|
|
233
|
-
// Merge user provided settings with defaults
|
|
234
322
|
var windowSettings = (0, _assign.default)(defaultWindowSettings, (0, _typeof2.default)(options.separateWindow) === 'object' ? options.separateWindow : {});
|
|
235
|
-
// Convert settings object to window.open features string
|
|
236
323
|
var windowFeatures = (0, _entries.default)(windowSettings).map(function (_ref) {
|
|
237
324
|
var _ref2 = (0, _slicedToArray2.default)(_ref, 2),
|
|
238
325
|
key = _ref2[0],
|
|
@@ -241,18 +328,21 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
241
328
|
}).join(',');
|
|
242
329
|
this.webex.getWindow().open(loginUrl, '_blank', windowFeatures);
|
|
243
330
|
} else {
|
|
244
|
-
//
|
|
331
|
+
// Normal (in-tab) redirect
|
|
245
332
|
this.webex.getWindow().location = loginUrl;
|
|
246
333
|
}
|
|
247
334
|
return _promise.default.resolve();
|
|
248
335
|
},
|
|
249
336
|
/**
|
|
250
|
-
* Called by {@link WebexCore#logout()}.
|
|
337
|
+
* Called by {@link WebexCore#logout()}.
|
|
338
|
+
* Constructs logout URL and (unless suppressed) navigates away to ensure
|
|
339
|
+
* server-side session termination.
|
|
340
|
+
*
|
|
251
341
|
* @instance
|
|
252
342
|
* @memberof AuthorizationBrowserFirstParty
|
|
253
343
|
* @param {Object} options
|
|
254
344
|
* @param {boolean} options.noRedirect if true, does not redirect
|
|
255
|
-
* @returns {Promise}
|
|
345
|
+
* @returns {Promise<void>}
|
|
256
346
|
*/
|
|
257
347
|
logout: function logout() {
|
|
258
348
|
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
@@ -261,11 +351,23 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
261
351
|
}
|
|
262
352
|
},
|
|
263
353
|
/**
|
|
264
|
-
* Exchanges an authorization code for an access token
|
|
354
|
+
* Exchanges an authorization code for an access (super) token bundle.
|
|
355
|
+
*
|
|
356
|
+
* Decorators:
|
|
357
|
+
* - @whileInFlight('isAuthorizing'): prevents overlapping exchanges.
|
|
358
|
+
* - @oneFlight: collapses simultaneous calls into one network request.
|
|
359
|
+
*
|
|
360
|
+
* Includes PKCE code_verifier if present from earlier login initiation.
|
|
361
|
+
*
|
|
362
|
+
* Error Handling:
|
|
363
|
+
* - Non-400 responses are propagated.
|
|
364
|
+
* - 400 responses map to OAuth-specific grantErrors.
|
|
365
|
+
*
|
|
265
366
|
* @instance
|
|
266
367
|
* @memberof AuthorizationBrowserFirstParty
|
|
267
368
|
* @param {Object} options
|
|
268
|
-
* @param {
|
|
369
|
+
* @param {string} options.code - Authorization code from redirect
|
|
370
|
+
* @param {string} [options.codeVerifier] - PKCE code verifier if used
|
|
269
371
|
* @returns {Promise}
|
|
270
372
|
*/
|
|
271
373
|
requestAuthorizationCodeGrant: function requestAuthorizationCodeGrant() {
|
|
@@ -279,8 +381,9 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
279
381
|
grant_type: 'authorization_code',
|
|
280
382
|
redirect_uri: this.config.redirect_uri,
|
|
281
383
|
code: options.code,
|
|
282
|
-
self_contained_token: true
|
|
384
|
+
self_contained_token: true // Request combined access/refresh response
|
|
283
385
|
};
|
|
386
|
+
|
|
284
387
|
if (options.codeVerifier) {
|
|
285
388
|
form.code_verifier = options.codeVerifier;
|
|
286
389
|
}
|
|
@@ -293,8 +396,9 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
293
396
|
pass: this.config.client_secret,
|
|
294
397
|
sendImmediately: true
|
|
295
398
|
},
|
|
296
|
-
shouldRefreshAccessToken: false
|
|
399
|
+
shouldRefreshAccessToken: false // This is the token acquisition call itself
|
|
297
400
|
}).then(function (res) {
|
|
401
|
+
// Store supertoken into credentials (includes refresh token)
|
|
298
402
|
_this2.webex.credentials.set({
|
|
299
403
|
supertoken: res.body
|
|
300
404
|
});
|
|
@@ -302,16 +406,22 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
302
406
|
if (res.statusCode !== 400) {
|
|
303
407
|
return _promise.default.reject(res);
|
|
304
408
|
}
|
|
409
|
+
|
|
410
|
+
// Map standard OAuth error to strongly typed error class
|
|
305
411
|
var ErrorConstructor = _webexCore.grantErrors.select(res.body.error);
|
|
306
412
|
return _promise.default.reject(new ErrorConstructor(res._res || res));
|
|
307
413
|
});
|
|
308
414
|
},
|
|
309
415
|
/**
|
|
310
|
-
* Generate a QR code URL
|
|
416
|
+
* Generate a QR code verification URL for device authorization flow.
|
|
417
|
+
* When a user scans the QR code with a mobile device, this deep-links into
|
|
418
|
+
* Webex (web) to continue login, including passing along userCode and the
|
|
419
|
+
* helper service base URL.
|
|
420
|
+
*
|
|
311
421
|
* @instance
|
|
312
422
|
* @memberof AuthorizationBrowserFirstParty
|
|
313
|
-
* @param {String} verificationUrl
|
|
314
|
-
* @returns {String}
|
|
423
|
+
* @param {String} verificationUrl - Original verification URI (complete)
|
|
424
|
+
* @returns {String} Possibly rewritten verification URL
|
|
315
425
|
*/
|
|
316
426
|
_generateQRCodeVerificationUrl: function _generateQRCodeVerificationUrl(verificationUrl) {
|
|
317
427
|
var baseUrl = 'https://web.webex.com/deviceAuth';
|
|
@@ -329,7 +439,15 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
329
439
|
}
|
|
330
440
|
},
|
|
331
441
|
/**
|
|
332
|
-
*
|
|
442
|
+
* Initiates Device Authorization (QR Code) flow.
|
|
443
|
+
*
|
|
444
|
+
* Steps:
|
|
445
|
+
* 1. Obtain device_code, user_code, verification URLs from oauth-helper.
|
|
446
|
+
* 2. Emit getUserCodeSuccess (provides data for generating QR code).
|
|
447
|
+
* 3. Start polling token endpoint with device_code.
|
|
448
|
+
*
|
|
449
|
+
* Emits qRCodeLogin events for UI to react (success, failure, pending, etc.).
|
|
450
|
+
*
|
|
333
451
|
* @instance
|
|
334
452
|
* @memberof AuthorizationBrowserFirstParty
|
|
335
453
|
* @emits #qRCodeLogin
|
|
@@ -337,6 +455,7 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
337
455
|
initQRCodeLogin: function initQRCodeLogin() {
|
|
338
456
|
var _this3 = this;
|
|
339
457
|
if (this.pollingTimer) {
|
|
458
|
+
// Prevent concurrent device authorization attempts
|
|
340
459
|
this.eventEmitter.emit(Events.qRCodeLogin, {
|
|
341
460
|
eventType: 'getUserCodeFailure',
|
|
342
461
|
data: {
|
|
@@ -372,7 +491,7 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
372
491
|
verificationUriComplete: verificationUriComplete
|
|
373
492
|
}
|
|
374
493
|
});
|
|
375
|
-
//
|
|
494
|
+
// Begin polling for authorization completion
|
|
376
495
|
_this3._startQRCodePolling(res.body);
|
|
377
496
|
}).catch(function (res) {
|
|
378
497
|
_this3.eventEmitter.emit(Events.qRCodeLogin, {
|
|
@@ -382,10 +501,21 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
382
501
|
});
|
|
383
502
|
},
|
|
384
503
|
/**
|
|
385
|
-
*
|
|
504
|
+
* Poll the device token endpoint until user authorizes, an error occurs,
|
|
505
|
+
* or timeout happens.
|
|
506
|
+
*
|
|
507
|
+
* Polling behavior:
|
|
508
|
+
* - Interval provided by server (default 2s). 'slow_down' doubles interval once.
|
|
509
|
+
* - 428 status => pending (continue).
|
|
510
|
+
* - Success => set credentials + emit authorizationSuccess + stop polling.
|
|
511
|
+
* - Any other error => emit authorizationFailure + stop polling.
|
|
512
|
+
*
|
|
513
|
+
* Cancellation:
|
|
514
|
+
* - cancelQRCodePolling() resets timers and polling ids so late responses are ignored.
|
|
515
|
+
*
|
|
386
516
|
* @instance
|
|
387
517
|
* @memberof AuthorizationBrowserFirstParty
|
|
388
|
-
* @param {Object} options
|
|
518
|
+
* @param {Object} options - Must include device_code, may include interval/expires_in
|
|
389
519
|
* @emits #qRCodeLogin
|
|
390
520
|
*/
|
|
391
521
|
_startQRCodePolling: function _startQRCodePolling() {
|
|
@@ -402,6 +532,7 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
402
532
|
return;
|
|
403
533
|
}
|
|
404
534
|
if (this.pollingTimer) {
|
|
535
|
+
// Already polling; avoid starting a duplicate cycle
|
|
405
536
|
this.eventEmitter.emit(Events.qRCodeLogin, {
|
|
406
537
|
eventType: 'authorizationFailure',
|
|
407
538
|
data: {
|
|
@@ -413,7 +544,10 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
413
544
|
var deviceCode = options.device_code,
|
|
414
545
|
_options$expires_in = options.expires_in,
|
|
415
546
|
expiresIn = _options$expires_in === void 0 ? 300 : _options$expires_in;
|
|
547
|
+
// Server recommended polling interval (seconds)
|
|
416
548
|
var interval = (_options$interval = options.interval) !== null && _options$interval !== void 0 ? _options$interval : 2;
|
|
549
|
+
|
|
550
|
+
// Global timeout for entire device authorization attempt
|
|
417
551
|
this.pollingExpirationTimer = setTimeout(function () {
|
|
418
552
|
_this4.cancelQRCodePolling(false);
|
|
419
553
|
_this4.eventEmitter.emit(Events.qRCodeLogin, {
|
|
@@ -424,6 +558,7 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
424
558
|
});
|
|
425
559
|
}, expiresIn * 1000);
|
|
426
560
|
var polling = function polling() {
|
|
561
|
+
// Increment id so any previous poll loops can be invalidated
|
|
427
562
|
_this4.pollingId += 1;
|
|
428
563
|
_this4.currentPollingId = _this4.pollingId;
|
|
429
564
|
_this4.webex.request({
|
|
@@ -441,7 +576,7 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
441
576
|
sendImmediately: true
|
|
442
577
|
}
|
|
443
578
|
}).then(function (res) {
|
|
444
|
-
//
|
|
579
|
+
// If polling canceled (id changed), ignore this response
|
|
445
580
|
if (_this4.currentPollingId !== _this4.pollingId) return;
|
|
446
581
|
_this4.eventEmitter.emit(Events.qRCodeLogin, {
|
|
447
582
|
eventType: 'authorizationSuccess',
|
|
@@ -452,18 +587,15 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
452
587
|
});
|
|
453
588
|
_this4.cancelQRCodePolling();
|
|
454
589
|
}).catch(function (res) {
|
|
455
|
-
// if the pollingId has changed, it means that the polling request has been canceled
|
|
456
590
|
if (_this4.currentPollingId !== _this4.pollingId) return;
|
|
457
591
|
|
|
458
|
-
//
|
|
459
|
-
// So, skip one interval and then poll again.
|
|
592
|
+
// Backoff signal from server; increase interval just once for next cycle
|
|
460
593
|
if (res.statusCode === 400 && res.body.message === 'slow_down') {
|
|
461
594
|
schedulePolling(interval * 2);
|
|
462
595
|
return;
|
|
463
596
|
}
|
|
464
597
|
|
|
465
|
-
//
|
|
466
|
-
// as the end user hasn't yet completed the user-interaction steps. So keep polling.
|
|
598
|
+
// Pending: keep polling
|
|
467
599
|
if (res.statusCode === 428) {
|
|
468
600
|
_this4.eventEmitter.emit(Events.qRCodeLogin, {
|
|
469
601
|
eventType: 'authorizationPending',
|
|
@@ -472,6 +604,8 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
472
604
|
schedulePolling(interval);
|
|
473
605
|
return;
|
|
474
606
|
}
|
|
607
|
+
|
|
608
|
+
// Terminal error
|
|
475
609
|
_this4.cancelQRCodePolling();
|
|
476
610
|
_this4.eventEmitter.emit(Events.qRCodeLogin, {
|
|
477
611
|
eventType: 'authorizationFailure',
|
|
@@ -479,13 +613,17 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
479
613
|
});
|
|
480
614
|
});
|
|
481
615
|
};
|
|
616
|
+
|
|
617
|
+
// Schedules next poll invocation
|
|
482
618
|
var schedulePolling = function schedulePolling(interval) {
|
|
483
619
|
return _this4.pollingTimer = setTimeout(polling, interval * 1000);
|
|
484
620
|
};
|
|
485
621
|
schedulePolling(interval);
|
|
486
622
|
},
|
|
487
623
|
/**
|
|
488
|
-
*
|
|
624
|
+
* Cancel active device authorization polling loop.
|
|
625
|
+
*
|
|
626
|
+
* @param {boolean} withCancelEvent emit a pollingCanceled event (default true)
|
|
489
627
|
* @instance
|
|
490
628
|
* @memberof AuthorizationBrowserFirstParty
|
|
491
629
|
* @returns {void}
|
|
@@ -504,25 +642,31 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
504
642
|
this.pollingTimer = null;
|
|
505
643
|
},
|
|
506
644
|
/**
|
|
507
|
-
* Extracts the orgId from the returned code from idbroker
|
|
508
|
-
*
|
|
509
|
-
*
|
|
645
|
+
* Extracts the orgId from the returned code from idbroker.
|
|
646
|
+
*
|
|
647
|
+
* Certain authorization codes encode organization info in a structured
|
|
648
|
+
* underscore-delimited format. This method parses out the 3rd segment.
|
|
649
|
+
*
|
|
650
|
+
* For undocumented formats or unexpected code shapes, returns undefined.
|
|
651
|
+
*
|
|
510
652
|
* @instance
|
|
511
653
|
* @memberof AuthorizationBrowserFirstParty
|
|
512
654
|
* @param {String} code
|
|
513
655
|
* @private
|
|
514
|
-
* @returns {String}
|
|
656
|
+
* @returns {String|undefined}
|
|
515
657
|
*/
|
|
516
658
|
_extractOrgIdFromCode: function _extractOrgIdFromCode(code) {
|
|
517
659
|
return (code === null || code === void 0 ? void 0 : code.split('_')[2]) || undefined;
|
|
518
660
|
},
|
|
519
661
|
/**
|
|
520
|
-
* Checks if the result of the login redirect contains an error
|
|
662
|
+
* Checks if the result of the login redirect contains an OAuth error.
|
|
663
|
+
* Throws a mapped grant error if encountered.
|
|
664
|
+
*
|
|
521
665
|
* @instance
|
|
522
666
|
* @memberof AuthorizationBrowserFirstParty
|
|
523
667
|
* @param {Object} location
|
|
524
668
|
* @private
|
|
525
|
-
* @returns {
|
|
669
|
+
* @returns {void}
|
|
526
670
|
*/
|
|
527
671
|
_checkForErrors: function _checkForErrors(location) {
|
|
528
672
|
var query = location.query;
|
|
@@ -532,12 +676,23 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
532
676
|
}
|
|
533
677
|
},
|
|
534
678
|
/**
|
|
535
|
-
* Removes no-longer needed values from the
|
|
679
|
+
* Removes no-longer needed values from the URL (authorization code, CSRF token).
|
|
680
|
+
* This is important to avoid leaking sensitive parameters via:
|
|
681
|
+
* - Browser history
|
|
682
|
+
* - Copy/paste of URL
|
|
683
|
+
* - HTTP referrer headers to third-party content
|
|
684
|
+
*
|
|
685
|
+
* Approach:
|
|
686
|
+
* - Remove 'code'.
|
|
687
|
+
* - Remove 'state' entirely if only contained csrf_token.
|
|
688
|
+
* - Else, re-encode remaining state fields (minus csrf_token).
|
|
689
|
+
* - Replace current history entry (no page reload).
|
|
690
|
+
*
|
|
536
691
|
* @instance
|
|
537
692
|
* @memberof AuthorizationBrowserFirstParty
|
|
538
693
|
* @param {Object} location
|
|
539
694
|
* @private
|
|
540
|
-
* @returns {
|
|
695
|
+
* @returns {void}
|
|
541
696
|
*/
|
|
542
697
|
_cleanUrl: function _cleanUrl(location) {
|
|
543
698
|
location = (0, _lodash.cloneDeep)(location);
|
|
@@ -554,11 +709,18 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
554
709
|
}
|
|
555
710
|
},
|
|
556
711
|
/**
|
|
557
|
-
* Generates PKCE
|
|
712
|
+
* Generates a PKCE (RFC 7636) code verifier and corresponding S256 code challenge.
|
|
713
|
+
* Persists the verifier in sessionStorage (single-use) for later retrieval
|
|
714
|
+
* during authorization code exchange; removes it once consumed.
|
|
715
|
+
*
|
|
716
|
+
* Implementation details:
|
|
717
|
+
* - Creates a 128 character string using base64url safe alphabet.
|
|
718
|
+
* - Computes SHA256 hash, encodes to base64url (no padding).
|
|
719
|
+
*
|
|
558
720
|
* @instance
|
|
559
721
|
* @memberof AuthorizationBrowserFirstParty
|
|
560
722
|
* @private
|
|
561
|
-
* @returns {string}
|
|
723
|
+
* @returns {string} code_challenge
|
|
562
724
|
*/
|
|
563
725
|
_generateCodeChallenge: function _generateCodeChallenge() {
|
|
564
726
|
this.logger.info('authorization: generating PKCE code challenge');
|
|
@@ -573,11 +735,15 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
573
735
|
return codeChallenge;
|
|
574
736
|
},
|
|
575
737
|
/**
|
|
576
|
-
* Generates a CSRF token and
|
|
738
|
+
* Generates a CSRF token and stores it in sessionStorage.
|
|
739
|
+
* Token is embedded in 'state' and validated upon redirect return.
|
|
740
|
+
*
|
|
741
|
+
* Uses UUID v4 for randomness.
|
|
742
|
+
*
|
|
577
743
|
* @instance
|
|
578
744
|
* @memberof AuthorizationBrowserFirstParty
|
|
579
745
|
* @private
|
|
580
|
-
* @returns {
|
|
746
|
+
* @returns {string} token
|
|
581
747
|
*/
|
|
582
748
|
_generateSecurityToken: function _generateSecurityToken() {
|
|
583
749
|
this.logger.info('authorization: generating csrf token');
|
|
@@ -586,13 +752,21 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
586
752
|
return token;
|
|
587
753
|
},
|
|
588
754
|
/**
|
|
589
|
-
*
|
|
590
|
-
* in
|
|
755
|
+
* Verifies that the CSRF token returned in the 'state' matches the one
|
|
756
|
+
* previously stored in sessionStorage.
|
|
757
|
+
*
|
|
758
|
+
* Steps:
|
|
759
|
+
* - Retrieve and immediately remove stored token (one-time use).
|
|
760
|
+
* - Ensure state + state.csrf_token exist.
|
|
761
|
+
* - Compare values; throw descriptive errors on mismatch / absence.
|
|
762
|
+
*
|
|
763
|
+
* If no stored token (e.g., user navigated directly), silently returns.
|
|
764
|
+
*
|
|
591
765
|
* @instance
|
|
592
766
|
* @memberof AuthorizationBrowserFirstParty
|
|
593
|
-
* @param {Object} query
|
|
767
|
+
* @param {Object} query - Parsed query (location.query)
|
|
594
768
|
* @private
|
|
595
|
-
* @returns {
|
|
769
|
+
* @returns {void}
|
|
596
770
|
*/
|
|
597
771
|
_verifySecurityToken: function _verifySecurityToken(query) {
|
|
598
772
|
var sessionToken = this.webex.getWindow().sessionStorage.getItem(OAUTH2_CSRF_TOKEN);
|
|
@@ -611,7 +785,7 @@ var Authorization = _webexCore.WebexPlugin.extend((_dec = (0, _common.whileInFli
|
|
|
611
785
|
throw new Error("CSRF token ".concat(token, " does not match stored token ").concat(sessionToken));
|
|
612
786
|
}
|
|
613
787
|
},
|
|
614
|
-
version: "3.
|
|
788
|
+
version: "3.9.0-multiple-llm.1"
|
|
615
789
|
}, ((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)));
|
|
616
790
|
var _default = exports.default = Authorization;
|
|
617
791
|
//# sourceMappingURL=authorization.js.map
|