Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@ -0,0 +1,89 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// keytar depends on a native module shipped in vscode, so this is
|
||||
// how we load it
|
||||
import type * as keytarType from 'keytar';
|
||||
import * as vscode from 'vscode';
|
||||
import Logger from './logger';
|
||||
import * as nls from 'vscode-nls';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
function getKeytar(): Keytar | undefined {
|
||||
try {
|
||||
return require('keytar');
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export type Keytar = {
|
||||
getPassword: typeof keytarType['getPassword'];
|
||||
setPassword: typeof keytarType['setPassword'];
|
||||
deletePassword: typeof keytarType['deletePassword'];
|
||||
};
|
||||
|
||||
const SERVICE_ID = `github.auth`;
|
||||
|
||||
export class Keychain {
|
||||
async setToken(token: string): Promise<void> {
|
||||
try {
|
||||
return await vscode.authentication.setPassword(SERVICE_ID, token);
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
Logger.error(`Setting token failed: ${e}`);
|
||||
const troubleshooting = localize('troubleshooting', "Troubleshooting Guide");
|
||||
const result = await vscode.window.showErrorMessage(localize('keychainWriteError', "Writing login information to the keychain failed with error '{0}'.", e.message), troubleshooting);
|
||||
if (result === troubleshooting) {
|
||||
vscode.env.openExternal(vscode.Uri.parse('https://code.visualstudio.com/docs/editor/settings-sync#_troubleshooting-keychain-issues'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getToken(): Promise<string | null | undefined> {
|
||||
try {
|
||||
return await vscode.authentication.getPassword(SERVICE_ID);
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
Logger.error(`Getting token failed: ${e}`);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteToken(): Promise<void> {
|
||||
try {
|
||||
return await vscode.authentication.deletePassword(SERVICE_ID);
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
Logger.error(`Deleting token failed: ${e}`);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
async tryMigrate(): Promise<string | null | undefined> {
|
||||
try {
|
||||
const keytar = getKeytar();
|
||||
if (!keytar) {
|
||||
throw new Error('keytar unavailable');
|
||||
}
|
||||
|
||||
const oldValue = await keytar.getPassword(`${vscode.env.uriScheme}-github.login`, 'account');
|
||||
if (oldValue) {
|
||||
await this.setToken(oldValue);
|
||||
await keytar.deletePassword(`${vscode.env.uriScheme}-github.login`, 'account');
|
||||
}
|
||||
|
||||
return oldValue;
|
||||
} catch (_) {
|
||||
// Ignore
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const keychain = new Keychain();
|
@ -0,0 +1,55 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
type LogLevel = 'Trace' | 'Info' | 'Error';
|
||||
|
||||
class Log {
|
||||
private output: vscode.OutputChannel;
|
||||
|
||||
constructor() {
|
||||
this.output = vscode.window.createOutputChannel('GitHub Authentication');
|
||||
}
|
||||
|
||||
private data2String(data: any): string {
|
||||
if (data instanceof Error) {
|
||||
return data.stack || data.message;
|
||||
}
|
||||
if (data.success === false && data.message) {
|
||||
return data.message;
|
||||
}
|
||||
return data.toString();
|
||||
}
|
||||
|
||||
public info(message: string, data?: any): void {
|
||||
this.logLevel('Info', message, data);
|
||||
}
|
||||
|
||||
public error(message: string, data?: any): void {
|
||||
this.logLevel('Error', message, data);
|
||||
}
|
||||
|
||||
public logLevel(level: LogLevel, message: string, data?: any): void {
|
||||
this.output.appendLine(`[${level} - ${this.now()}] ${message}`);
|
||||
if (data) {
|
||||
this.output.appendLine(this.data2String(data));
|
||||
}
|
||||
}
|
||||
|
||||
private now(): string {
|
||||
const now = new Date();
|
||||
return padLeft(now.getUTCHours() + '', 2, '0')
|
||||
+ ':' + padLeft(now.getMinutes() + '', 2, '0')
|
||||
+ ':' + padLeft(now.getUTCSeconds() + '', 2, '0') + '.' + now.getMilliseconds();
|
||||
}
|
||||
}
|
||||
|
||||
function padLeft(s: string, n: number, pad = ' ') {
|
||||
return pad.repeat(Math.max(0, n - s.length)) + s;
|
||||
}
|
||||
|
||||
const Logger = new Log();
|
||||
export default Logger;
|
@ -0,0 +1,73 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, Disposable } from 'vscode';
|
||||
|
||||
export function filterEvent<T>(event: Event<T>, filter: (e: T) => boolean): Event<T> {
|
||||
return (listener, thisArgs = null, disposables?) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables);
|
||||
}
|
||||
|
||||
export function onceEvent<T>(event: Event<T>): Event<T> {
|
||||
return (listener, thisArgs = null, disposables?) => {
|
||||
const result = event(e => {
|
||||
result.dispose();
|
||||
return listener.call(thisArgs, e);
|
||||
}, null, disposables);
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export interface PromiseAdapter<T, U> {
|
||||
(
|
||||
value: T,
|
||||
resolve:
|
||||
(value: U | PromiseLike<U>) => void,
|
||||
reject:
|
||||
(reason: any) => void
|
||||
): any;
|
||||
}
|
||||
|
||||
const passthrough = (value: any, resolve: (value?: any) => void) => resolve(value);
|
||||
|
||||
/**
|
||||
* Return a promise that resolves with the next emitted event, or with some future
|
||||
* event as decided by an adapter.
|
||||
*
|
||||
* If specified, the adapter is a function that will be called with
|
||||
* `(event, resolve, reject)`. It will be called once per event until it resolves or
|
||||
* rejects.
|
||||
*
|
||||
* The default adapter is the passthrough function `(value, resolve) => resolve(value)`.
|
||||
*
|
||||
* @param event the event
|
||||
* @param adapter controls resolution of the returned promise
|
||||
* @returns a promise that resolves or rejects as specified by the adapter
|
||||
*/
|
||||
export async function promiseFromEvent<T, U>(
|
||||
event: Event<T>,
|
||||
adapter: PromiseAdapter<T, U> = passthrough): Promise<U> {
|
||||
let subscription: Disposable;
|
||||
return new Promise<U>((resolve, reject) =>
|
||||
subscription = event((value: T) => {
|
||||
try {
|
||||
Promise.resolve(adapter(value, resolve, reject))
|
||||
.catch(reject);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
).then(
|
||||
(result: U) => {
|
||||
subscription.dispose();
|
||||
return result;
|
||||
},
|
||||
error => {
|
||||
subscription.dispose();
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
}
|
79
lib/vscode/extensions/github-authentication/src/extension.ts
Normal file
79
lib/vscode/extensions/github-authentication/src/extension.ts
Normal file
@ -0,0 +1,79 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { GitHubAuthenticationProvider, onDidChangeSessions } from './github';
|
||||
import { uriHandler } from './githubServer';
|
||||
import Logger from './common/logger';
|
||||
import TelemetryReporter from 'vscode-extension-telemetry';
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
const { name, version, aiKey } = require('../package.json') as { name: string, version: string, aiKey: string };
|
||||
const telemetryReporter = new TelemetryReporter(name, version, aiKey);
|
||||
|
||||
context.subscriptions.push(vscode.window.registerUriHandler(uriHandler));
|
||||
const loginService = new GitHubAuthenticationProvider();
|
||||
|
||||
await loginService.initialize(context);
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('github.provide-token', () => {
|
||||
return loginService.manuallyProvideToken();
|
||||
}));
|
||||
|
||||
context.subscriptions.push(vscode.authentication.registerAuthenticationProvider({
|
||||
id: 'github',
|
||||
label: 'GitHub',
|
||||
supportsMultipleAccounts: false,
|
||||
onDidChangeSessions: onDidChangeSessions.event,
|
||||
getSessions: () => Promise.resolve(loginService.sessions),
|
||||
login: async (scopeList: string[]) => {
|
||||
try {
|
||||
/* __GDPR__
|
||||
"login" : { }
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('login');
|
||||
|
||||
const session = await loginService.login(scopeList.sort().join(' '));
|
||||
Logger.info('Login success!');
|
||||
onDidChangeSessions.fire({ added: [session.id], removed: [], changed: [] });
|
||||
return session;
|
||||
} catch (e) {
|
||||
/* __GDPR__
|
||||
"loginFailed" : { }
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('loginFailed');
|
||||
|
||||
vscode.window.showErrorMessage(`Sign in failed: ${e}`);
|
||||
Logger.error(e);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
logout: async (id: string) => {
|
||||
try {
|
||||
/* __GDPR__
|
||||
"logout" : { }
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('logout');
|
||||
|
||||
await loginService.logout(id);
|
||||
onDidChangeSessions.fire({ added: [], removed: [id], changed: [] });
|
||||
} catch (e) {
|
||||
/* __GDPR__
|
||||
"logoutFailed" : { }
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('logoutFailed');
|
||||
|
||||
vscode.window.showErrorMessage(`Sign out failed: ${e}`);
|
||||
Logger.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// this method is called when your extension is deactivated
|
||||
export function deactivate() { }
|
190
lib/vscode/extensions/github-authentication/src/github.ts
Normal file
190
lib/vscode/extensions/github-authentication/src/github.ts
Normal file
@ -0,0 +1,190 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { keychain } from './common/keychain';
|
||||
import { GitHubServer, NETWORK_ERROR } from './githubServer';
|
||||
import Logger from './common/logger';
|
||||
|
||||
export const onDidChangeSessions = new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
|
||||
|
||||
interface SessionData {
|
||||
id: string;
|
||||
account?: {
|
||||
label?: string;
|
||||
displayName?: string;
|
||||
id: string;
|
||||
}
|
||||
scopes: string[];
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
export class GitHubAuthenticationProvider {
|
||||
private _sessions: vscode.AuthenticationSession[] = [];
|
||||
private _githubServer = new GitHubServer();
|
||||
|
||||
public async initialize(context: vscode.ExtensionContext): Promise<void> {
|
||||
try {
|
||||
this._sessions = await this.readSessions();
|
||||
await this.verifySessions();
|
||||
} catch (e) {
|
||||
// Ignore, network request failed
|
||||
}
|
||||
|
||||
context.subscriptions.push(vscode.authentication.onDidChangePassword(() => this.checkForUpdates()));
|
||||
}
|
||||
|
||||
private async verifySessions(): Promise<void> {
|
||||
const verifiedSessions: vscode.AuthenticationSession[] = [];
|
||||
const verificationPromises = this._sessions.map(async session => {
|
||||
try {
|
||||
await this._githubServer.getUserInfo(session.accessToken);
|
||||
verifiedSessions.push(session);
|
||||
} catch (e) {
|
||||
// Remove sessions that return unauthorized response
|
||||
if (e.message !== 'Unauthorized') {
|
||||
verifiedSessions.push(session);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Promise.all(verificationPromises).then(_ => {
|
||||
if (this._sessions.length !== verifiedSessions.length) {
|
||||
this._sessions = verifiedSessions;
|
||||
this.storeSessions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async checkForUpdates() {
|
||||
let storedSessions: vscode.AuthenticationSession[];
|
||||
try {
|
||||
storedSessions = await this.readSessions();
|
||||
} catch (e) {
|
||||
// Ignore, network request failed
|
||||
return;
|
||||
}
|
||||
|
||||
const added: string[] = [];
|
||||
const removed: string[] = [];
|
||||
|
||||
storedSessions.forEach(session => {
|
||||
const matchesExisting = this._sessions.some(s => s.id === session.id);
|
||||
// Another window added a session to the keychain, add it to our state as well
|
||||
if (!matchesExisting) {
|
||||
Logger.info('Adding session found in keychain');
|
||||
this._sessions.push(session);
|
||||
added.push(session.id);
|
||||
}
|
||||
});
|
||||
|
||||
this._sessions.map(session => {
|
||||
const matchesExisting = storedSessions.some(s => s.id === session.id);
|
||||
// Another window has logged out, remove from our state
|
||||
if (!matchesExisting) {
|
||||
Logger.info('Removing session no longer found in keychain');
|
||||
const sessionIndex = this._sessions.findIndex(s => s.id === session.id);
|
||||
if (sessionIndex > -1) {
|
||||
this._sessions.splice(sessionIndex, 1);
|
||||
}
|
||||
|
||||
removed.push(session.id);
|
||||
}
|
||||
});
|
||||
|
||||
if (added.length || removed.length) {
|
||||
onDidChangeSessions.fire({ added, removed, changed: [] });
|
||||
}
|
||||
}
|
||||
|
||||
private async readSessions(): Promise<vscode.AuthenticationSession[]> {
|
||||
const storedSessions = await keychain.getToken() || await keychain.tryMigrate();
|
||||
if (storedSessions) {
|
||||
try {
|
||||
const sessionData: SessionData[] = JSON.parse(storedSessions);
|
||||
const sessionPromises = sessionData.map(async (session: SessionData): Promise<vscode.AuthenticationSession> => {
|
||||
const needsUserInfo = !session.account;
|
||||
let userInfo: { id: string, accountName: string };
|
||||
if (needsUserInfo) {
|
||||
userInfo = await this._githubServer.getUserInfo(session.accessToken);
|
||||
}
|
||||
|
||||
return {
|
||||
id: session.id,
|
||||
account: {
|
||||
label: session.account
|
||||
? session.account.label || session.account.displayName!
|
||||
: userInfo!.accountName,
|
||||
id: session.account?.id ?? userInfo!.id
|
||||
},
|
||||
scopes: session.scopes,
|
||||
accessToken: session.accessToken
|
||||
};
|
||||
});
|
||||
|
||||
return Promise.all(sessionPromises);
|
||||
} catch (e) {
|
||||
if (e === NETWORK_ERROR) {
|
||||
return [];
|
||||
}
|
||||
|
||||
Logger.error(`Error reading sessions: ${e}`);
|
||||
await keychain.deleteToken();
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private async storeSessions(): Promise<void> {
|
||||
await keychain.setToken(JSON.stringify(this._sessions));
|
||||
}
|
||||
|
||||
get sessions(): vscode.AuthenticationSession[] {
|
||||
return this._sessions;
|
||||
}
|
||||
|
||||
public async login(scopes: string): Promise<vscode.AuthenticationSession> {
|
||||
const token = await this._githubServer.login(scopes);
|
||||
const session = await this.tokenToSession(token, scopes.split(' '));
|
||||
await this.setToken(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
public async manuallyProvideToken(): Promise<void> {
|
||||
this._githubServer.manuallyProvideToken();
|
||||
}
|
||||
|
||||
private async tokenToSession(token: string, scopes: string[]): Promise<vscode.AuthenticationSession> {
|
||||
const userInfo = await this._githubServer.getUserInfo(token);
|
||||
return {
|
||||
id: uuid(),
|
||||
accessToken: token,
|
||||
account: { label: userInfo.accountName, id: userInfo.id },
|
||||
scopes
|
||||
};
|
||||
}
|
||||
|
||||
private async setToken(session: vscode.AuthenticationSession): Promise<void> {
|
||||
const sessionIndex = this._sessions.findIndex(s => s.id === session.id);
|
||||
if (sessionIndex > -1) {
|
||||
this._sessions.splice(sessionIndex, 1, session);
|
||||
} else {
|
||||
this._sessions.push(session);
|
||||
}
|
||||
|
||||
await this.storeSessions();
|
||||
}
|
||||
|
||||
public async logout(id: string) {
|
||||
const sessionIndex = this._sessions.findIndex(session => session.id === id);
|
||||
if (sessionIndex > -1) {
|
||||
this._sessions.splice(sessionIndex, 1);
|
||||
}
|
||||
|
||||
await this.storeSessions();
|
||||
}
|
||||
}
|
209
lib/vscode/extensions/github-authentication/src/githubServer.ts
Normal file
209
lib/vscode/extensions/github-authentication/src/githubServer.ts
Normal file
@ -0,0 +1,209 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as vscode from 'vscode';
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { PromiseAdapter, promiseFromEvent } from './common/utils';
|
||||
import Logger from './common/logger';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export const NETWORK_ERROR = 'network error';
|
||||
const AUTH_RELAY_SERVER = 'vscode-auth.github.com';
|
||||
|
||||
class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
|
||||
public handleUri(uri: vscode.Uri) {
|
||||
this.fire(uri);
|
||||
}
|
||||
}
|
||||
|
||||
export const uriHandler = new UriEventHandler;
|
||||
|
||||
const onDidManuallyProvideToken = new vscode.EventEmitter<string>();
|
||||
|
||||
|
||||
|
||||
function parseQuery(uri: vscode.Uri) {
|
||||
return uri.query.split('&').reduce((prev: any, current) => {
|
||||
const queryString = current.split('=');
|
||||
prev[queryString[0]] = queryString[1];
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export class GitHubServer {
|
||||
private _statusBarItem: vscode.StatusBarItem | undefined;
|
||||
|
||||
private _pendingStates = new Map<string, string[]>();
|
||||
private _codeExchangePromises = new Map<string, Promise<string>>();
|
||||
|
||||
private isTestEnvironment(url: vscode.Uri): boolean {
|
||||
return url.authority === 'vscode-web-test-playground.azurewebsites.net' || url.authority.startsWith('localhost:');
|
||||
}
|
||||
|
||||
public async login(scopes: string): Promise<string> {
|
||||
Logger.info('Logging in...');
|
||||
this.updateStatusBarItem(true);
|
||||
|
||||
const state = uuid();
|
||||
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'); }
|
||||
|
||||
const tokenScopes = await this.getScopes(token); // Example: ['repo', 'user']
|
||||
const scopesList = scopes.split(' '); // Example: 'read:user repo user:email'
|
||||
if (!scopesList.every(scope => {
|
||||
const included = tokenScopes.includes(scope);
|
||||
if (included || !scope.includes(':')) {
|
||||
return included;
|
||||
}
|
||||
|
||||
return scope.split(':').some(splitScopes => {
|
||||
return tokenScopes.includes(splitScopes);
|
||||
});
|
||||
})) {
|
||||
throw new Error(`The provided token is does not match the requested scopes: ${scopes}`);
|
||||
}
|
||||
|
||||
this.updateStatusBarItem(false);
|
||||
return token;
|
||||
} else {
|
||||
const existingStates = this._pendingStates.get(scopes) || [];
|
||||
this._pendingStates.set(scopes, [...existingStates, state]);
|
||||
|
||||
const uri = vscode.Uri.parse(`https://${AUTH_RELAY_SERVER}/authorize/?callbackUri=${encodeURIComponent(callbackUri.toString())}&scope=${scopes}&state=${state}&responseType=code&authServer=https://github.com`);
|
||||
await vscode.env.openExternal(uri);
|
||||
}
|
||||
|
||||
// Register a single listener for the URI callback, in case the user starts the login process multiple times
|
||||
// before completing it.
|
||||
let existingPromise = this._codeExchangePromises.get(scopes);
|
||||
if (!existingPromise) {
|
||||
existingPromise = promiseFromEvent(uriHandler.event, this.exchangeCodeForToken(scopes));
|
||||
this._codeExchangePromises.set(scopes, existingPromise);
|
||||
}
|
||||
|
||||
return Promise.race([
|
||||
existingPromise,
|
||||
promiseFromEvent<string, string>(onDidManuallyProvideToken.event)
|
||||
]).finally(() => {
|
||||
this._pendingStates.delete(scopes);
|
||||
this._codeExchangePromises.delete(scopes);
|
||||
this.updateStatusBarItem(false);
|
||||
});
|
||||
}
|
||||
|
||||
private exchangeCodeForToken: (scopes: string) => PromiseAdapter<vscode.Uri, string> =
|
||||
(scopes) => async (uri, resolve, reject) => {
|
||||
Logger.info('Exchanging code for token...');
|
||||
const query = parseQuery(uri);
|
||||
const code = query.code;
|
||||
|
||||
const acceptedStates = this._pendingStates.get(scopes) || [];
|
||||
if (!acceptedStates.includes(query.state)) {
|
||||
reject('Received mismatched state');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await fetch(`https://${AUTH_RELAY_SERVER}/token?code=${code}&state=${query.state}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
const json = await result.json();
|
||||
Logger.info('Token exchange success!');
|
||||
resolve(json.access_token);
|
||||
} else {
|
||||
reject(result.statusText);
|
||||
}
|
||||
} catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
};
|
||||
|
||||
private updateStatusBarItem(isStart?: boolean) {
|
||||
if (isStart && !this._statusBarItem) {
|
||||
this._statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
|
||||
this._statusBarItem.text = localize('signingIn', "$(mark-github) Signing in to github.com...");
|
||||
this._statusBarItem.command = 'github.provide-token';
|
||||
this._statusBarItem.show();
|
||||
}
|
||||
|
||||
if (!isStart && this._statusBarItem) {
|
||||
this._statusBarItem.dispose();
|
||||
this._statusBarItem = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public async manuallyProvideToken() {
|
||||
const uriOrToken = await vscode.window.showInputBox({ prompt: 'Token', ignoreFocusOut: true });
|
||||
if (!uriOrToken) { return; }
|
||||
try {
|
||||
const uri = vscode.Uri.parse(uriOrToken);
|
||||
if (!uri.scheme || uri.scheme === 'file') { throw new Error; }
|
||||
uriHandler.handleUri(uri);
|
||||
} catch (e) {
|
||||
// If it doesn't look like a URI, treat it as a token.
|
||||
Logger.info('Treating input as token');
|
||||
onDidManuallyProvideToken.fire(uriOrToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async getScopes(token: string): Promise<string[]> {
|
||||
try {
|
||||
Logger.info('Getting token scopes...');
|
||||
const result = await fetch('https://api.github.com', {
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
'User-Agent': 'Visual-Studio-Code'
|
||||
}
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
const scopes = result.headers.get('X-OAuth-Scopes');
|
||||
return scopes ? scopes.split(',').map(scope => scope.trim()) : [];
|
||||
} else {
|
||||
Logger.error(`Getting scopes failed: ${result.statusText}`);
|
||||
throw new Error(result.statusText);
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger.error(ex.message);
|
||||
throw new Error(NETWORK_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
public async getUserInfo(token: string): Promise<{ id: string, accountName: string }> {
|
||||
let result: Response;
|
||||
try {
|
||||
Logger.info('Getting user info...');
|
||||
result = await fetch('https://api.github.com/user', {
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
'User-Agent': 'Visual-Studio-Code'
|
||||
}
|
||||
});
|
||||
} catch (ex) {
|
||||
Logger.error(ex.message);
|
||||
throw new Error(NETWORK_ERROR);
|
||||
}
|
||||
|
||||
if (result.ok) {
|
||||
const json = await result.json();
|
||||
Logger.info('Got account info!');
|
||||
return { id: json.id, accountName: json.login };
|
||||
} else {
|
||||
Logger.error(`Getting account info failed: ${result.statusText}`);
|
||||
throw new Error(result.statusText);
|
||||
}
|
||||
}
|
||||
}
|
7
lib/vscode/extensions/github-authentication/src/typings/ref.d.ts
vendored
Normal file
7
lib/vscode/extensions/github-authentication/src/typings/ref.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
|
Reference in New Issue
Block a user