mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 04:49:35 +00:00
Use device flow over PAT when we are running in a server full environment but not in a supported uri (#139255)
* initial attempt * use github-authentication instead * rework error handling * update copy * explain why Workspace
This commit is contained in:
parent
e7b3724e0c
commit
f67a8b753f
|
@ -29,6 +29,7 @@ const extensionsPath = path.join(path.dirname(__dirname), 'extensions');
|
|||
// ignore: ['**/out/**', '**/node_modules/**']
|
||||
// });
|
||||
const compilations = [
|
||||
'authentication-proxy/tsconfig.json',
|
||||
'configuration-editing/build/tsconfig.json',
|
||||
'configuration-editing/tsconfig.json',
|
||||
'css-language-features/client/tsconfig.json',
|
||||
|
|
18
extensions/github-authentication/src/common/env.ts
Normal file
18
extensions/github-authentication/src/common/env.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Uri } from 'vscode';
|
||||
|
||||
const VALID_DESKTOP_CALLBACK_SCHEMES = [
|
||||
'vscode',
|
||||
'vscode-insiders',
|
||||
'code-oss',
|
||||
'vscode-wsl',
|
||||
'vscode-exploration'
|
||||
];
|
||||
|
||||
// This comes from the GitHub Authentication server
|
||||
export function isSupportedEnvironment(url: Uri): boolean {
|
||||
return VALID_DESKTOP_CALLBACK_SCHEMES.includes(url.scheme) || url.authority.endsWith('vscode.dev') || url.authority.endsWith('github.dev');
|
||||
}
|
|
@ -43,7 +43,11 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
|
|||
this._telemetryReporter = new ExperimentationTelemetry(context, new TelemetryReporter(name, version, aiKey));
|
||||
|
||||
if (this.type === AuthProviderType.github) {
|
||||
this._githubServer = new GitHubServer(this._logger, this._telemetryReporter);
|
||||
this._githubServer = new GitHubServer(
|
||||
// We only can use the Device Code flow when we are running with a remote extension host.
|
||||
context.extension.extensionKind === vscode.ExtensionKind.Workspace,
|
||||
this._logger,
|
||||
this._telemetryReporter);
|
||||
} else {
|
||||
this._githubServer = new GitHubEnterpriseServer(this._logger, this._telemetryReporter);
|
||||
}
|
||||
|
@ -216,7 +220,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
|
|||
return session;
|
||||
} catch (e) {
|
||||
// If login was cancelled, do not notify user.
|
||||
if (e === 'Cancelled') {
|
||||
if (e === 'Cancelled' || e.message === 'Cancelled') {
|
||||
/* __GDPR__
|
||||
"loginCancelled" : { }
|
||||
*/
|
||||
|
|
|
@ -11,8 +11,10 @@ import { PromiseAdapter, promiseFromEvent } from './common/utils';
|
|||
import { ExperimentationTelemetry } from './experimentationService';
|
||||
import { AuthProviderType } from './github';
|
||||
import { Log } from './common/logger';
|
||||
import { isSupportedEnvironment } from './common/env';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
const CLIENT_ID = '01ab8ac9400c4e429b23';
|
||||
|
||||
const NETWORK_ERROR = 'network error';
|
||||
const AUTH_RELAY_SERVER = 'vscode-auth.github.com';
|
||||
|
@ -45,6 +47,13 @@ export interface IGitHubServer extends vscode.Disposable {
|
|||
type: AuthProviderType;
|
||||
}
|
||||
|
||||
interface IGitHubDeviceCodeResponse {
|
||||
device_code: string;
|
||||
user_code: string;
|
||||
verification_uri: string;
|
||||
interval: number;
|
||||
}
|
||||
|
||||
async function getScopes(token: string, serverUri: vscode.Uri, logger: Log): Promise<string[]> {
|
||||
try {
|
||||
logger.info('Getting token scopes...');
|
||||
|
@ -105,7 +114,7 @@ export class GitHubServer implements IGitHubServer {
|
|||
private _disposable: vscode.Disposable;
|
||||
private _uriHandler = new UriEventHandler(this._logger);
|
||||
|
||||
constructor(private readonly _logger: Log, private readonly _telemetryReporter: ExperimentationTelemetry) {
|
||||
constructor(private readonly _supportDeviceCodeFlow: boolean, private readonly _logger: Log, private readonly _telemetryReporter: ExperimentationTelemetry) {
|
||||
this._disposable = vscode.Disposable.from(
|
||||
vscode.commands.registerCommand(this._statusBarCommandId, () => this.manuallyProvideUri()),
|
||||
vscode.window.registerUriHandler(this._uriHandler));
|
||||
|
@ -115,10 +124,6 @@ export class GitHubServer implements IGitHubServer {
|
|||
this._disposable.dispose();
|
||||
}
|
||||
|
||||
private isTestEnvironment(url: vscode.Uri): boolean {
|
||||
return /\.azurewebsites\.net$/.test(url.authority) || url.authority.startsWith('localhost:');
|
||||
}
|
||||
|
||||
// TODO@joaomoreno TODO@TylerLeonhardt
|
||||
private async isNoCorsEnvironment(): Promise<boolean> {
|
||||
const uri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/dummy`));
|
||||
|
@ -130,9 +135,12 @@ export class GitHubServer implements IGitHubServer {
|
|||
|
||||
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate`));
|
||||
|
||||
if (this.isTestEnvironment(callbackUri)) {
|
||||
const token = await vscode.window.showInputBox({ prompt: 'GitHub Personal Access Token', ignoreFocusOut: true });
|
||||
if (!token) { throw new Error('Sign in failed: No token provided'); }
|
||||
if (!isSupportedEnvironment(callbackUri)) {
|
||||
const token = this._supportDeviceCodeFlow
|
||||
? await this.doDeviceCodeFlow(scopes)
|
||||
: await vscode.window.showInputBox({ prompt: 'GitHub Personal Access Token', ignoreFocusOut: true });
|
||||
|
||||
if (!token) { throw new Error('No token provided'); }
|
||||
|
||||
const tokenScopes = await getScopes(token, this.getServerUri('/'), this._logger); // Example: ['repo', 'user']
|
||||
const scopesList = scopes.split(' '); // Example: 'read:user repo user:email'
|
||||
|
@ -187,6 +195,96 @@ export class GitHubServer implements IGitHubServer {
|
|||
});
|
||||
}
|
||||
|
||||
private async doDeviceCodeFlow(scopes: string): Promise<string> {
|
||||
// Get initial device code
|
||||
const uri = `https://github.com/login/device/code?client_id=${CLIENT_ID}&scope=${scopes}`;
|
||||
const result = await fetch(uri, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
if (!result.ok) {
|
||||
throw new Error(`Failed to get one-time code: ${await result.text()}`);
|
||||
}
|
||||
|
||||
const json = await result.json() as IGitHubDeviceCodeResponse;
|
||||
|
||||
await vscode.env.clipboard.writeText(json.user_code);
|
||||
|
||||
const modalResult = await vscode.window.showInformationMessage(
|
||||
localize('code.title', "Your Code: {0}", json.user_code),
|
||||
{
|
||||
modal: true,
|
||||
detail: localize('code.detail', "The above one-time code has been copied to your clipboard. To finish authenticating, paste it on GitHub.")
|
||||
}, 'Continue to GitHub');
|
||||
|
||||
if (modalResult !== 'Continue to GitHub') {
|
||||
throw new Error('Cancelled');
|
||||
}
|
||||
|
||||
const uriToOpen = await vscode.env.asExternalUri(vscode.Uri.parse(json.verification_uri));
|
||||
await vscode.env.openExternal(uriToOpen);
|
||||
|
||||
return await vscode.window.withProgress<string>({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
cancellable: true,
|
||||
title: localize(
|
||||
'progress',
|
||||
"Open [{0}]({0}) in a new tab and paste your one-time code: {1}",
|
||||
json.verification_uri,
|
||||
json.user_code)
|
||||
}, async (_, token) => {
|
||||
return await this.waitForDeviceCodeAccessToken(json, token);
|
||||
});
|
||||
}
|
||||
|
||||
private async waitForDeviceCodeAccessToken(
|
||||
json: IGitHubDeviceCodeResponse,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<string> {
|
||||
|
||||
const refreshTokenUri = `https://github.com/login/oauth/access_token?client_id=${CLIENT_ID}&device_code=${json.device_code}&grant_type=urn:ietf:params:oauth:grant-type:device_code`;
|
||||
|
||||
// Try for 2 minutes
|
||||
const attempts = 120 / json.interval;
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, json.interval * 1000));
|
||||
if (token.isCancellationRequested) {
|
||||
throw new Error('Cancelled');
|
||||
}
|
||||
let accessTokenResult;
|
||||
try {
|
||||
accessTokenResult = await fetch(refreshTokenUri, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!accessTokenResult.ok) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const accessTokenJson = await accessTokenResult.json();
|
||||
|
||||
if (accessTokenJson.error === 'authorization_pending') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (accessTokenJson.error) {
|
||||
throw new Error(accessTokenJson.error_description);
|
||||
}
|
||||
|
||||
return accessTokenJson.access_token;
|
||||
}
|
||||
|
||||
throw new Error('Cancelled');
|
||||
}
|
||||
|
||||
private exchangeCodeForToken: (scopes: string) => PromiseAdapter<vscode.Uri, string> =
|
||||
(scopes) => async (uri, resolve, reject) => {
|
||||
const query = parseQuery(uri);
|
||||
|
|
Loading…
Reference in a new issue