use-github-pat.patch 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts
  2. index 3662719..5bf55e0 100644
  3. --- a/extensions/github-authentication/src/githubServer.ts
  4. +++ b/extensions/github-authentication/src/githubServer.ts
  5. @@ -6,4 +6,2 @@
  6. import * as vscode from 'vscode';
  7. -import * as path from 'path';
  8. -import { PromiseAdapter, promiseFromEvent } from './common/utils';
  9. import { ExperimentationTelemetry } from './common/experimentationService';
  10. @@ -11,10 +9,5 @@ import { AuthProviderType, UriEventHandler } from './github';
  11. import { Log } from './common/logger';
  12. -import { isSupportedClient, isSupportedTarget } from './common/env';
  13. -import { LoopbackAuthServer } from './node/authServer';
  14. -import { crypto } from './node/crypto';
  15. +import { isSupportedTarget } from './common/env';
  16. import { fetching } from './node/fetch';
  17. -const CLIENT_ID = '01ab8ac9400c4e429b23';
  18. -const GITHUB_TOKEN_URL = 'https://vscode.dev/codeExchangeProxyEndpoints/github/login/oauth/access_token';
  19. -
  20. // This is the error message that we throw if the login was cancelled for any reason. Extensions
  21. @@ -23,3 +16,2 @@ const CANCELLATION_ERROR = 'Cancelled';
  22. // These error messages are internal and should not be shown to the user in any way.
  23. -const TIMED_OUT_ERROR = 'Timed out';
  24. const USER_CANCELLATION_ERROR = 'User Cancelled';
  25. @@ -27,5 +19,2 @@ const NETWORK_ERROR = 'network error';
  26. -const REDIRECT_URL_STABLE = 'https://vscode.dev/redirect';
  27. -const REDIRECT_URL_INSIDERS = 'https://insiders.vscode.dev/redirect';
  28. -
  29. export interface IGitHubServer {
  30. @@ -37,9 +26,2 @@ export interface IGitHubServer {
  31. -interface IGitHubDeviceCodeResponse {
  32. - device_code: string;
  33. - user_code: string;
  34. - verification_uri: string;
  35. - interval: number;
  36. -}
  37. -
  38. async function getScopes(token: string, serverUri: vscode.Uri, logger: Log): Promise<string[]> {
  39. @@ -70,8 +52,4 @@ export class GitHubServer implements IGitHubServer {
  40. - private readonly _pendingNonces = new Map<string, string[]>();
  41. - private readonly _codeExchangePromises = new Map<string, { promise: Promise<string>; cancel: vscode.EventEmitter<void> }>();
  42. private readonly _type: AuthProviderType;
  43. - private _redirectEndpoint: string | undefined;
  44. -
  45. constructor(
  46. @@ -79,3 +57,5 @@ export class GitHubServer implements IGitHubServer {
  47. private readonly _telemetryReporter: ExperimentationTelemetry,
  48. + // @ts-ignore
  49. private readonly _uriHandler: UriEventHandler,
  50. + // @ts-ignore
  51. private readonly _extensionKind: vscode.ExtensionKind,
  52. @@ -94,26 +74,2 @@ export class GitHubServer implements IGitHubServer {
  53. - private async getRedirectEndpoint(): Promise<string> {
  54. - if (this._redirectEndpoint) {
  55. - return this._redirectEndpoint;
  56. - }
  57. - if (this._type === AuthProviderType.github) {
  58. - const proxyEndpoints = await vscode.commands.executeCommand<{ [providerId: string]: string } | undefined>('workbench.getCodeExchangeProxyEndpoints');
  59. - // If we are running in insiders vscode.dev, then ensure we use the redirect route on that.
  60. - this._redirectEndpoint = REDIRECT_URL_STABLE;
  61. - if (proxyEndpoints?.github && new URL(proxyEndpoints.github).hostname === 'insiders.vscode.dev') {
  62. - this._redirectEndpoint = REDIRECT_URL_INSIDERS;
  63. - }
  64. - } else {
  65. - // GHE only supports a single redirect endpoint, so we can't use
  66. - // insiders.vscode.dev/redirect when we're running in Insiders, unfortunately.
  67. - // Additionally, we make the assumption that this function will only be used
  68. - // in flows that target supported GHE targets, not on-prem GHES. Because of this
  69. - // assumption, we can assume that the GHE version used is at least 3.8 which is
  70. - // the version that changed the redirect endpoint to this URI from the old
  71. - // GitHub maintained server.
  72. - this._redirectEndpoint = 'https://vscode.dev/redirect';
  73. - }
  74. - return this._redirectEndpoint;
  75. - }
  76. -
  77. // TODO@joaomoreno TODO@TylerLeonhardt
  78. @@ -129,68 +85,7 @@ export class GitHubServer implements IGitHubServer {
  79. let userCancelled: boolean | undefined;
  80. - const yes = vscode.l10n.t('Yes');
  81. - const no = vscode.l10n.t('No');
  82. - const promptToContinue = async (mode: string) => {
  83. - if (userCancelled === undefined) {
  84. - // We haven't had a failure yet so wait to prompt
  85. - return;
  86. - }
  87. - const message = userCancelled
  88. - ? vscode.l10n.t('Having trouble logging in? Would you like to try a different way? ({0})', mode)
  89. - : vscode.l10n.t('You have not yet finished authorizing this extension to use GitHub. Would you like to try a different way? ({0})', mode);
  90. - const result = await vscode.window.showWarningMessage(message, yes, no);
  91. - if (result !== yes) {
  92. - throw new Error(CANCELLATION_ERROR);
  93. - }
  94. - };
  95. -
  96. - const nonce: string = crypto.getRandomValues(new Uint32Array(2)).reduce((prev, curr) => prev += curr.toString(16), '');
  97. - const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate?nonce=${encodeURIComponent(nonce)}`));
  98. -
  99. - const supportedClient = isSupportedClient(callbackUri);
  100. - const supportedTarget = isSupportedTarget(this._type, this._ghesUri);
  101. - if (supportedClient && supportedTarget) {
  102. - try {
  103. - return await this.doLoginWithoutLocalServer(scopes, nonce, callbackUri);
  104. - } catch (e) {
  105. - this._logger.error(e);
  106. - userCancelled = e.message ?? e === USER_CANCELLATION_ERROR;
  107. - }
  108. - }
  109. -
  110. - // Starting a local server is only supported if:
  111. - // 1. We are in a UI extension because we need to open a port on the machine that has the browser
  112. - // 2. We are in a node runtime because we need to open a port on the machine
  113. - // 3. code exchange can only be done with a supported target
  114. - if (
  115. - this._extensionKind === vscode.ExtensionKind.UI &&
  116. - typeof navigator === 'undefined' &&
  117. - supportedTarget
  118. - ) {
  119. - try {
  120. - await promptToContinue(vscode.l10n.t('local server'));
  121. - return await this.doLoginWithLocalServer(scopes);
  122. - } catch (e) {
  123. - userCancelled = this.processLoginError(e);
  124. - }
  125. - }
  126. -
  127. - // We only can use the Device Code flow when we have a full node environment because of CORS.
  128. - if (typeof navigator === 'undefined') {
  129. - try {
  130. - await promptToContinue(vscode.l10n.t('device code'));
  131. - return await this.doLoginDeviceCodeFlow(scopes);
  132. - } catch (e) {
  133. - userCancelled = this.processLoginError(e);
  134. - }
  135. - }
  136. - // In a supported environment, we can't use PAT auth because we use this auth for Settings Sync and it doesn't support PATs.
  137. - // With that said, GitHub Enterprise isn't used by Settings Sync so we can use PATs for that.
  138. - if (!supportedClient || this._type === AuthProviderType.githubEnterprise) {
  139. - try {
  140. - await promptToContinue(vscode.l10n.t('personal access token'));
  141. - return await this.doLoginWithPat(scopes);
  142. - } catch (e) {
  143. - userCancelled = this.processLoginError(e);
  144. - }
  145. + try {
  146. + return await this.doLoginWithPat(scopes);
  147. + } catch (e) {
  148. + userCancelled = this.processLoginError(e);
  149. }
  150. @@ -200,136 +95,2 @@ export class GitHubServer implements IGitHubServer {
  151. - private async doLoginWithoutLocalServer(scopes: string, nonce: string, callbackUri: vscode.Uri): Promise<string> {
  152. - this._logger.info(`Trying without local server... (${scopes})`);
  153. - return await vscode.window.withProgress<string>({
  154. - location: vscode.ProgressLocation.Notification,
  155. - title: vscode.l10n.t({
  156. - message: 'Signing in to {0}...',
  157. - args: [this.baseUri.authority],
  158. - comment: ['The {0} will be a url, e.g. github.com']
  159. - }),
  160. - cancellable: true
  161. - }, async (_, token) => {
  162. - const existingNonces = this._pendingNonces.get(scopes) || [];
  163. - this._pendingNonces.set(scopes, [...existingNonces, nonce]);
  164. - const redirectUri = await this.getRedirectEndpoint();
  165. - const searchParams = new URLSearchParams([
  166. - ['client_id', CLIENT_ID],
  167. - ['redirect_uri', redirectUri],
  168. - ['scope', scopes],
  169. - ['state', encodeURIComponent(callbackUri.toString(true))]
  170. - ]);
  171. -
  172. - const uri = vscode.Uri.parse(this.baseUri.with({
  173. - path: '/login/oauth/authorize',
  174. - query: searchParams.toString()
  175. - }).toString(true));
  176. - await vscode.env.openExternal(uri);
  177. -
  178. - // Register a single listener for the URI callback, in case the user starts the login process multiple times
  179. - // before completing it.
  180. - let codeExchangePromise = this._codeExchangePromises.get(scopes);
  181. - if (!codeExchangePromise) {
  182. - codeExchangePromise = promiseFromEvent(this._uriHandler!.event, this.handleUri(scopes));
  183. - this._codeExchangePromises.set(scopes, codeExchangePromise);
  184. - }
  185. -
  186. - try {
  187. - return await Promise.race([
  188. - codeExchangePromise.promise,
  189. - new Promise<string>((_, reject) => setTimeout(() => reject(TIMED_OUT_ERROR), 300_000)), // 5min timeout
  190. - promiseFromEvent<any, any>(token.onCancellationRequested, (_, __, reject) => { reject(USER_CANCELLATION_ERROR); }).promise
  191. - ]);
  192. - } finally {
  193. - this._pendingNonces.delete(scopes);
  194. - codeExchangePromise?.cancel.fire();
  195. - this._codeExchangePromises.delete(scopes);
  196. - }
  197. - });
  198. - }
  199. -
  200. - private async doLoginWithLocalServer(scopes: string): Promise<string> {
  201. - this._logger.info(`Trying with local server... (${scopes})`);
  202. - return await vscode.window.withProgress<string>({
  203. - location: vscode.ProgressLocation.Notification,
  204. - title: vscode.l10n.t({
  205. - message: 'Signing in to {0}...',
  206. - args: [this.baseUri.authority],
  207. - comment: ['The {0} will be a url, e.g. github.com']
  208. - }),
  209. - cancellable: true
  210. - }, async (_, token) => {
  211. - const redirectUri = await this.getRedirectEndpoint();
  212. - const searchParams = new URLSearchParams([
  213. - ['client_id', CLIENT_ID],
  214. - ['redirect_uri', redirectUri],
  215. - ['scope', scopes],
  216. - ]);
  217. -
  218. - const loginUrl = this.baseUri.with({
  219. - path: '/login/oauth/authorize',
  220. - query: searchParams.toString()
  221. - });
  222. - const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl.toString(true));
  223. - const port = await server.start();
  224. -
  225. - let codeToExchange;
  226. - try {
  227. - vscode.env.openExternal(vscode.Uri.parse(`http://127.0.0.1:${port}/signin?nonce=${encodeURIComponent(server.nonce)}`));
  228. - const { code } = await Promise.race([
  229. - server.waitForOAuthResponse(),
  230. - new Promise<any>((_, reject) => setTimeout(() => reject(TIMED_OUT_ERROR), 300_000)), // 5min timeout
  231. - promiseFromEvent<any, any>(token.onCancellationRequested, (_, __, reject) => { reject(USER_CANCELLATION_ERROR); }).promise
  232. - ]);
  233. - codeToExchange = code;
  234. - } finally {
  235. - setTimeout(() => {
  236. - void server.stop();
  237. - }, 5000);
  238. - }
  239. -
  240. - const accessToken = await this.exchangeCodeForToken(codeToExchange);
  241. - return accessToken;
  242. - });
  243. - }
  244. -
  245. - private async doLoginDeviceCodeFlow(scopes: string): Promise<string> {
  246. - this._logger.info(`Trying device code flow... (${scopes})`);
  247. -
  248. - // Get initial device code
  249. - const uri = this.baseUri.with({
  250. - path: '/login/device/code',
  251. - query: `client_id=${CLIENT_ID}&scope=${scopes}`
  252. - });
  253. - const result = await fetching(uri.toString(true), {
  254. - method: 'POST',
  255. - headers: {
  256. - Accept: 'application/json'
  257. - }
  258. - });
  259. - if (!result.ok) {
  260. - throw new Error(`Failed to get one-time code: ${await result.text()}`);
  261. - }
  262. -
  263. - const json = await result.json() as IGitHubDeviceCodeResponse;
  264. -
  265. - const button = vscode.l10n.t('Copy & Continue to GitHub');
  266. - const modalResult = await vscode.window.showInformationMessage(
  267. - vscode.l10n.t({ message: 'Your Code: {0}', args: [json.user_code], comment: ['The {0} will be a code, e.g. 123-456'] }),
  268. - {
  269. - modal: true,
  270. - detail: vscode.l10n.t('To finish authenticating, navigate to GitHub and paste in the above one-time code.')
  271. - }, button);
  272. -
  273. - if (modalResult !== button) {
  274. - throw new Error(USER_CANCELLATION_ERROR);
  275. - }
  276. -
  277. - await vscode.env.clipboard.writeText(json.user_code);
  278. -
  279. - const uriToOpen = await vscode.env.asExternalUri(vscode.Uri.parse(json.verification_uri));
  280. - await vscode.env.openExternal(uriToOpen);
  281. -
  282. - return await this.waitForDeviceCodeAccessToken(json);
  283. - }
  284. -
  285. private async doLoginWithPat(scopes: string): Promise<string> {
  286. @@ -373,124 +134,2 @@ export class GitHubServer implements IGitHubServer {
  287. - private async waitForDeviceCodeAccessToken(
  288. - json: IGitHubDeviceCodeResponse,
  289. - ): Promise<string> {
  290. - return await vscode.window.withProgress<string>({
  291. - location: vscode.ProgressLocation.Notification,
  292. - cancellable: true,
  293. - title: vscode.l10n.t({
  294. - message: 'Open [{0}]({0}) in a new tab and paste your one-time code: {1}',
  295. - args: [json.verification_uri, json.user_code],
  296. - comment: [
  297. - 'The [{0}]({0}) will be a url and the {1} will be a code, e.g. 123-456',
  298. - '{Locked="[{0}]({0})"}'
  299. - ]
  300. - })
  301. - }, async (_, token) => {
  302. - const refreshTokenUri = this.baseUri.with({
  303. - path: '/login/oauth/access_token',
  304. - query: `client_id=${CLIENT_ID}&device_code=${json.device_code}&grant_type=urn:ietf:params:oauth:grant-type:device_code`
  305. - });
  306. -
  307. - // Try for 2 minutes
  308. - const attempts = 120 / json.interval;
  309. - for (let i = 0; i < attempts; i++) {
  310. - await new Promise(resolve => setTimeout(resolve, json.interval * 1000));
  311. - if (token.isCancellationRequested) {
  312. - throw new Error(USER_CANCELLATION_ERROR);
  313. - }
  314. - let accessTokenResult;
  315. - try {
  316. - accessTokenResult = await fetching(refreshTokenUri.toString(true), {
  317. - method: 'POST',
  318. - headers: {
  319. - Accept: 'application/json'
  320. - }
  321. - });
  322. - } catch {
  323. - continue;
  324. - }
  325. -
  326. - if (!accessTokenResult.ok) {
  327. - continue;
  328. - }
  329. -
  330. - const accessTokenJson = await accessTokenResult.json();
  331. -
  332. - if (accessTokenJson.error === 'authorization_pending') {
  333. - continue;
  334. - }
  335. -
  336. - if (accessTokenJson.error) {
  337. - throw new Error(accessTokenJson.error_description);
  338. - }
  339. -
  340. - return accessTokenJson.access_token;
  341. - }
  342. -
  343. - throw new Error(TIMED_OUT_ERROR);
  344. - });
  345. - }
  346. -
  347. - private handleUri: (scopes: string) => PromiseAdapter<vscode.Uri, string> =
  348. - (scopes) => (uri, resolve, reject) => {
  349. - const query = new URLSearchParams(uri.query);
  350. - const code = query.get('code');
  351. - const nonce = query.get('nonce');
  352. - if (!code) {
  353. - reject(new Error('No code'));
  354. - return;
  355. - }
  356. - if (!nonce) {
  357. - reject(new Error('No nonce'));
  358. - return;
  359. - }
  360. -
  361. - const acceptedNonces = this._pendingNonces.get(scopes) || [];
  362. - if (!acceptedNonces.includes(nonce)) {
  363. - // A common scenario of this happening is if you:
  364. - // 1. Trigger a sign in with one set of scopes
  365. - // 2. Before finishing 1, you trigger a sign in with a different set of scopes
  366. - // In this scenario we should just return and wait for the next UriHandler event
  367. - // to run as we are probably still waiting on the user to hit 'Continue'
  368. - this._logger.info('Nonce not found in accepted nonces. Skipping this execution...');
  369. - return;
  370. - }
  371. -
  372. - resolve(this.exchangeCodeForToken(code));
  373. - };
  374. -
  375. - private async exchangeCodeForToken(code: string): Promise<string> {
  376. - this._logger.info('Exchanging code for token...');
  377. -
  378. - const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints');
  379. - const endpointUrl = proxyEndpoints?.github ? `${proxyEndpoints.github}login/oauth/access_token` : GITHUB_TOKEN_URL;
  380. -
  381. - const body = new URLSearchParams([['code', code]]);
  382. - if (this._type === AuthProviderType.githubEnterprise) {
  383. - body.append('github_enterprise', this.baseUri.toString(true));
  384. - body.append('redirect_uri', await this.getRedirectEndpoint());
  385. - }
  386. - const result = await fetching(endpointUrl, {
  387. - method: 'POST',
  388. - headers: {
  389. - Accept: 'application/json',
  390. - 'Content-Type': 'application/x-www-form-urlencoded',
  391. - 'Content-Length': body.toString()
  392. -
  393. - },
  394. - body: body.toString()
  395. - });
  396. -
  397. - if (result.ok) {
  398. - const json = await result.json();
  399. - this._logger.info('Token exchange success!');
  400. - return json.access_token;
  401. - } else {
  402. - const text = await result.text();
  403. - const error = new Error(text);
  404. - error.name = 'GitHubTokenExchangeError';
  405. - throw error;
  406. - }
  407. - }
  408. -
  409. private getServerUri(path: string = '') {
  410. diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts
  411. index ce04327..e66a7f5 100644
  412. --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts
  413. +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts
  414. @@ -355,3 +355,3 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem {
  415. - if (providers.length && !menus.length) {
  416. + if (!menus.length) {
  417. const noAccountsAvailableAction = disposables.add(new Action('noAccountsAvailable', localize('noAccounts', "You are not signed in to any accounts"), undefined, false));
  418. diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts
  419. index 5285663..08f9e42 100644
  420. --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts
  421. +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts
  422. @@ -274,12 +274,2 @@ export class AuthenticationService extends Disposable implements IAuthentication
  423. }
  424. -
  425. - if (!this._authenticationProviders.size) {
  426. - placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
  427. - command: {
  428. - id: 'noAuthenticationProviders',
  429. - title: nls.localize('loading', "Loading..."),
  430. - precondition: ContextKeyExpr.false()
  431. - },
  432. - });
  433. - }
  434. }