Path: blob/main/extensions/copilot/src/platform/authentication/vscode-node/copilotTokenManager.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { env, window } from 'vscode';6import { TaskSingler } from '../../../util/common/taskSingler';7import { ConfigKey, IConfigurationService } from '../../configuration/common/configurationService';8import { ICAPIClientService } from '../../endpoint/common/capiClient';9import { IDomainService } from '../../endpoint/common/domainService';10import { IEnvService } from '../../env/common/envService';11import { BaseOctoKitService } from '../../github/common/githubService';12import { ILogService } from '../../log/common/logService';13import { IFetcherService } from '../../networking/common/fetcherService';14import { ITelemetryService } from '../../telemetry/common/telemetry';15import { CopilotToken, ExtendedTokenInfo, TokenErrorNotificationId, TokenInfoOrError } from '../common/copilotToken';16import { nowSeconds } from '../common/copilotTokenManager';17import { BaseCopilotTokenManager } from '../node/copilotTokenManager';18import { getAnyAuthSession } from './session';1920//Flag if we've shown message about broken oauth token.21let shown401Message = false;2223export class NotSignedUpError extends Error { }24export class SubscriptionExpiredError extends Error { }25export class ContactSupportError extends Error { }26export class EnterpriseManagedError extends Error { }27export class InvalidTokenError extends Error { }28export class RateLimitedError extends Error { }29export class GitHubLoginFailedError extends Error { }3031export class VSCodeCopilotTokenManager extends BaseCopilotTokenManager {32private _taskSingler = new TaskSingler<TokenInfoOrError>();3334constructor(35@ILogService logService: ILogService,36@ITelemetryService telemetryService: ITelemetryService,37@IDomainService domainService: IDomainService,38@ICAPIClientService capiClientService: ICAPIClientService,39@IFetcherService fetcherService: IFetcherService,40@IEnvService envService: IEnvService,41@IConfigurationService protected readonly configurationService: IConfigurationService42) {43super(new BaseOctoKitService(capiClientService, fetcherService, logService, telemetryService), logService, telemetryService, domainService, capiClientService, fetcherService, envService);44}4546async getCopilotToken(force?: boolean): Promise<CopilotToken> {47const failWith = this.configurationService.getConfig(ConfigKey.Advanced.DebugGitHubAuthFailWith);48if (failWith) {49this.copilotToken = undefined;50}5152if (!this.copilotToken || this.copilotToken.expires_at - (60 * 5 /* 5min */) < nowSeconds() || force) {53try {54this._logService.debug(`Getting CopilotToken (force: ${force})...`);55this.copilotToken = await this._authShowWarnings();56this._logService.debug(`Got CopilotToken (force: ${force}).`);57} catch (e) {58this._logService.debug(`Getting CopilotToken (force: ${force}) threw error: ${e}`);59this.copilotToken = undefined;60throw e;61}62}63return new CopilotToken(this.copilotToken);64}6566private async _auth(): Promise<TokenInfoOrError> {67const failWith = this.configurationService.getConfig(ConfigKey.Advanced.DebugGitHubAuthFailWith);68if (failWith) {69return { kind: 'failure', reason: failWith };70}7172const allowNoAuthAccess = this.configurationService.getNonExtensionConfig<boolean>('chat.allowAnonymousAccess');73const session = await getAnyAuthSession(this.configurationService, { silent: true });74if (!session && !allowNoAuthAccess) {75this._logService.warn('GitHub login failed');76this._telemetryService.sendGHTelemetryErrorEvent('auth.github_login_failed');77return { kind: 'failure', reason: 'GitHubLoginFailed' };78}79if (session) {80// Log the steps by default, but only log actual token values when the log level is set to debug.81this._logService.info(`Logged in as ${session.account.label}`);82const tokenResult = await this.authFromGitHubToken(session.accessToken, session.account.label);83if (tokenResult.kind === 'success') {84this._logService.info(`Got Copilot token for ${session.account.label}`);85this._logService.info(`Copilot Chat: ${this._envService.getVersion()}, VS Code: ${this._envService.vscodeVersion}`);86}87return tokenResult;88} else {89this._logService.info(`Allowing anonymous access with devDeviceId`);90const tokenResult = await this.authFromDevDeviceId(env.devDeviceId);91if (tokenResult.kind === 'success') {92this._logService.info(`Got Copilot token for devDeviceId`);93this._logService.info(`Copilot Chat: ${this._envService.getVersion()}, VS Code: ${this._envService.vscodeVersion}`);94} else {95this._logService.warn('GitHub login failed');96return { kind: 'failure', reason: 'GitHubLoginFailed' };97}98return tokenResult;99}100}101102private async _authShowWarnings(): Promise<ExtendedTokenInfo> {103const tokenResult = await this._taskSingler.getOrCreate('auth', () => this._auth());104this.sendTokenResultErrorTelemetry(tokenResult);105106if (tokenResult.kind === 'failure' && tokenResult.reason === 'NotAuthorized') {107const message = tokenResult.message;108switch (tokenResult.notification_id) {109case TokenErrorNotificationId.NotSignedUp:110case TokenErrorNotificationId.NoCopilotAccess:111throw new NotSignedUpError(message ?? 'User not authorized');112case TokenErrorNotificationId.SubscriptionEnded:113throw new SubscriptionExpiredError(message);114case TokenErrorNotificationId.EnterPriseManagedUserAccount:115throw new EnterpriseManagedError(message);116case TokenErrorNotificationId.ServerError:117case TokenErrorNotificationId.FeatureFlagBlocked:118case TokenErrorNotificationId.SpammyUser:119case TokenErrorNotificationId.SnippyNotConfigured:120throw new ContactSupportError(message);121}122}123if (tokenResult.kind === 'failure' && tokenResult.reason === 'HTTP401') {124const message =125'Your GitHub token is invalid. Please sign out from your GitHub account using the VS Code accounts menu and try again.';126if (!shown401Message) {127shown401Message = true;128window.showWarningMessage(message);129}130throw new InvalidTokenError(message);131}132133if (tokenResult.kind === 'failure' && tokenResult.reason === 'GitHubLoginFailed') {134throw new GitHubLoginFailedError('GitHubLoginFailed');135}136137if (tokenResult.kind === 'failure' && tokenResult.reason === 'RateLimited') {138throw new RateLimitedError(`Your account has exceeded GitHub's API rate limit. Please try again later.`);139}140141if (tokenResult.kind === 'failure') {142throw Error('Failed to get copilot token. reason: ' + tokenResult.reason);143}144145return tokenResult;146}147148private sendTokenResultErrorTelemetry(tokenResult: TokenInfoOrError): void {149if (tokenResult.kind === 'success') {150return;151}152153/* __GDPR__154"copilotTokenFetching.error" : {155"owner": "TylerLeonhardt",156"comment": "Report on the frequency of token retrieval failures.",157"reason": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The reason for the token retrieval failure" },158"notification_id": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The notification ID associated with the failure, if any" }159}160*/161this._telemetryService.sendMSFTTelemetryEvent('copilotTokenFetching.error', {162reason: tokenResult.reason,163notification_id: tokenResult.notification_id,164});165}166}167168169