diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index 9dec4a9aa4a..51b61396ab5 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -164,6 +164,7 @@ export class AzureActiveDirectoryService { sessionId: session.id }); } else { + vscode.window.showErrorMessage(localize('signOut', "You have been signed out because reading stored authentication information failed.")); Logger.error(e); await this.removeSession(session.id); } @@ -189,7 +190,7 @@ export class AzureActiveDirectoryService { return sessions; } - const modifiedScopes = [...scopes]; + let modifiedScopes = [...scopes]; if (!modifiedScopes.includes('openid')) { modifiedScopes.push('openid'); } @@ -199,9 +200,10 @@ export class AzureActiveDirectoryService { if (!modifiedScopes.includes('profile')) { modifiedScopes.push('profile'); } + modifiedScopes = modifiedScopes.sort(); - let orderedScopes = modifiedScopes.sort().join(' '); - Logger.info(`Getting sessions for the following scopes: ${orderedScopes}`); + let modifiedScopesStr = modifiedScopes.join(' '); + Logger.info(`Getting sessions for the following scopes: ${modifiedScopesStr}`); if (this._refreshingPromise) { Logger.info('Refreshing in progress. Waiting for completion before continuing.'); @@ -212,18 +214,51 @@ export class AzureActiveDirectoryService { } } - let matchingTokens = this._tokens.filter(token => token.scope === orderedScopes); + let matchingTokens = this._tokens.filter(token => token.scope === modifiedScopesStr); // The user may still have a token that doesn't have the openid & email scopes so check for that as well. // Eventually, we should remove this and force the user to re-log in so that we don't have any sessions // without an idtoken. if (!matchingTokens.length) { - orderedScopes = scopes.sort().join(' '); - Logger.trace(`No session found with idtoken scopes... Using fallback scope list of: ${orderedScopes}`); - matchingTokens = this._tokens.filter(token => token.scope === orderedScopes); + const fallbackOrderedScopes = scopes.sort().join(' '); + Logger.trace(`No session found with idtoken scopes... Using fallback scope list of: ${fallbackOrderedScopes}`); + matchingTokens = this._tokens.filter(token => token.scope === fallbackOrderedScopes); + if (matchingTokens.length) { + modifiedScopesStr = fallbackOrderedScopes; + } } - Logger.info(`Got ${matchingTokens.length} sessions for scopes: ${orderedScopes}`); + // If we still don't have a matching token try to get a new token from an existing token by using + // the refreshToken. This is documented here: + // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#refresh-the-access-token + // "Refresh tokens are valid for all permissions that your client has already received consent for." + if (!matchingTokens.length) { + const clientId = this.getClientId(modifiedScopes); + // Get a token with the correct client id. + const token = clientId === DEFAULT_CLIENT_ID + ? this._tokens.find(t => t.refreshToken && !t.scope.includes('VSCODE_CLIENT_ID')) + : this._tokens.find(t => t.refreshToken && t.scope.includes(`VSCODE_CLIENT_ID:${clientId}`)); + + if (token) { + const scopeData: IScopeData = { + clientId, + scopes: modifiedScopes, + scopeStr: modifiedScopesStr, + // filter our special scopes + scopesToSend: modifiedScopes.filter(s => !s.startsWith('VSCODE_')).join(' '), + tenant: this.getTenantId(modifiedScopes), + }; + + try { + const itoken = await this.refreshToken(token.refreshToken, scopeData); + matchingTokens.push(itoken); + } catch (err) { + Logger.error(`Attempted to get a new session for scopes '${scopeData.scopeStr}' using the existing session with scopes '${token.scope}' but it failed due to: ${err.message ?? err}`); + } + } + } + + Logger.info(`Got ${matchingTokens.length} sessions for scopes: ${modifiedScopesStr}`); return Promise.all(matchingTokens.map(token => this.convertToSession(token))); } @@ -402,6 +437,7 @@ export class AzureActiveDirectoryService { onDidChangeSessions.fire({ added: [], removed: [], changed: [this.convertToSessionSync(refreshedToken)] }); } catch (e) { if (e.message !== REFRESH_NETWORK_FAILURE) { + vscode.window.showErrorMessage(localize('signOut', "You have been signed out because reading stored authentication information failed.")); await this.removeSession(sessionId); } } @@ -517,7 +553,7 @@ export class AzureActiveDirectoryService { //#region refresh logic - private async refreshToken(refreshToken: string, scopeData: IScopeData, sessionId: string): Promise { + private async refreshToken(refreshToken: string, scopeData: IScopeData, sessionId?: string): Promise { this._refreshingPromise = this.doRefreshToken(refreshToken, scopeData, sessionId); try { const result = await this._refreshingPromise; @@ -527,7 +563,7 @@ export class AzureActiveDirectoryService { } } - private async doRefreshToken(refreshToken: string, scopeData: IScopeData, sessionId: string): Promise { + private async doRefreshToken(refreshToken: string, scopeData: IScopeData, sessionId?: string): Promise { Logger.info(`Refreshing token for scopes: ${scopeData.scopeStr}`); const postData = querystring.stringify({ refresh_token: refreshToken, @@ -552,13 +588,14 @@ export class AzureActiveDirectoryService { } catch (e) { if (e.message === REFRESH_NETWORK_FAILURE) { // We were unable to refresh because of a network failure (i.e. the user lost internet access). - // so set up a timeout to try again later. - this.setSessionTimeout(sessionId, refreshToken, scopeData, AzureActiveDirectoryService.POLLING_CONSTANT); + // so set up a timeout to try again later. We only do this if we have a session id to reference later. + if (sessionId) { + this.setSessionTimeout(sessionId, refreshToken, scopeData, AzureActiveDirectoryService.POLLING_CONSTANT); + } throw e; } - vscode.window.showErrorMessage(localize('signOut', "You have been signed out because reading stored authentication information failed.")); Logger.error(`Refreshing token failed (for scopes: ${scopeData.scopeStr}): ${e.message}`); - throw new Error('Refreshing token failed'); + throw e; } } @@ -743,6 +780,7 @@ export class AzureActiveDirectoryService { } catch (e) { // Network failures will automatically retry on next poll. if (e.message !== REFRESH_NETWORK_FAILURE) { + vscode.window.showErrorMessage(localize('signOut', "You have been signed out because reading stored authentication information failed.")); await this.removeSession(session.id); } return;