Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend Github Authentication to support seamless SSO #209482

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion extensions/github-authentication/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@
"properties": {
"github-enterprise.uri": {
"type": "string",
"description": "GitHub Enterprise Server URI"
"description": "GitHub Enterprise Server URI. Can't be used along with github-enterprise.sso-id"
},
"github-enterprise.sso-id": {
"type": "string",
"description": "GitHub Enterprise Name. Can't be used along with github-enterprise.uri"
}
}
}
Expand Down
22 changes: 18 additions & 4 deletions extensions/github-authentication/src/common/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri } from 'vscode';
import { AuthProviderType } from '../github';
import { AuthProviderType, EnterpriseSettings } from '../github';
import { GitHubTarget } from '../flows';

const VALID_DESKTOP_CALLBACK_SCHEMES = [
'vscode',
Expand All @@ -26,13 +27,26 @@ export function isSupportedClient(uri: Uri): boolean {
);
}

export function isSupportedTarget(type: AuthProviderType, gheUri?: Uri): boolean {
export function isSupportedTarget(type: AuthProviderType, enterpriseSettings?: EnterpriseSettings): boolean {
return (
type === AuthProviderType.github ||
isHostedGitHubEnterprise(gheUri!)
type === AuthProviderType.github || enterpriseSettings?.ssoId !== undefined ||
isHostedGitHubEnterprise(enterpriseSettings?.uri!)
);
}

export function isHostedGitHubEnterprise(uri: Uri): boolean {
return /\.ghe\.com$/.test(uri.authority);
}

export function getTarget(enterpriseSettings?: EnterpriseSettings): GitHubTarget {
if (!enterpriseSettings) {
return GitHubTarget.DotCom;
}

if (enterpriseSettings.uri) {
return isHostedGitHubEnterprise(enterpriseSettings.uri!) ? GitHubTarget.HostedEnterprise : GitHubTarget.Enterprise;
}

return GitHubTarget.EnterpriseSSO;

}
66 changes: 43 additions & 23 deletions extensions/github-authentication/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,55 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { GitHubAuthenticationProvider, UriEventHandler } from './github';
import { EnterpriseSettings, GitHubAuthenticationProvider, UriEventHandler } from './github';

function initGHES(context: vscode.ExtensionContext, uriHandler: UriEventHandler) {
const settingValue = vscode.workspace.getConfiguration().get<string>('github-enterprise.uri');
if (!settingValue) {
function areEnterpriseSettingsValid(enterpriseUri?: string, enterpriseSsoId?: string): { valid: boolean; error?: string } {
if (enterpriseUri && enterpriseSsoId) {
return { valid: false, error: vscode.l10n.t('Only one of github-enterprise.uri and github-enterprise.sso-id are allowed') };
}
if (enterpriseUri) {
try {
vscode.Uri.parse(enterpriseUri, true);
} catch (e) {
return { valid: false, error: vscode.l10n.t('GitHub Enterprise Server URI is not a valid URI: {0}', e.message ?? e) };
}
} else if (enterpriseSsoId && (enterpriseSsoId.includes('/') || enterpriseSsoId.includes('.'))) {
return { valid: false, error: vscode.l10n.t('GitHub Enterprise SSO ID is not valid') };
}
return { valid: true };
}

function setupGHES(context: vscode.ExtensionContext, uriHandler: UriEventHandler) {
const uriSettingKey = 'github-enterprise.uri';
const ssoIdSettingKey = 'github-enterprise.sso-id';
const uriValue = vscode.workspace.getConfiguration().get<string>(uriSettingKey);
const ssoIdValue = vscode.workspace.getConfiguration().get<string>(ssoIdSettingKey);
if (!uriValue && !ssoIdValue) {
return undefined;
}
let authProvider: GitHubAuthenticationProvider | undefined = initGHES(context, uriHandler, uriValue, ssoIdValue);

context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(async e => {
if (e.affectsConfiguration(uriSettingKey) || e.affectsConfiguration(ssoIdSettingKey)) {
const uriValue = vscode.workspace.getConfiguration().get<string>(uriSettingKey);
const nameValue = vscode.workspace.getConfiguration().get<string>(ssoIdSettingKey);
if (uriValue || nameValue) {
authProvider?.dispose();
authProvider = initGHES(context, uriHandler, uriValue, nameValue);
}
}
}));
}

// validate user value
let uri: vscode.Uri;
try {
uri = vscode.Uri.parse(settingValue, true);
} catch (e) {
vscode.window.showErrorMessage(vscode.l10n.t('GitHub Enterprise Server URI is not a valid URI: {0}', e.message ?? e));
function initGHES(context: vscode.ExtensionContext, uriHandler: UriEventHandler, uri?: string, ssoId?: string) {
const { valid, error } = areEnterpriseSettingsValid(uri, ssoId);
if (!valid) {
vscode.window.showErrorMessage(error!);
return;
}

const githubEnterpriseAuthProvider = new GitHubAuthenticationProvider(context, uriHandler, uri);
const enterpriseSettings = new EnterpriseSettings(uri, ssoId);
const githubEnterpriseAuthProvider = new GitHubAuthenticationProvider(context, uriHandler, enterpriseSettings);
context.subscriptions.push(githubEnterpriseAuthProvider);
return githubEnterpriseAuthProvider;
}
Expand All @@ -30,17 +61,6 @@ export function activate(context: vscode.ExtensionContext) {
const uriHandler = new UriEventHandler();
context.subscriptions.push(uriHandler);
context.subscriptions.push(vscode.window.registerUriHandler(uriHandler));

context.subscriptions.push(new GitHubAuthenticationProvider(context, uriHandler));

let githubEnterpriseAuthProvider: GitHubAuthenticationProvider | undefined = initGHES(context, uriHandler);

context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(async e => {
if (e.affectsConfiguration('github-enterprise.uri')) {
if (vscode.workspace.getConfiguration().get<string>('github-enterprise.uri')) {
githubEnterpriseAuthProvider?.dispose();
githubEnterpriseAuthProvider = initGHES(context, uriHandler);
}
}
}));
setupGHES(context, uriHandler);
}
80 changes: 57 additions & 23 deletions extensions/github-authentication/src/flows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import * as path from 'path';
import { ProgressLocation, Uri, commands, env, l10n, window } from 'vscode';
import { Log } from './common/logger';
import { Config } from './config';
import { UriEventHandler } from './github';
import { EnterpriseSettings, UriEventHandler } from './github';
import { fetching } from './node/fetch';
import { LoopbackAuthServer } from './node/authServer';
import { promiseFromEvent } from './common/utils';
import { isHostedGitHubEnterprise } from './common/env';
import { NETWORK_ERROR, TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors';
import { URLSearchParams } from 'url';

interface IGitHubDeviceCodeResponse {
device_code: string;
Expand All @@ -28,6 +29,8 @@ interface IFlowOptions {
readonly supportsGitHubEnterpriseServer: boolean;
// A GitHub Enterprise Server that is hosted by GitHub for an organization
readonly supportsHostedGitHubEnterprise: boolean;
// A Github Enterprise Server that is hosted by Github but does not follow the <org>.ghe.com domain format
readonly supportsGithubEnterpriseSSO: boolean;

// Runtimes - there are constraints on which runtimes support which flows
readonly supportsWebWorkerExtensionHost: boolean;
Expand All @@ -44,7 +47,8 @@ interface IFlowOptions {
export const enum GitHubTarget {
DotCom,
Enterprise,
HostedEnterprise
HostedEnterprise,
EnterpriseSSO,
}

export const enum ExtensionHost {
Expand All @@ -68,6 +72,7 @@ interface IFlowTriggerOptions {
callbackUri: Uri;
uriHandler: UriEventHandler;
enterpriseUri?: Uri;
enterpriseSettings?: EnterpriseSettings;
existingLogin?: string;
}

Expand Down Expand Up @@ -134,6 +139,7 @@ const allFlows: IFlow[] = [
// other flows that work well.
supportsGitHubEnterpriseServer: false,
supportsHostedGitHubEnterprise: true,
supportsGithubEnterpriseSSO: true,
supportsRemoteExtensionHost: true,
supportsWebWorkerExtensionHost: true,
// exchanging a code for a token requires a client secret
Expand All @@ -150,8 +156,8 @@ const allFlows: IFlow[] = [
nonce,
callbackUri,
uriHandler,
enterpriseUri,
existingLogin
existingLogin,
enterpriseSettings,
}: IFlowTriggerOptions): Promise<string> {
logger.info(`Trying without local server... (${scopes})`);
return await window.withProgress<string>({
Expand All @@ -164,33 +170,44 @@ const allFlows: IFlow[] = [
cancellable: true
}, async (_, token) => {
const promise = uriHandler.waitForCode(logger, scopes, nonce, token);

const searchParams = new URLSearchParams([
const loginAuthorizeSearchParams = new URLSearchParams([
['client_id', Config.gitHubClientId],
['redirect_uri', redirectUri.toString(true)],
['scope', scopes],
['state', encodeURIComponent(callbackUri.toString(true))]
['state', encodeURIComponent(callbackUri.toString(true))],
]);
if (existingLogin) {
searchParams.append('login', existingLogin);
loginAuthorizeSearchParams.append('login', existingLogin);
}

// The extra toString, parse is apparently needed for env.openExternal
// to open the correct URL.
const uri = Uri.parse(baseUri.with({
const loginAuthorizeUri = baseUri.with({
path: '/login/oauth/authorize',
query: searchParams.toString()
}).toString(true));
await env.openExternal(uri);
query: loginAuthorizeSearchParams.toString()
});
let uri = loginAuthorizeUri;

if (enterpriseSettings?.ssoId !== undefined) {
const searchParams = new URLSearchParams([
['return_to', encodeURIComponent(loginAuthorizeUri.toString(true))],
]);
uri = Uri.parse(baseUri.with({
path: `/enterprises/${enterpriseSettings.ssoId}/sso`,
query: searchParams.toString()
}).toString(true));
}

logger.info(`opening ${uri.toString(true)}`);
// This cast from string to any is needed due to issue described in https://github.com/microsoft/vscode/issues/85930
await env.openExternal(<any>(uri.toString(true)));

const code = await promise;
logger.info(`received code`);

const proxyEndpoints: { [providerId: string]: string } | undefined = await commands.executeCommand('workbench.getCodeExchangeProxyEndpoints');
const endpointUrl = proxyEndpoints?.github
? Uri.parse(`${proxyEndpoints.github}login/oauth/access_token`)
: baseUri.with({ path: '/login/oauth/access_token' });

const accessToken = await exchangeCodeForToken(logger, endpointUrl, redirectUri, code, enterpriseUri);
const accessToken = await exchangeCodeForToken(logger, endpointUrl, redirectUri, code, enterpriseSettings?.uri);
return accessToken;
});
}
Expand All @@ -205,6 +222,7 @@ const allFlows: IFlow[] = [
// other flows that work well.
supportsGitHubEnterpriseServer: false,
supportsHostedGitHubEnterprise: true,
supportsGithubEnterpriseSSO: false,
// Opening a port on the remote side can't be open in the browser on
// the client side so this flow won't work in remote extension hosts
supportsRemoteExtensionHost: false,
Expand All @@ -220,8 +238,8 @@ const allFlows: IFlow[] = [
baseUri,
redirectUri,
logger,
enterpriseUri,
existingLogin
existingLogin,
enterpriseSettings,
}: IFlowTriggerOptions): Promise<string> {
logger.info(`Trying with local server... (${scopes})`);
return await window.withProgress<string>({
Expand Down Expand Up @@ -269,7 +287,7 @@ const allFlows: IFlow[] = [
baseUri.with({ path: '/login/oauth/access_token' }),
redirectUri,
codeToExchange,
enterpriseUri);
enterpriseSettings?.uri);
return accessToken;
});
}
Expand All @@ -280,16 +298,16 @@ const allFlows: IFlow[] = [
supportsGitHubDotCom: true,
supportsGitHubEnterpriseServer: true,
supportsHostedGitHubEnterprise: true,
supportsGithubEnterpriseSSO: true,
supportsRemoteExtensionHost: true,
// CORS prevents this from working in web workers
supportsWebWorkerExtensionHost: false,
supportsNoClientSecret: true,
supportsSupportedClients: true,
supportsUnsupportedClients: true
};
async trigger({ scopes, baseUri, logger }: IFlowTriggerOptions) {
async trigger({ scopes, baseUri, logger, enterpriseSettings }: IFlowTriggerOptions) {
logger.info(`Trying device code flow... (${scopes})`);

// Get initial device code
const uri = baseUri.with({
path: '/login/device/code',
Expand All @@ -306,6 +324,7 @@ const allFlows: IFlow[] = [
}

const json = await result.json() as IGitHubDeviceCodeResponse;
logger.info(`json: ${json.verification_uri}`);

const button = l10n.t('Copy & Continue to GitHub');
const modalResult = await window.showInformationMessage(
Expand All @@ -321,8 +340,19 @@ const allFlows: IFlow[] = [

await env.clipboard.writeText(json.user_code);

const uriToOpen = await env.asExternalUri(Uri.parse(json.verification_uri));
await env.openExternal(uriToOpen);
let verificationUri = json.verification_uri;
if (enterpriseSettings?.ssoId !== undefined) {
verificationUri = baseUri.with({
path: `/enterprises/${enterpriseSettings.ssoId}/sso`,
query: new URLSearchParams([
['return_to', encodeURIComponent(verificationUri)],
]).toString(),
}).toString(true);
}

const uriToOpen = await env.asExternalUri(Uri.parse(verificationUri));
// This cast from string to any is needed due to issue described in https://github.com/microsoft/vscode/issues/85930
await env.openExternal(<any>(uriToOpen.toString(true)));

return await this.waitForDeviceCodeAccessToken(baseUri, json);
}
Expand Down Expand Up @@ -394,6 +424,7 @@ const allFlows: IFlow[] = [
supportsGitHubDotCom: true,
supportsGitHubEnterpriseServer: true,
supportsHostedGitHubEnterprise: true,
supportsGithubEnterpriseSSO: false,
supportsRemoteExtensionHost: true,
supportsWebWorkerExtensionHost: true,
supportsNoClientSecret: true,
Expand Down Expand Up @@ -484,6 +515,9 @@ export function getFlows(query: IFlowQuery) {
case GitHubTarget.HostedEnterprise:
useFlow &&= flow.options.supportsHostedGitHubEnterprise;
break;
case GitHubTarget.EnterpriseSSO:
useFlow &&= flow.options.supportsGithubEnterpriseSSO;
break;
}

switch (query.extensionHost) {
Expand Down
Loading