Skip to content

Commit 99c21e8

Browse files
authored
fix(authorization): qrcode - too fast polling (#4007)
1 parent 4666a02 commit 99c21e8

File tree

3 files changed

+275
-137
lines changed

3 files changed

+275
-137
lines changed

packages/@webex/plugin-authorization-browser-first-party/src/authorization.js

+96-36
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ const lodash = require('lodash');
2222
const OAUTH2_CSRF_TOKEN = 'oauth2-csrf-token';
2323
const OAUTH2_CODE_VERIFIER = 'oauth2-code-verifier';
2424

25+
/**
26+
* Authorization plugin events
27+
*/
28+
export const Events = {
29+
/**
30+
* QR code login events
31+
*/
32+
qRCodeLogin: 'qRCodeLogin',
33+
};
34+
2535
/**
2636
* Browser support for OAuth2. Automatically parses the URL query for an
2737
* authorization code
@@ -67,17 +77,49 @@ const Authorization = WebexPlugin.extend({
6777

6878
namespace: 'Credentials',
6979

80+
/**
81+
* EventEmitter for authorization events
82+
* @instance
83+
* @memberof AuthorizationBrowserFirstParty
84+
* @type {EventEmitter}
85+
* @public
86+
*/
87+
eventEmitter: new EventEmitter(),
7088

7189
/**
72-
* Stores the interval ID for QR code polling
90+
* Stores the timer ID for QR code polling
7391
* @instance
7492
* @memberof AuthorizationBrowserFirstParty
7593
* @type {?number}
7694
* @private
7795
*/
78-
pollingRequest: null,
96+
pollingTimer: null,
97+
/**
98+
* Stores the expiration timer ID for QR code polling
99+
* @instance
100+
* @memberof AuthorizationBrowserFirstParty
101+
* @type {?number}
102+
* @private
103+
*/
104+
pollingExpirationTimer: null,
79105

80-
eventEmitter: new EventEmitter(),
106+
/**
107+
* Monotonically increasing id to identify the current polling request
108+
* @instance
109+
* @memberof AuthorizationBrowserFirstParty
110+
* @type {number}
111+
* @private
112+
*/
113+
pollingId: 0,
114+
115+
/**
116+
* Identifier for the current polling request
117+
* @instance
118+
* @memberof AuthorizationBrowserFirstParty
119+
* @type {?number}
120+
* @private
121+
*/
122+
currentPollingId: null,
81123

82124
/**
83125
* Initializer
@@ -260,8 +302,8 @@ const Authorization = WebexPlugin.extend({
260302
* @emits #qRCodeLogin
261303
*/
262304
initQRCodeLogin() {
263-
if (this.pollingRequest) {
264-
this.eventEmitter.emit('qRCodeLogin', {
305+
if (this.pollingTimer) {
306+
this.eventEmitter.emit(Events.qRCodeLogin, {
265307
eventType: 'getUserCodeFailure',
266308
data: {message: 'There is already a polling request'},
267309
});
@@ -285,19 +327,19 @@ const Authorization = WebexPlugin.extend({
285327
})
286328
.then((res) => {
287329
const {user_code, verification_uri, verification_uri_complete} = res.body;
288-
this.eventEmitter.emit('qRCodeLogin', {
330+
this.eventEmitter.emit(Events.qRCodeLogin, {
289331
eventType: 'getUserCodeSuccess',
290332
userData: {
291333
userCode: user_code,
292334
verificationUri: verification_uri,
293335
verificationUriComplete: verification_uri_complete,
294-
}
336+
},
295337
});
296338
// if device authorization success, then start to poll server to check whether the user has completed authorization
297339
this._startQRCodePolling(res.body);
298340
})
299341
.catch((res) => {
300-
this.eventEmitter.emit('qRCodeLogin', {
342+
this.eventEmitter.emit(Events.qRCodeLogin, {
301343
eventType: 'getUserCodeFailure',
302344
data: res.body,
303345
});
@@ -313,30 +355,36 @@ const Authorization = WebexPlugin.extend({
313355
*/
314356
_startQRCodePolling(options = {}) {
315357
if (!options.device_code) {
316-
this.eventEmitter.emit('qRCodeLogin', {
358+
this.eventEmitter.emit(Events.qRCodeLogin, {
317359
eventType: 'authorizationFailure',
318360
data: {message: 'A deviceCode is required'},
319361
});
320362
return;
321363
}
322364

323-
if (this.pollingRequest) {
324-
this.eventEmitter.emit('qRCodeLogin', {
365+
if (this.pollingTimer) {
366+
this.eventEmitter.emit(Events.qRCodeLogin, {
325367
eventType: 'authorizationFailure',
326368
data: {message: 'There is already a polling request'},
327369
});
328370
return;
329371
}
330372

331-
const {device_code: deviceCode, interval = 2, expires_in: expiresIn = 300} = options;
373+
const {device_code: deviceCode, expires_in: expiresIn = 300} = options;
374+
let interval = options.interval ?? 2;
332375

333-
let attempts = 0;
334-
const maxAttempts = expiresIn / interval;
376+
this.pollingExpirationTimer = setTimeout(() => {
377+
this.cancelQRCodePolling(false);
378+
this.eventEmitter.emit(Events.qRCodeLogin, {
379+
eventType: 'authorizationFailure',
380+
data: {message: 'Authorization timed out'},
381+
});
382+
}, expiresIn * 1000);
335383

336-
this.pollingRequest = setInterval(() => {
337-
attempts += 1;
384+
const polling = () => {
385+
this.pollingId += 1;
386+
this.currentPollingId = this.pollingId;
338387

339-
const currentAttempts = attempts;
340388
this.webex
341389
.request({
342390
method: 'POST',
@@ -354,43 +402,50 @@ const Authorization = WebexPlugin.extend({
354402
},
355403
})
356404
.then((res) => {
357-
if (this.pollingRequest === null) return;
405+
// if the pollingId has changed, it means that the polling request has been canceled
406+
if (this.currentPollingId !== this.pollingId) return;
358407

359-
this.eventEmitter.emit('qRCodeLogin', {
408+
this.eventEmitter.emit(Events.qRCodeLogin, {
360409
eventType: 'authorizationSuccess',
361410
data: res.body,
362411
});
363412
this.cancelQRCodePolling();
364413
})
365414
.catch((res) => {
366-
if (this.pollingRequest === null) return;
415+
// if the pollingId has changed, it means that the polling request has been canceled
416+
if (this.currentPollingId !== this.pollingId) return;
367417

368-
if (currentAttempts >= maxAttempts) {
369-
this.eventEmitter.emit('qRCodeLogin', {
370-
eventType: 'authorizationFailure',
371-
data: {message: 'Authorization timed out'}
372-
});
373-
this.cancelQRCodePolling();
418+
// When server sends 400 status code with message 'slow_down', it means that last request happened too soon.
419+
// So, skip one interval and then poll again.
420+
if (res.statusCode === 400 && res.body.message === 'slow_down') {
421+
schedulePolling(interval * 2);
374422
return;
375423
}
424+
376425
// if the statusCode is 428 which means that the authorization request is still pending
377426
// as the end user hasn't yet completed the user-interaction steps. So keep polling.
378427
if (res.statusCode === 428) {
379-
this.eventEmitter.emit('qRCodeLogin', {
428+
this.eventEmitter.emit(Events.qRCodeLogin, {
380429
eventType: 'authorizationPending',
381-
data: res.body
430+
data: res.body,
382431
});
432+
schedulePolling(interval);
383433
return;
384434
}
385435

386436
this.cancelQRCodePolling();
387437

388-
this.eventEmitter.emit('qRCodeLogin', {
438+
this.eventEmitter.emit(Events.qRCodeLogin, {
389439
eventType: 'authorizationFailure',
390-
data: res.body
440+
data: res.body,
391441
});
392442
});
393-
}, interval * 1000);
443+
};
444+
445+
const schedulePolling = (interval) =>
446+
(this.pollingTimer = setTimeout(polling, interval * 1000));
447+
448+
schedulePolling(interval);
394449
},
395450

396451
/**
@@ -399,14 +454,19 @@ const Authorization = WebexPlugin.extend({
399454
* @memberof AuthorizationBrowserFirstParty
400455
* @returns {void}
401456
*/
402-
cancelQRCodePolling() {
403-
if (this.pollingRequest) {
404-
clearInterval(this.pollingRequest);
405-
this.eventEmitter.emit('qRCodeLogin', {
457+
cancelQRCodePolling(withCancelEvent = true) {
458+
if (this.pollingTimer && withCancelEvent) {
459+
this.eventEmitter.emit(Events.qRCodeLogin, {
406460
eventType: 'pollingCanceled',
407461
});
408-
this.pollingRequest = null;
409462
}
463+
464+
this.currentPollingId = null;
465+
466+
clearTimeout(this.pollingExpirationTimer);
467+
this.pollingExpirationTimer = null;
468+
clearTimeout(this.pollingTimer);
469+
this.pollingTimer = null;
410470
},
411471

412472
/**

packages/@webex/plugin-authorization-browser-first-party/src/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ registerPlugin('authorization', Authorization, {
1414
proxies,
1515
});
1616

17-
export {default} from './authorization';
17+
export {default, Events} from './authorization';
1818
export {default as config} from './config';

0 commit comments

Comments
 (0)