"Fossies" - the Fresh Open Source Software Archive

Member "vscode-1.49.1/extensions/microsoft-authentication/src/AADHelper.ts" (16 Sep 2020, 20212 Bytes) of package /linux/misc/vscode-1.49.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) TypeScript source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "AADHelper.ts": 1.48.2_vs_1.49.0.

    1 /*---------------------------------------------------------------------------------------------
    2  *  Copyright (c) Microsoft Corporation. All rights reserved.
    3  *  Licensed under the MIT License. See License.txt in the project root for license information.
    4  *--------------------------------------------------------------------------------------------*/
    5 
    6 import * as randomBytes from 'randombytes';
    7 import * as querystring from 'querystring';
    8 import { Buffer } from 'buffer';
    9 import * as vscode from 'vscode';
   10 import { createServer, startServer } from './authServer';
   11 
   12 import { v4 as uuid } from 'uuid';
   13 import { keychain } from './keychain';
   14 import Logger from './logger';
   15 import { toBase64UrlEncoding } from './utils';
   16 import fetch from 'node-fetch';
   17 import { sha256 } from './env/node/sha256';
   18 
   19 const redirectUrl = 'https://vscode-redirect.azurewebsites.net/';
   20 const loginEndpointUrl = 'https://login.microsoftonline.com/';
   21 const clientId = 'aebc6443-996d-45c2-90f0-388ff96faa56';
   22 const tenant = 'organizations';
   23 
   24 interface IToken {
   25     accessToken?: string; // When unable to refresh due to network problems, the access token becomes undefined
   26 
   27     expiresIn?: number; // How long access token is valid, in seconds
   28     expiresAt?: number; // UNIX epoch time at which token will expire
   29     refreshToken: string;
   30 
   31     account: {
   32         label: string;
   33         id: string;
   34     };
   35     scope: string;
   36     sessionId: string; // The account id + the scope
   37 }
   38 
   39 interface ITokenClaims {
   40     tid: string;
   41     email?: string;
   42     unique_name?: string;
   43     oid?: string;
   44     altsecid?: string;
   45     ipd?: string;
   46     scp: string;
   47 }
   48 
   49 interface IStoredSession {
   50     id: string;
   51     refreshToken: string;
   52     scope: string; // Scopes are alphabetized and joined with a space
   53     account: {
   54         label?: string;
   55         displayName?: string,
   56         id: string
   57     }
   58 }
   59 
   60 export interface ITokenResponse {
   61     access_token: string;
   62     expires_in: number;
   63     ext_expires_in: number;
   64     refresh_token: string;
   65     scope: string;
   66     token_type: string;
   67 }
   68 
   69 function parseQuery(uri: vscode.Uri) {
   70     return uri.query.split('&').reduce((prev: any, current) => {
   71         const queryString = current.split('=');
   72         prev[queryString[0]] = queryString[1];
   73         return prev;
   74     }, {});
   75 }
   76 
   77 export const onDidChangeSessions = new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
   78 
   79 export const REFRESH_NETWORK_FAILURE = 'Network failure';
   80 
   81 class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
   82     public handleUri(uri: vscode.Uri) {
   83         this.fire(uri);
   84     }
   85 }
   86 
   87 export class AzureActiveDirectoryService {
   88     private _tokens: IToken[] = [];
   89     private _refreshTimeouts: Map<string, NodeJS.Timeout> = new Map<string, NodeJS.Timeout>();
   90     private _uriHandler: UriEventHandler;
   91 
   92     constructor() {
   93         this._uriHandler = new UriEventHandler();
   94         vscode.window.registerUriHandler(this._uriHandler);
   95     }
   96 
   97     public async initialize(): Promise<void> {
   98         const storedData = await keychain.getToken();
   99         if (storedData) {
  100             try {
  101                 const sessions = this.parseStoredData(storedData);
  102                 const refreshes = sessions.map(async session => {
  103                     if (!session.refreshToken) {
  104                         return Promise.resolve();
  105                     }
  106 
  107                     try {
  108                         await this.refreshToken(session.refreshToken, session.scope, session.id);
  109                     } catch (e) {
  110                         if (e.message === REFRESH_NETWORK_FAILURE) {
  111                             const didSucceedOnRetry = await this.handleRefreshNetworkError(session.id, session.refreshToken, session.scope);
  112                             if (!didSucceedOnRetry) {
  113                                 this._tokens.push({
  114                                     accessToken: undefined,
  115                                     refreshToken: session.refreshToken,
  116                                     account: {
  117                                         label: session.account.label ?? session.account.displayName!,
  118                                         id: session.account.id
  119                                     },
  120                                     scope: session.scope,
  121                                     sessionId: session.id
  122                                 });
  123                                 this.pollForReconnect(session.id, session.refreshToken, session.scope);
  124                             }
  125                         } else {
  126                             await this.logout(session.id);
  127                         }
  128                     }
  129                 });
  130 
  131                 await Promise.all(refreshes);
  132             } catch (e) {
  133                 Logger.info('Failed to initialize stored data');
  134                 await this.clearSessions();
  135             }
  136         }
  137 
  138         this.pollForChange();
  139     }
  140 
  141     private parseStoredData(data: string): IStoredSession[] {
  142         return JSON.parse(data);
  143     }
  144 
  145     private async storeTokenData(): Promise<void> {
  146         const serializedData: IStoredSession[] = this._tokens.map(token => {
  147             return {
  148                 id: token.sessionId,
  149                 refreshToken: token.refreshToken,
  150                 scope: token.scope,
  151                 account: token.account
  152             };
  153         });
  154 
  155         await keychain.setToken(JSON.stringify(serializedData));
  156     }
  157 
  158     private pollForChange() {
  159         setTimeout(async () => {
  160             const addedIds: string[] = [];
  161             let removedIds: string[] = [];
  162             const storedData = await keychain.getToken();
  163             if (storedData) {
  164                 try {
  165                     const sessions = this.parseStoredData(storedData);
  166                     let promises = sessions.map(async session => {
  167                         const matchesExisting = this._tokens.some(token => token.scope === session.scope && token.sessionId === session.id);
  168                         if (!matchesExisting && session.refreshToken) {
  169                             try {
  170                                 await this.refreshToken(session.refreshToken, session.scope, session.id);
  171                                 addedIds.push(session.id);
  172                             } catch (e) {
  173                                 if (e.message === REFRESH_NETWORK_FAILURE) {
  174                                     // Ignore, will automatically retry on next poll.
  175                                 } else {
  176                                     await this.logout(session.id);
  177                                 }
  178                             }
  179                         }
  180                     });
  181 
  182                     promises = promises.concat(this._tokens.map(async token => {
  183                         const matchesExisting = sessions.some(session => token.scope === session.scope && token.sessionId === session.id);
  184                         if (!matchesExisting) {
  185                             await this.logout(token.sessionId);
  186                             removedIds.push(token.sessionId);
  187                         }
  188                     }));
  189 
  190                     await Promise.all(promises);
  191                 } catch (e) {
  192                     Logger.error(e.message);
  193                     // if data is improperly formatted, remove all of it and send change event
  194                     removedIds = this._tokens.map(token => token.sessionId);
  195                     this.clearSessions();
  196                 }
  197             } else {
  198                 if (this._tokens.length) {
  199                     // Log out all, remove all local data
  200                     removedIds = this._tokens.map(token => token.sessionId);
  201                     Logger.info('No stored keychain data, clearing local data');
  202 
  203                     this._tokens = [];
  204 
  205                     this._refreshTimeouts.forEach(timeout => {
  206                         clearTimeout(timeout);
  207                     });
  208 
  209                     this._refreshTimeouts.clear();
  210                 }
  211             }
  212 
  213             if (addedIds.length || removedIds.length) {
  214                 onDidChangeSessions.fire({ added: addedIds, removed: removedIds, changed: [] });
  215             }
  216 
  217             this.pollForChange();
  218         }, 1000 * 30);
  219     }
  220 
  221     private async convertToSession(token: IToken): Promise<vscode.AuthenticationSession> {
  222         const resolvedToken = await this.resolveAccessToken(token);
  223         return {
  224             id: token.sessionId,
  225             accessToken: resolvedToken,
  226             account: token.account,
  227             scopes: token.scope.split(' ')
  228         };
  229     }
  230 
  231     private async resolveAccessToken(token: IToken): Promise<string> {
  232         if (token.accessToken && (!token.expiresAt || token.expiresAt > Date.now())) {
  233             token.expiresAt
  234                 ? Logger.info(`Token available from cache, expires in ${token.expiresAt - Date.now()} milliseconds`)
  235                 : Logger.info('Token available from cache');
  236             return Promise.resolve(token.accessToken);
  237         }
  238 
  239         try {
  240             Logger.info('Token expired or unavailable, trying refresh');
  241             const refreshedToken = await this.refreshToken(token.refreshToken, token.scope, token.sessionId);
  242             if (refreshedToken.accessToken) {
  243                 return refreshedToken.accessToken;
  244             } else {
  245                 throw new Error();
  246             }
  247         } catch (e) {
  248             throw new Error('Unavailable due to network problems');
  249         }
  250     }
  251 
  252     private getTokenClaims(accessToken: string): ITokenClaims {
  253         try {
  254             return JSON.parse(Buffer.from(accessToken.split('.')[1], 'base64').toString());
  255         } catch (e) {
  256             Logger.error(e.message);
  257             throw new Error('Unable to read token claims');
  258         }
  259     }
  260 
  261     get sessions(): Promise<vscode.AuthenticationSession[]> {
  262         return Promise.all(this._tokens.map(token => this.convertToSession(token)));
  263     }
  264 
  265     public async login(scope: string): Promise<vscode.AuthenticationSession> {
  266         Logger.info('Logging in...');
  267         if (!scope.includes('offline_access')) {
  268             Logger.info('Warning: The \'offline_access\' scope was not included, so the generated token will not be able to be refreshed.');
  269         }
  270 
  271         return new Promise(async (resolve, reject) => {
  272             if (vscode.env.uiKind === vscode.UIKind.Web) {
  273                 resolve(this.loginWithoutLocalServer(scope));
  274                 return;
  275             }
  276 
  277             const nonce = randomBytes(16).toString('base64');
  278             const { server, redirectPromise, codePromise } = createServer(nonce);
  279 
  280             let token: IToken | undefined;
  281             try {
  282                 const port = await startServer(server);
  283                 vscode.env.openExternal(vscode.Uri.parse(`http://localhost:${port}/signin?nonce=${encodeURIComponent(nonce)}`));
  284 
  285                 const redirectReq = await redirectPromise;
  286                 if ('err' in redirectReq) {
  287                     const { err, res } = redirectReq;
  288                     res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` });
  289                     res.end();
  290                     throw err;
  291                 }
  292 
  293                 const host = redirectReq.req.headers.host || '';
  294                 const updatedPortStr = (/^[^:]+:(\d+)$/.exec(Array.isArray(host) ? host[0] : host) || [])[1];
  295                 const updatedPort = updatedPortStr ? parseInt(updatedPortStr, 10) : port;
  296 
  297                 const state = `${updatedPort},${encodeURIComponent(nonce)}`;
  298 
  299                 const codeVerifier = toBase64UrlEncoding(randomBytes(32).toString('base64'));
  300                 const codeChallenge = toBase64UrlEncoding(await sha256(codeVerifier));
  301                 const loginUrl = `${loginEndpointUrl}${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${state}&scope=${encodeURIComponent(scope)}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}`;
  302 
  303                 await redirectReq.res.writeHead(302, { Location: loginUrl });
  304                 redirectReq.res.end();
  305 
  306                 const codeRes = await codePromise;
  307                 const res = codeRes.res;
  308 
  309                 try {
  310                     if ('err' in codeRes) {
  311                         throw codeRes.err;
  312                     }
  313                     token = await this.exchangeCodeForToken(codeRes.code, codeVerifier, scope);
  314                     this.setToken(token, scope);
  315                     Logger.info('Login successful');
  316                     res.writeHead(302, { Location: '/' });
  317                     const session = await this.convertToSession(token);
  318                     resolve(session);
  319                     res.end();
  320                 } catch (err) {
  321                     res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` });
  322                     res.end();
  323                     reject(err.message);
  324                 }
  325             } catch (e) {
  326                 Logger.error(e.message);
  327 
  328                 // If the error was about starting the server, try directly hitting the login endpoint instead
  329                 if (e.message === 'Error listening to server' || e.message === 'Closed' || e.message === 'Timeout waiting for port') {
  330                     await this.loginWithoutLocalServer(scope);
  331                 }
  332 
  333                 reject(e.message);
  334             } finally {
  335                 setTimeout(() => {
  336                     server.close();
  337                 }, 5000);
  338             }
  339         });
  340     }
  341 
  342     private getCallbackEnvironment(callbackUri: vscode.Uri): string {
  343         if (callbackUri.authority.endsWith('.workspaces.github.com') || callbackUri.authority.endsWith('.github.dev')) {
  344             return `${callbackUri.authority},`;
  345         }
  346 
  347         switch (callbackUri.authority) {
  348             case 'online.visualstudio.com':
  349                 return 'vso,';
  350             case 'online-ppe.core.vsengsaas.visualstudio.com':
  351                 return 'vsoppe,';
  352             case 'online.dev.core.vsengsaas.visualstudio.com':
  353                 return 'vsodev,';
  354             default:
  355                 return '';
  356         }
  357     }
  358 
  359     private async loginWithoutLocalServer(scope: string): Promise<vscode.AuthenticationSession> {
  360         const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.microsoft-authentication`));
  361         const nonce = randomBytes(16).toString('base64');
  362         const port = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80);
  363         const callbackEnvironment = this.getCallbackEnvironment(callbackUri);
  364         const state = `${callbackEnvironment}${port},${encodeURIComponent(nonce)},${encodeURIComponent(callbackUri.query)}`;
  365         const signInUrl = `${loginEndpointUrl}${tenant}/oauth2/v2.0/authorize`;
  366         let uri = vscode.Uri.parse(signInUrl);
  367         const codeVerifier = toBase64UrlEncoding(randomBytes(32).toString('base64'));
  368         const codeChallenge = toBase64UrlEncoding(await sha256(codeVerifier));
  369         uri = uri.with({
  370             query: `response_type=code&client_id=${encodeURIComponent(clientId)}&response_mode=query&redirect_uri=${redirectUrl}&state=${state}&scope=${scope}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}`
  371         });
  372         vscode.env.openExternal(uri);
  373 
  374         const timeoutPromise = new Promise((_: (value: vscode.AuthenticationSession) => void, reject) => {
  375             const wait = setTimeout(() => {
  376                 clearTimeout(wait);
  377                 reject('Login timed out.');
  378             }, 1000 * 60 * 5);
  379         });
  380 
  381         return Promise.race([this.handleCodeResponse(state, codeVerifier, scope), timeoutPromise]);
  382     }
  383 
  384     private async handleCodeResponse(state: string, codeVerifier: string, scope: string): Promise<vscode.AuthenticationSession> {
  385         let uriEventListener: vscode.Disposable;
  386         return new Promise((resolve: (value: vscode.AuthenticationSession) => void, reject) => {
  387             uriEventListener = this._uriHandler.event(async (uri: vscode.Uri) => {
  388                 try {
  389                     const query = parseQuery(uri);
  390                     const code = query.code;
  391 
  392                     // Workaround double encoding issues of state in web
  393                     if (query.state !== state && decodeURIComponent(query.state) !== state) {
  394                         throw new Error('State does not match.');
  395                     }
  396 
  397                     const token = await this.exchangeCodeForToken(code, codeVerifier, scope);
  398                     this.setToken(token, scope);
  399 
  400                     const session = await this.convertToSession(token);
  401                     resolve(session);
  402                 } catch (err) {
  403                     reject(err);
  404                 }
  405             });
  406         }).then(result => {
  407             uriEventListener.dispose();
  408             return result;
  409         }).catch(err => {
  410             uriEventListener.dispose();
  411             throw err;
  412         });
  413     }
  414 
  415     private async setToken(token: IToken, scope: string): Promise<void> {
  416         const existingTokenIndex = this._tokens.findIndex(t => t.sessionId === token.sessionId);
  417         if (existingTokenIndex > -1) {
  418             this._tokens.splice(existingTokenIndex, 1, token);
  419         } else {
  420             this._tokens.push(token);
  421         }
  422 
  423         this.clearSessionTimeout(token.sessionId);
  424 
  425         if (token.expiresIn) {
  426             this._refreshTimeouts.set(token.sessionId, setTimeout(async () => {
  427                 try {
  428                     await this.refreshToken(token.refreshToken, scope, token.sessionId);
  429                     onDidChangeSessions.fire({ added: [], removed: [], changed: [token.sessionId] });
  430                 } catch (e) {
  431                     if (e.message === REFRESH_NETWORK_FAILURE) {
  432                         const didSucceedOnRetry = await this.handleRefreshNetworkError(token.sessionId, token.refreshToken, scope);
  433                         if (!didSucceedOnRetry) {
  434                             this.pollForReconnect(token.sessionId, token.refreshToken, token.scope);
  435                         }
  436                     } else {
  437                         await this.logout(token.sessionId);
  438                         onDidChangeSessions.fire({ added: [], removed: [token.sessionId], changed: [] });
  439                     }
  440                 }
  441             }, 1000 * (token.expiresIn - 30)));
  442         }
  443 
  444         this.storeTokenData();
  445     }
  446 
  447     private getTokenFromResponse(json: ITokenResponse, scope: string, existingId?: string): IToken {
  448         const claims = this.getTokenClaims(json.access_token);
  449         return {
  450             expiresIn: json.expires_in,
  451             expiresAt: json.expires_in ? Date.now() + json.expires_in * 1000 : undefined,
  452             accessToken: json.access_token,
  453             refreshToken: json.refresh_token,
  454             scope,
  455             sessionId: existingId || `${claims.tid}/${(claims.oid || (claims.altsecid || '' + claims.ipd || ''))}/${uuid()}`,
  456             account: {
  457                 label: claims.email || claims.unique_name || 'user@example.com',
  458                 id: `${claims.tid}/${(claims.oid || (claims.altsecid || '' + claims.ipd || ''))}`
  459             }
  460         };
  461     }
  462 
  463     private async exchangeCodeForToken(code: string, codeVerifier: string, scope: string): Promise<IToken> {
  464         Logger.info('Exchanging login code for token');
  465         try {
  466             const postData = querystring.stringify({
  467                 grant_type: 'authorization_code',
  468                 code: code,
  469                 client_id: clientId,
  470                 scope: scope,
  471                 code_verifier: codeVerifier,
  472                 redirect_uri: redirectUrl
  473             });
  474 
  475             const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints');
  476             const endpoint = proxyEndpoints && proxyEndpoints['microsoft'] || `${loginEndpointUrl}${tenant}/oauth2/v2.0/token`;
  477 
  478             const result = await fetch(endpoint, {
  479                 method: 'POST',
  480                 headers: {
  481                     'Content-Type': 'application/x-www-form-urlencoded',
  482                     'Content-Length': postData.length.toString()
  483                 },
  484                 body: postData
  485             });
  486 
  487             if (result.ok) {
  488                 Logger.info('Exchanging login code for token success');
  489                 const json = await result.json();
  490                 return this.getTokenFromResponse(json, scope);
  491             } else {
  492                 Logger.error('Exchanging login code for token failed');
  493                 throw new Error('Unable to login.');
  494             }
  495         } catch (e) {
  496             Logger.error(e.message);
  497             throw e;
  498         }
  499     }
  500 
  501     private async refreshToken(refreshToken: string, scope: string, sessionId: string): Promise<IToken> {
  502         try {
  503             Logger.info('Refreshing token...');
  504             const postData = querystring.stringify({
  505                 refresh_token: refreshToken,
  506                 client_id: clientId,
  507                 grant_type: 'refresh_token',
  508                 scope: scope
  509             });
  510 
  511             const result = await fetch(`https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`, {
  512                 method: 'POST',
  513                 headers: {
  514                     'Content-Type': 'application/x-www-form-urlencoded',
  515                     'Content-Length': postData.length.toString()
  516                 },
  517                 body: postData
  518             });
  519 
  520             if (result.ok) {
  521                 const json = await result.json();
  522                 const token = this.getTokenFromResponse(json, scope, sessionId);
  523                 this.setToken(token, scope);
  524                 Logger.info('Token refresh success');
  525                 return token;
  526             } else {
  527                 Logger.error('Refreshing token failed');
  528                 throw new Error('Refreshing token failed.');
  529             }
  530         } catch (e) {
  531             Logger.error('Refreshing token failed');
  532             throw new Error(REFRESH_NETWORK_FAILURE);
  533         }
  534     }
  535 
  536     private clearSessionTimeout(sessionId: string): void {
  537         const timeout = this._refreshTimeouts.get(sessionId);
  538         if (timeout) {
  539             clearTimeout(timeout);
  540             this._refreshTimeouts.delete(sessionId);
  541         }
  542     }
  543 
  544     private removeInMemorySessionData(sessionId: string) {
  545         const tokenIndex = this._tokens.findIndex(token => token.sessionId === sessionId);
  546         if (tokenIndex > -1) {
  547             this._tokens.splice(tokenIndex, 1);
  548         }
  549 
  550         this.clearSessionTimeout(sessionId);
  551     }
  552 
  553     private pollForReconnect(sessionId: string, refreshToken: string, scope: string): void {
  554         this.clearSessionTimeout(sessionId);
  555 
  556         this._refreshTimeouts.set(sessionId, setTimeout(async () => {
  557             try {
  558                 await this.refreshToken(refreshToken, scope, sessionId);
  559             } catch (e) {
  560                 this.pollForReconnect(sessionId, refreshToken, scope);
  561             }
  562         }, 1000 * 60 * 30));
  563     }
  564 
  565     private handleRefreshNetworkError(sessionId: string, refreshToken: string, scope: string, attempts: number = 1): Promise<boolean> {
  566         return new Promise((resolve, _) => {
  567             if (attempts === 3) {
  568                 Logger.error('Token refresh failed after 3 attempts');
  569                 return resolve(false);
  570             }
  571 
  572             if (attempts === 1) {
  573                 const token = this._tokens.find(token => token.sessionId === sessionId);
  574                 if (token) {
  575                     token.accessToken = undefined;
  576                     onDidChangeSessions.fire({ added: [], removed: [], changed: [token.sessionId] });
  577                 }
  578             }
  579 
  580             const delayBeforeRetry = 5 * attempts * attempts;
  581 
  582             this.clearSessionTimeout(sessionId);
  583 
  584             this._refreshTimeouts.set(sessionId, setTimeout(async () => {
  585                 try {
  586                     await this.refreshToken(refreshToken, scope, sessionId);
  587                     return resolve(true);
  588                 } catch (e) {
  589                     return resolve(await this.handleRefreshNetworkError(sessionId, refreshToken, scope, attempts + 1));
  590                 }
  591             }, 1000 * delayBeforeRetry));
  592         });
  593     }
  594 
  595     public async logout(sessionId: string) {
  596         Logger.info(`Logging out of session '${sessionId}'`);
  597         this.removeInMemorySessionData(sessionId);
  598 
  599         if (this._tokens.length === 0) {
  600             await keychain.deleteToken();
  601         } else {
  602             this.storeTokenData();
  603         }
  604     }
  605 
  606     public async clearSessions() {
  607         Logger.info('Logging out of all sessions');
  608         this._tokens = [];
  609         await keychain.deleteToken();
  610 
  611         this._refreshTimeouts.forEach(timeout => {
  612             clearTimeout(timeout);
  613         });
  614 
  615         this._refreshTimeouts.clear();
  616     }
  617 }