Skip to content

Commit

Permalink
URL and Device Code flow work with SSO login
Browse files Browse the repository at this point in the history
  • Loading branch information
imarban committed Apr 5, 2024
1 parent 884c8a6 commit 4fa8746
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 80 deletions.
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

0 comments on commit 4fa8746

Please sign in to comment.