diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index dc7278f6d4f..da57e3bcb3c 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -16,6 +16,13 @@ import { fetching } from './node/fetch'; const CLIENT_ID = '01ab8ac9400c4e429b23'; const GITHUB_TOKEN_URL = 'https://vscode.dev/codeExchangeProxyEndpoints/github/login/oauth/access_token'; + +// This is the error message that we throw if the login was cancelled for any reason. Extensions +// calling `getSession` can handle this error to know that the user cancelled the login. +const CANCELLATION_ERROR = 'Cancelled'; +// These error messages are internal and should not be shown to the user in any way. +const TIMED_OUT_ERROR = 'Timed out'; +const USER_CANCELLATION_ERROR = 'User Cancelled'; const NETWORK_ERROR = 'network error'; const REDIRECT_URL_STABLE = 'https://vscode.dev/redirect'; @@ -132,7 +139,7 @@ export class GitHubServer implements IGitHubServer { : vscode.l10n.t('You have not yet finished authorizing this extension to use GitHub. Would you like to keep trying?'); const result = await vscode.window.showWarningMessage(message, yes, no); if (result !== yes) { - throw new Error('Cancelled'); + throw new Error(CANCELLATION_ERROR); } }; @@ -146,7 +153,7 @@ export class GitHubServer implements IGitHubServer { return await this.doLoginWithoutLocalServer(scopes, nonce, callbackUri); } catch (e) { this._logger.error(e); - userCancelled = e.message ?? e === 'User Cancelled'; + userCancelled = e.message ?? e === USER_CANCELLATION_ERROR; } } @@ -163,8 +170,7 @@ export class GitHubServer implements IGitHubServer { await promptToContinue(); return await this.doLoginWithLocalServer(scopes); } catch (e) { - this._logger.error(e); - userCancelled = e.message ?? e === 'User Cancelled'; + userCancelled = this.processLoginError(e); } } @@ -174,8 +180,7 @@ export class GitHubServer implements IGitHubServer { await promptToContinue(); return await this.doLoginDeviceCodeFlow(scopes); } catch (e) { - this._logger.error(e); - userCancelled = e.message ?? e === 'User Cancelled'; + userCancelled = this.processLoginError(e); } } @@ -186,12 +191,11 @@ export class GitHubServer implements IGitHubServer { await promptToContinue(); return await this.doLoginWithPat(scopes); } catch (e) { - this._logger.error(e); - userCancelled = e.message ?? e === 'User Cancelled'; + userCancelled = this.processLoginError(e); } } - throw new Error(userCancelled ? 'Cancelled' : 'No auth flow succeeded.'); + throw new Error(userCancelled ? CANCELLATION_ERROR : 'No auth flow succeeded.'); } private async doLoginWithoutLocalServer(scopes: string, nonce: string, callbackUri: vscode.Uri): Promise { @@ -232,8 +236,8 @@ export class GitHubServer implements IGitHubServer { try { return await Promise.race([ codeExchangePromise.promise, - new Promise((_, reject) => setTimeout(() => reject('Timed out'), 300_000)), // 5min timeout - promiseFromEvent(token.onCancellationRequested, (_, __, reject) => { reject('User Cancelled'); }).promise + new Promise((_, reject) => setTimeout(() => reject(TIMED_OUT_ERROR), 300_000)), // 5min timeout + promiseFromEvent(token.onCancellationRequested, (_, __, reject) => { reject(USER_CANCELLATION_ERROR); }).promise ]); } finally { this._pendingNonces.delete(scopes); @@ -273,8 +277,8 @@ export class GitHubServer implements IGitHubServer { vscode.env.openExternal(vscode.Uri.parse(`http://127.0.0.1:${port}/signin?nonce=${encodeURIComponent(server.nonce)}`)); const { code } = await Promise.race([ server.waitForOAuthResponse(), - new Promise((_, reject) => setTimeout(() => reject('Timed out'), 300_000)), // 5min timeout - promiseFromEvent(token.onCancellationRequested, (_, __, reject) => { reject('User Cancelled'); }).promise + new Promise((_, reject) => setTimeout(() => reject(TIMED_OUT_ERROR), 300_000)), // 5min timeout + promiseFromEvent(token.onCancellationRequested, (_, __, reject) => { reject(USER_CANCELLATION_ERROR); }).promise ]); codeToExchange = code; } finally { @@ -317,7 +321,7 @@ export class GitHubServer implements IGitHubServer { }, button); if (modalResult !== button) { - throw new Error('User Cancelled'); + throw new Error(USER_CANCELLATION_ERROR); } await vscode.env.clipboard.writeText(json.user_code); @@ -340,14 +344,14 @@ export class GitHubServer implements IGitHubServer { }, button); if (modalResult !== button) { - throw new Error('User Cancelled'); + throw new Error(USER_CANCELLATION_ERROR); } const description = `${vscode.env.appName} (${scopes})`; const uriToOpen = await vscode.env.asExternalUri(this.baseUri.with({ path: '/settings/tokens/new', query: `description=${description}&scopes=${scopes.split(' ').join(',')}` })); await vscode.env.openExternal(uriToOpen); const token = await vscode.window.showInputBox({ placeHolder: `ghp_1a2b3c4...`, prompt: `GitHub Personal Access Token - ${scopes}`, ignoreFocusOut: true }); - if (!token) { throw new Error('User Cancelled'); } + if (!token) { throw new Error(USER_CANCELLATION_ERROR); } const tokenScopes = await getScopes(token, this.getServerUri('/'), this._logger); // Example: ['repo', 'user'] const scopesList = scopes.split(' '); // Example: 'read:user repo user:email' @@ -392,7 +396,7 @@ export class GitHubServer implements IGitHubServer { for (let i = 0; i < attempts; i++) { await new Promise(resolve => setTimeout(resolve, json.interval * 1000)); if (token.isCancellationRequested) { - throw new Error('User Cancelled'); + throw new Error(USER_CANCELLATION_ERROR); } let accessTokenResult; try { @@ -423,7 +427,7 @@ export class GitHubServer implements IGitHubServer { return accessTokenJson.access_token; } - throw new Error('Cancelled'); + throw new Error(TIMED_OUT_ERROR); }); } @@ -641,4 +645,12 @@ export class GitHubServer implements IGitHubServer { // No-op } } + + private processLoginError(error: Error): boolean { + if (error.message === CANCELLATION_ERROR) { + throw error; + } + this._logger.error(error.message ?? error); + return error.message === USER_CANCELLATION_ERROR; + } }