Skip to content

Commit 9b3e147

Browse files
GitHub Enterprise Auth improvements (microsoft#165082)
1. Namespace secrets based on the value of github-enterprise.uri to support "multiple separate GHES instances" 2. If the setting value disappears, continue using last set value. Fixes microsoft/vscode-pull-request-github#3992 3. Mark github-enterprise.uri as requires trust 3. Refactoring like: * UriHandler is handled in extension.ts and passed down everywhere since we can only have 1 instance of it * misc style (`private` usage, better `disposable` handling)
1 parent c60980c commit 9b3e147

File tree

4 files changed

+88
-63
lines changed

4 files changed

+88
-63
lines changed

extensions/github-authentication/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
"capabilities": {
2525
"virtualWorkspaces": true,
2626
"untrustedWorkspaces": {
27-
"supported": true
27+
"supported": "limited",
28+
"restrictedConfigurations": [
29+
"github-enterprise.uri"
30+
]
2831
}
2932
},
3033
"contributes": {

extensions/github-authentication/src/extension.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,42 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as vscode from 'vscode';
7-
import { GitHubAuthenticationProvider, AuthProviderType } from './github';
7+
import { GitHubAuthenticationProvider, UriEventHandler } from './github';
88

9-
export function activate(context: vscode.ExtensionContext) {
10-
context.subscriptions.push(new GitHubAuthenticationProvider(context, AuthProviderType.github));
9+
function initGHES(context: vscode.ExtensionContext, uriHandler: UriEventHandler) {
10+
const settingValue = vscode.workspace.getConfiguration().get<string>('github-enterprise.uri');
11+
if (!settingValue) {
12+
return undefined;
13+
}
1114

12-
let githubEnterpriseAuthProvider: GitHubAuthenticationProvider | undefined;
13-
if (vscode.workspace.getConfiguration().get<string>('github-enterprise.uri')) {
14-
githubEnterpriseAuthProvider = new GitHubAuthenticationProvider(context, AuthProviderType.githubEnterprise);
15-
context.subscriptions.push(githubEnterpriseAuthProvider);
15+
// validate user value
16+
let uri: vscode.Uri;
17+
try {
18+
uri = vscode.Uri.parse(settingValue, true);
19+
} catch (e) {
20+
vscode.window.showErrorMessage(vscode.l10n.t('GitHub Enterprise Server URI is not a valid URI: {0}', e.message ?? e));
21+
return;
1622
}
1723

24+
const githubEnterpriseAuthProvider = new GitHubAuthenticationProvider(context, uriHandler, uri);
25+
context.subscriptions.push(githubEnterpriseAuthProvider);
26+
return githubEnterpriseAuthProvider;
27+
}
28+
29+
export function activate(context: vscode.ExtensionContext) {
30+
const uriHandler = new UriEventHandler();
31+
context.subscriptions.push(uriHandler);
32+
context.subscriptions.push(vscode.window.registerUriHandler(uriHandler));
33+
34+
context.subscriptions.push(new GitHubAuthenticationProvider(context, uriHandler));
35+
36+
let githubEnterpriseAuthProvider: GitHubAuthenticationProvider | undefined = initGHES(context, uriHandler);
37+
1838
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(async e => {
1939
if (e.affectsConfiguration('github-enterprise.uri')) {
20-
if (!githubEnterpriseAuthProvider && vscode.workspace.getConfiguration().get<string>('github-enterprise.uri')) {
21-
githubEnterpriseAuthProvider = new GitHubAuthenticationProvider(context, AuthProviderType.githubEnterprise);
22-
context.subscriptions.push(githubEnterpriseAuthProvider);
40+
if (vscode.workspace.getConfiguration().get<string>('github-enterprise.uri')) {
41+
githubEnterpriseAuthProvider?.dispose();
42+
githubEnterpriseAuthProvider = initGHES(context, uriHandler);
2343
}
2444
}
2545
}));

extensions/github-authentication/src/github.ts

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,27 +28,49 @@ export enum AuthProviderType {
2828
githubEnterprise = 'github-enterprise'
2929
}
3030

31+
export class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
32+
public handleUri(uri: vscode.Uri) {
33+
this.fire(uri);
34+
}
35+
}
36+
3137
export class GitHubAuthenticationProvider implements vscode.AuthenticationProvider, vscode.Disposable {
32-
private _sessionChangeEmitter = new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
33-
private _logger = new Log(this.type);
34-
private _githubServer: IGitHubServer;
35-
private _telemetryReporter: ExperimentationTelemetry;
38+
private readonly _sessionChangeEmitter = new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
39+
private readonly _logger: Log;
40+
private readonly _githubServer: IGitHubServer;
41+
private readonly _telemetryReporter: ExperimentationTelemetry;
42+
private readonly _keychain: Keychain;
43+
private readonly _accountsSeen = new Set<string>();
44+
private readonly _disposable: vscode.Disposable | undefined;
3645

37-
private _keychain: Keychain = new Keychain(this.context, `${this.type}.auth`, this._logger);
3846
private _sessionsPromise: Promise<vscode.AuthenticationSession[]>;
39-
private _accountsSeen = new Set<string>();
40-
private _disposable: vscode.Disposable;
4147

42-
constructor(private readonly context: vscode.ExtensionContext, private readonly type: AuthProviderType) {
48+
constructor(
49+
private readonly context: vscode.ExtensionContext,
50+
uriHandler: UriEventHandler,
51+
ghesUri?: vscode.Uri
52+
) {
4353
const { name, version, aiKey } = context.extension.packageJSON as { name: string; version: string; aiKey: string };
4454
this._telemetryReporter = new ExperimentationTelemetry(context, new TelemetryReporter(name, version, aiKey));
4555

56+
const type = ghesUri ? AuthProviderType.githubEnterprise : AuthProviderType.github;
57+
58+
this._logger = new Log(type);
59+
60+
this._keychain = new Keychain(
61+
this.context,
62+
type === AuthProviderType.github
63+
? `${type}.auth`
64+
: `${ghesUri?.authority}${ghesUri?.path}.ghes.auth`,
65+
this._logger);
66+
4667
this._githubServer = new GitHubServer(
47-
this.type,
68+
this._logger,
69+
this._telemetryReporter,
70+
uriHandler,
4871
// We only can use the Device Code flow when we have a full node environment because of CORS.
4972
context.extension.extensionKind === vscode.ExtensionKind.Workspace || vscode.env.uiKind === vscode.UIKind.Desktop,
50-
this._logger,
51-
this._telemetryReporter);
73+
ghesUri);
5274

5375
// Contains the current state of the sessions we have available.
5476
this._sessionsPromise = this.readSessions().then((sessions) => {
@@ -59,14 +81,13 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
5981

6082
this._disposable = vscode.Disposable.from(
6183
this._telemetryReporter,
62-
this._githubServer,
6384
vscode.authentication.registerAuthenticationProvider(type, this._githubServer.friendlyName, this, { supportsMultipleAccounts: false }),
6485
this.context.secrets.onDidChange(() => this.checkForUpdates())
6586
);
6687
}
6788

6889
dispose() {
69-
this._disposable.dispose();
90+
this._disposable?.dispose();
7091
}
7192

7293
get onDidChangeSessions() {

extensions/github-authentication/src/githubServer.ts

Lines changed: 20 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import fetch, { Response } from 'node-fetch';
88
import { v4 as uuid } from 'uuid';
99
import { PromiseAdapter, promiseFromEvent } from './common/utils';
1010
import { ExperimentationTelemetry } from './experimentationService';
11-
import { AuthProviderType } from './github';
11+
import { AuthProviderType, UriEventHandler } from './github';
1212
import { Log } from './common/logger';
1313
import { isSupportedEnvironment } from './common/env';
1414
import { LoopbackAuthServer } from './authServer';
@@ -21,23 +21,11 @@ const NETWORK_ERROR = 'network error';
2121
const REDIRECT_URL_STABLE = 'https://vscode.dev/redirect';
2222
const REDIRECT_URL_INSIDERS = 'https://insiders.vscode.dev/redirect';
2323

24-
class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
25-
constructor(private readonly Logger: Log) {
26-
super();
27-
}
28-
29-
public handleUri(uri: vscode.Uri) {
30-
this.Logger.trace('Handling Uri...');
31-
this.fire(uri);
32-
}
33-
}
34-
35-
export interface IGitHubServer extends vscode.Disposable {
24+
export interface IGitHubServer {
3625
login(scopes: string): Promise<string>;
3726
getUserInfo(token: string): Promise<{ id: string; accountName: string }>;
3827
sendAdditionalTelemetryInfo(token: string): Promise<void>;
3928
friendlyName: string;
40-
type: AuthProviderType;
4129
}
4230

4331
interface IGitHubDeviceCodeResponse {
@@ -73,38 +61,35 @@ async function getScopes(token: string, serverUri: vscode.Uri, logger: Log): Pro
7361
export class GitHubServer implements IGitHubServer {
7462
readonly friendlyName: string;
7563

76-
private _pendingNonces = new Map<string, string[]>();
77-
private _codeExchangePromises = new Map<string, { promise: Promise<string>; cancel: vscode.EventEmitter<void> }>();
78-
private _disposable: vscode.Disposable | undefined;
79-
private static _uriHandler: UriEventHandler | undefined;
64+
private readonly _pendingNonces = new Map<string, string[]>();
65+
private readonly _codeExchangePromises = new Map<string, { promise: Promise<string>; cancel: vscode.EventEmitter<void> }>();
66+
private readonly _type: AuthProviderType;
67+
8068
private _redirectEndpoint: string | undefined;
8169

8270
constructor(
83-
public readonly type: AuthProviderType,
84-
private readonly _supportDeviceCodeFlow: boolean,
8571
private readonly _logger: Log,
86-
private readonly _telemetryReporter: ExperimentationTelemetry
72+
private readonly _telemetryReporter: ExperimentationTelemetry,
73+
private readonly _uriHandler: UriEventHandler,
74+
private readonly _supportDeviceCodeFlow: boolean,
75+
private readonly _ghesUri?: vscode.Uri
8776
) {
88-
this.friendlyName = type === AuthProviderType.github ? 'GitHub' : 'GitHub Enterprise';
89-
90-
if (!GitHubServer._uriHandler) {
91-
GitHubServer._uriHandler = new UriEventHandler(this._logger);
92-
this._disposable = vscode.window.registerUriHandler(GitHubServer._uriHandler);
93-
}
77+
this._type = _ghesUri ? AuthProviderType.githubEnterprise : AuthProviderType.github;
78+
this.friendlyName = this._type === AuthProviderType.github ? 'GitHub' : _ghesUri?.authority!;
9479
}
9580

9681
get baseUri() {
97-
if (this.type === AuthProviderType.github) {
82+
if (this._type === AuthProviderType.github) {
9883
return vscode.Uri.parse('https://github.com/');
9984
}
100-
return vscode.Uri.parse(vscode.workspace.getConfiguration('github-enterprise').get<string>('uri') || '', true);
85+
return this._ghesUri!;
10186
}
10287

10388
private async getRedirectEndpoint(): Promise<string> {
10489
if (this._redirectEndpoint) {
10590
return this._redirectEndpoint;
10691
}
107-
if (this.type === AuthProviderType.github) {
92+
if (this._type === AuthProviderType.github) {
10893
const proxyEndpoints = await vscode.commands.executeCommand<{ [providerId: string]: string } | undefined>('workbench.getCodeExchangeProxyEndpoints');
10994
// If we are running in insiders vscode.dev, then ensure we use the redirect route on that.
11095
this._redirectEndpoint = REDIRECT_URL_STABLE;
@@ -139,10 +124,6 @@ export class GitHubServer implements IGitHubServer {
139124
return this._redirectEndpoint;
140125
}
141126

142-
dispose() {
143-
this._disposable?.dispose();
144-
}
145-
146127
// TODO@joaomoreno TODO@TylerLeonhardt
147128
private async isNoCorsEnvironment(): Promise<boolean> {
148129
const uri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/dummy`));
@@ -246,7 +227,7 @@ export class GitHubServer implements IGitHubServer {
246227
// before completing it.
247228
let codeExchangePromise = this._codeExchangePromises.get(scopes);
248229
if (!codeExchangePromise) {
249-
codeExchangePromise = promiseFromEvent(GitHubServer._uriHandler!.event, this.handleUri(scopes));
230+
codeExchangePromise = promiseFromEvent(this._uriHandler!.event, this.handleUri(scopes));
250231
this._codeExchangePromises.set(scopes, codeExchangePromise);
251232
}
252233

@@ -467,7 +448,7 @@ export class GitHubServer implements IGitHubServer {
467448
const endpointUrl = proxyEndpoints?.github ? `${proxyEndpoints.github}login/oauth/access_token` : GITHUB_TOKEN_URL;
468449

469450
const body = new URLSearchParams([['code', code]]);
470-
if (this.type === AuthProviderType.githubEnterprise) {
451+
if (this._type === AuthProviderType.githubEnterprise) {
471452
body.append('github_enterprise', this.baseUri.toString(true));
472453
body.append('redirect_uri', await this.getRedirectEndpoint());
473454
}
@@ -495,11 +476,11 @@ export class GitHubServer implements IGitHubServer {
495476
}
496477

497478
private getServerUri(path: string = '') {
498-
if (this.type === AuthProviderType.github) {
479+
if (this._type === AuthProviderType.github) {
499480
return vscode.Uri.parse('https://api.github.com').with({ path });
500481
}
501482
// GHES
502-
const apiUri = vscode.Uri.parse(vscode.workspace.getConfiguration('github-enterprise').get<string>('uri') || '', true);
483+
const apiUri = this.baseUri;
503484
return vscode.Uri.parse(`${apiUri.scheme}://${apiUri.authority}/api/v3${path}`);
504485
}
505486

@@ -553,7 +534,7 @@ export class GitHubServer implements IGitHubServer {
553534
return;
554535
}
555536

556-
if (this.type === AuthProviderType.github) {
537+
if (this._type === AuthProviderType.github) {
557538
return await this.checkEduDetails(token);
558539
}
559540

0 commit comments

Comments
 (0)