use-github-pat.patch 17 KB

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