diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index 54d30a027f2..6dab0891278 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -14,6 +14,22 @@ "activationEvents": [ "*" ], + "contributes": { + "commands": [ + { + "command": "github.provide-token", + "title": "Manually Provide Token" + } + ], + "menus": { + "commandPalette": [ + { + "command": "github.provide-token", + "when": "false" + } + ] + } + }, "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", "main": "./out/extension.js", "scripts": { diff --git a/extensions/github-authentication/src/extension.ts b/extensions/github-authentication/src/extension.ts index aa2ecb54209..159f6c72234 100644 --- a/extensions/github-authentication/src/extension.ts +++ b/extensions/github-authentication/src/extension.ts @@ -18,6 +18,10 @@ export async function activate(context: vscode.ExtensionContext) { await loginService.initialize(); + context.subscriptions.push(vscode.commands.registerCommand('github.provide-token', () => { + return loginService.manuallyProvideToken(); + })); + vscode.authentication.registerAuthenticationProvider({ id: 'github', displayName: 'GitHub', diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index 1e9c726ed4c..c583b691f6b 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -44,7 +44,8 @@ export class GitHubAuthenticationProvider { // Ignore, network request failed } - await this.validateSessions(); + // TODO revert Cannot validate tokens from auth server, no available clientId + // await this.validateSessions(); this.pollForChange(); } @@ -93,27 +94,6 @@ export class GitHubAuthenticationProvider { }, 1000 * 30); } - private async validateSessions(): Promise { - const validationPromises = this._sessions.map(async session => { - try { - await this._githubServer.validateToken(await session.getAccessToken()); - return session; - } catch (e) { - if (e === NETWORK_ERROR) { - return session; - } - - return undefined; - } - }); - - const validSessions = (await Promise.all(validationPromises)).filter((x: vscode.AuthenticationSession | undefined): x is vscode.AuthenticationSession => !!x); - if (validSessions.length !== this._sessions.length) { - this._sessions = validSessions; - this.storeSessions(); - } - } - private async readSessions(): Promise { const storedSessions = await keychain.getToken(); if (storedSessions) { @@ -190,6 +170,10 @@ export class GitHubAuthenticationProvider { } } + public async manuallyProvideToken(): Promise { + this._githubServer.manuallyProvideToken(); + } + private async tokenToSession(token: string, scopes: string[]): Promise { const userInfo = await this._githubServer.getUserInfo(token); return { @@ -216,13 +200,15 @@ export class GitHubAuthenticationProvider { public async logout(id: string) { const sessionIndex = this._sessions.findIndex(session => session.id === id); if (sessionIndex > -1) { - const session = this._sessions.splice(sessionIndex, 1)[0]; - const token = await session.getAccessToken(); - try { - await this._githubServer.revokeToken(token); - } catch (_) { - // ignore, should still remove from keychain - } + this._sessions.splice(sessionIndex, 1); + // TODO revert + // Cannot revoke tokens from auth server, no clientId available + // const token = await session.getAccessToken(); + // try { + // await this._githubServer.revokeToken(token); + // } catch (_) { + // // ignore, should still remove from keychain + // } } await this.storeSessions(); diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index 082683ee107..79951e2e9a4 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -4,13 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import * as https from 'https'; +import * as nls from 'vscode-nls'; import * as vscode from 'vscode'; import * as uuid from 'uuid'; import { PromiseAdapter, promiseFromEvent } from './common/utils'; import Logger from './common/logger'; -import ClientRegistrar, { ClientDetails } from './common/clientRegistrar'; +import ClientRegistrar from './common/clientRegistrar'; + +const localize = nls.loadMessageBundle(); export const NETWORK_ERROR = 'network error'; +const AUTH_RELAY_SERVER = 'vscode-auth.github.com'; class UriEventHandler extends vscode.EventEmitter implements vscode.UriHandler { public handleUri(uri: vscode.Uri) { @@ -20,8 +24,8 @@ class UriEventHandler extends vscode.EventEmitter implements vscode. export const uriHandler = new UriEventHandler; -const exchangeCodeForToken: (state: string, clientDetails: ClientDetails) => PromiseAdapter = - (state, clientDetails) => async (uri, resolve, reject) => { +const exchangeCodeForToken: (state: string, host: string, getPath: (code: string) => string) => PromiseAdapter = + (state, host, getPath) => async (uri, resolve, reject) => { Logger.info('Exchanging code for token...'); const query = parseQuery(uri); const code = query.code; @@ -32,8 +36,8 @@ const exchangeCodeForToken: (state: string, clientDetails: ClientDetails) => Pro } const post = https.request({ - host: 'github.com', - path: `/login/oauth/access_token?client_id=${clientDetails.id}&client_secret=${clientDetails.secret}&state=${query.state}&code=${code}`, + host: host, + path: getPath(code), method: 'POST', headers: { Accept: 'application/json' @@ -69,15 +73,55 @@ function parseQuery(uri: vscode.Uri) { } export class GitHubServer { + private _statusBarItem: vscode.StatusBarItem | undefined; + public async login(scopes: string): Promise { 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`)); const clientDetails = scopes === 'vso' ? ClientRegistrar.getGitHubAppDetails() : ClientRegistrar.getClientDetails(callbackUri); - const uri = vscode.Uri.parse(`https://github.com/login/oauth/authorize?redirect_uri=${encodeURIComponent(callbackUri.toString())}&scope=${scopes}&state=${state}&client_id=${clientDetails.id}`); + const uri = scopes !== 'vso' + ? vscode.Uri.parse(`https://${AUTH_RELAY_SERVER}/authorize/?callbackUri=${encodeURIComponent(callbackUri.toString())}&scope=${scopes}&state=${state}&responseType=code`) + : vscode.Uri.parse(`https://github.com/login/oauth/authorize?redirect_uri=${encodeURIComponent(callbackUri.toString())}&scope=${scopes}&state=${state}&client_id=${clientDetails.id}`); vscode.env.openExternal(uri); - return promiseFromEvent(uriHandler.event, exchangeCodeForToken(state, clientDetails)); + + return promiseFromEvent(uriHandler.event, exchangeCodeForToken(state, AUTH_RELAY_SERVER, (code) => { + return scopes !== 'vso' + ? `/token?code=${code}&state=${state}` + : `/login/oauth/access_token?client_id=${clientDetails.id}&client_secret=${clientDetails.secret}&state=${state}&code=${code}&authServer=github.com`; + })).finally(() => { + this.updateStatusBarItem(false); + }); + } + + 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) { + Logger.error(e); + vscode.window.showErrorMessage(localize('unexpectedInput', "The input did not matched the expected format")); + } } public async hasUserInstallation(token: string): Promise { @@ -122,7 +166,7 @@ export class GitHubServer { const uri = vscode.Uri.parse(`https://github.com/apps/microsoft-visual-studio-code/installations/new?state=${state}`); vscode.env.openExternal(uri); - return promiseFromEvent(uriHandler.event, exchangeCodeForToken(state, clientDetails)); + return promiseFromEvent(uriHandler.event, exchangeCodeForToken(state, 'github.com', (code) => `/login/oauth/access_token?client_id=${clientDetails.id}&client_secret=${clientDetails.secret}&state=${state}&code=${code}`)); } public async getUserInfo(token: string): Promise<{ id: string, accountName: string }> {