Skip to content

Commit

Permalink
Adopt VSCode Authentication Provider API
Browse files Browse the repository at this point in the history
Fixes #1616
  • Loading branch information
Rachel Macfarlane authored Apr 17, 2020
1 parent 53927f7 commit 9f7d6a0
Show file tree
Hide file tree
Showing 18 changed files with 157 additions and 1,491 deletions.
21 changes: 2 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"*"
],
"extensionDependencies": [
"vscode.git"
"vscode.git",
"vscode.github-authentication"
],
"main": "./media/extension",
"contributes": {
Expand Down Expand Up @@ -356,11 +357,6 @@
]
},
"commands": [
{
"command": "auth.inputTokenCallback",
"title": "Manually Provide Authentication Response",
"category": "GitHub Pull Requests"
},
{
"command": "pr.create",
"title": "Create Pull Request",
Expand All @@ -379,11 +375,6 @@
},
"category": "GitHub Pull Requests"
},
{
"command": "auth.signout",
"title": "Sign out of GitHub",
"category": "GitHub Pull Requests"
},
{
"command": "pr.pick",
"title": "Checkout Pull Request",
Expand Down Expand Up @@ -665,14 +656,6 @@
],
"menus": {
"commandPalette": [
{
"command": "auth.inputTokenCallback",
"when": "gitOpenRepositoryCount != 0"
},
{
"command": "auth.signout",
"when": "gitOpenRepositoryCount != 0 && github:authenticated"
},
{
"command": "pr.configureRemotes",
"when": "gitOpenRepositoryCount != 0"
Expand Down
171 changes: 1 addition & 170 deletions src/authentication/githubServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,11 @@ import * as https from 'https';
import * as vscode from 'vscode';
import Logger from '../common/logger';
import { agent } from '../common/net';
import { handler as uriHandler } from '../common/uri';
import { PromiseAdapter, promiseFromEvent } from '../common/utils';
import { HostHelper, IHostConfiguration } from './configuration';
import { listHosts, onDidChange as onKeychainDidChange, toCanonical } from './keychain';
import uuid = require('uuid');
import { EXTENSION_ID } from '../constants';

const SCOPES: string = 'read:user user:email repo write:discussion';
const GHE_OPTIONAL_SCOPES: { [key: string]: boolean } = { 'write:discussion': true };

const AUTH_RELAY_SERVER = 'vscode-auth.github.com';
import { HostHelper } from './configuration';

export class GitHubManager {
private _servers: Map<string, boolean> = new Map().set('github.com', true);

private static GitHubScopesTable: { [key: string]: string[] } = {
repo: ['repo:status', 'repo_deployment', 'public_repo', 'repo:invite'],
'admin:org': ['write:org', 'read:org'],
'admin:public_key': ['write:public_key', 'read:public_key'],
'admin:org_hook': [],
gist: [],
notifications: [],
user: ['read:user', 'user:email', 'user:follow'],
delete_repo: [],
'write:discussion': ['read:discussion'],
'admin:gpg_key': ['write:gpg_key', 'read:gpg_key']
};

public static AppScopes: string[] = SCOPES.split(' ');

public async isGitHub(host: vscode.Uri): Promise<boolean> {
if (host === null) {
return false;
Expand All @@ -41,11 +16,6 @@ export class GitHubManager {
return !!this._servers.get(host.authority);
}

const keychainHosts = await listHosts();
if (keychainHosts.indexOf(toCanonical(host.authority)) !== -1) {
return true;
}

const options = await GitHubManager.getOptions(host, 'HEAD', '/rate_limit');
return new Promise<boolean>((resolve, _) => {
const get = https.request(options, res => {
Expand Down Expand Up @@ -85,143 +55,4 @@ export class GitHubManager {
agent,
};
}

public static validateScopes(host: vscode.Uri, scopes: string): boolean {
if (!scopes) {
Logger.appendLine(`[SKIP] validateScopes(${host.toString()}): No scopes available.`);
return true;
}
const tokenScopes = scopes.split(', ');
const scopesNotFound = this.AppScopes.filter(x => !(
tokenScopes.indexOf(x) >= 0 ||
tokenScopes.indexOf(this.getScopeSuperset(x)) >= 0 ||
// some scopes don't exist on older versions of GHE, treat them as optional
(this.isDotCom(host) || GHE_OPTIONAL_SCOPES[x])
));
if (scopesNotFound.length) {
Logger.appendLine(`[FAIL] validateScopes(${host.toString()}): ${scopesNotFound.length} scopes missing`);
scopesNotFound.forEach(scope => Logger.appendLine(` - ${scope}`));
return false;
}
return true;
}

private static getScopeSuperset(scope: string): string {
for (const key in this.GitHubScopesTable) {
if (this.GitHubScopesTable[key].indexOf(scope) >= 0) {
return key;
}
}
return scope;
}

private static isDotCom(host: vscode.Uri): boolean {
return host && host.authority.toLowerCase() === 'github.com';
}
}

const exchangeCodeForToken: (host: string, state: string) => PromiseAdapter<vscode.Uri, IHostConfiguration> =
(host, state) => async (uri, resolve, reject) => {
const query = parseQuery(uri);
const code = query.code;

if (query.state !== state) {
vscode.window.showInformationMessage('Sign in failed: Received bad state');
reject('Received bad state');
return;
}

const post = https.request({
host: AUTH_RELAY_SERVER,
path: `/token?code=${code}&state=${query.state}`,
method: 'POST',
headers: {
Accept: 'application/json'
}
}, result => {
const buffer: Buffer[] = [];
result.on('data', (chunk: Buffer) => {
buffer.push(chunk);
});
result.on('end', () => {
if (result.statusCode === 200) {
const json = JSON.parse(Buffer.concat(buffer).toString());
resolve({ host, token: json.access_token });
} else {
vscode.window.showInformationMessage(`Sign in failed: ${result.statusMessage}`);
reject(new Error(result.statusMessage));
}
});
});

post.end();
post.on('error', err => {
reject(err);
});
};

function parseQuery(uri: vscode.Uri) {
return uri.query.split('&').reduce((prev: any, current) => {
const queryString = current.split('=');
prev[queryString[0]] = queryString[1];
return prev;
}, {});
}

const manuallyEnteredToken: (host: string) => PromiseAdapter<IHostConfiguration, IHostConfiguration> =
host => (config: IHostConfiguration, resolve) =>
config.host === toCanonical(host) && resolve(config);

export class GitHubServer {
public hostConfiguration: IHostConfiguration;
private hostUri: vscode.Uri;

public constructor(host: string) {
host = host.toLocaleLowerCase();
this.hostConfiguration = { host, token: undefined };
this.hostUri = vscode.Uri.parse(host);
}

public async login(): Promise<IHostConfiguration> {
const state = uuid();
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://${EXTENSION_ID}/did-authenticate`));
const host = this.hostUri.toString();
const uri = vscode.Uri.parse(`https://${AUTH_RELAY_SERVER}/authorize/?callbackUri=${encodeURIComponent(callbackUri.toString())}&scope=${SCOPES}&state=${state}&responseType=code&authServer=${host}`);

vscode.env.openExternal(uri);
return Promise.race([
promiseFromEvent(uriHandler.event, exchangeCodeForToken(host, state)),
promiseFromEvent(onKeychainDidChange, manuallyEnteredToken(host))
]);
}

public async validate(token?: string): Promise<IHostConfiguration> {
if (!token) {
token = this.hostConfiguration.token;
}

const options = await GitHubManager.getOptions(this.hostUri, 'GET', '/user', token);

return new Promise<IHostConfiguration>((resolve, _) => {
const get = https.request(options, res => {
try {
if (res.statusCode === 200) {
const scopes = res.headers['x-oauth-scopes'] as string;
GitHubManager.validateScopes(this.hostUri, scopes);
resolve(this.hostConfiguration);
} else {
resolve(undefined);
}
} catch (e) {
Logger.appendLine(`validate() error ${e}`);
resolve(undefined);
}
});

get.end();
get.on('error', err => {
resolve(undefined);
});
});
}
}
112 changes: 0 additions & 112 deletions src/authentication/keychain.ts

This file was deleted.

27 changes: 0 additions & 27 deletions src/authentication/vsConfiguration.ts

This file was deleted.

Loading

0 comments on commit 9f7d6a0

Please sign in to comment.