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