diff --git a/extensions/vscode-account/.vscodeignore b/extensions/vscode-account/.vscodeignore new file mode 100644 index 00000000000..ed3f9d37c1f --- /dev/null +++ b/extensions/vscode-account/.vscodeignore @@ -0,0 +1,10 @@ +.vscode/** +.vscode-test/** +out/test/** +src/** +.gitignore +vsc-extension-quickstart.md +**/tsconfig.json +**/tslint.json +**/*.map +**/*.ts \ No newline at end of file diff --git a/src/vs/platform/auth/common/auth.css b/extensions/vscode-account/media/auth.css similarity index 100% rename from src/vs/platform/auth/common/auth.css rename to extensions/vscode-account/media/auth.css diff --git a/src/vs/platform/auth/common/auth.html b/extensions/vscode-account/media/auth.html similarity index 99% rename from src/vs/platform/auth/common/auth.html rename to extensions/vscode-account/media/auth.html index 8fe3e50e7b7..0fcba4e3c62 100644 --- a/src/vs/platform/auth/common/auth.html +++ b/extensions/vscode-account/media/auth.html @@ -1,6 +1,7 @@ + @@ -8,6 +9,7 @@ + Visual Studio Code @@ -32,4 +34,5 @@ } + diff --git a/extensions/vscode-account/package.json b/extensions/vscode-account/package.json new file mode 100644 index 00000000000..46b6c92ed36 --- /dev/null +++ b/extensions/vscode-account/package.json @@ -0,0 +1,30 @@ +{ + "name": "login", + "publisher": "vscode", + "displayName": "Account", + "description": "", + "version": "0.0.1", + "engines": { + "vscode": "^1.42.0" + }, + "categories": [ + "Other" + ], + "enableProposedApi": true, + "activationEvents": [ + "*" + ], + "main": "./out/extension.js", + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./" + }, + "devDependencies": { + "typescript": "^3.7.4", + "tslint": "^5.12.1", + "@types/node": "^10.12.21", + "@types/keytar": "^4.0.1", + "@types/vscode": "^1.41.0" + } +} diff --git a/extensions/vscode-account/src/AADHelper.ts b/extensions/vscode-account/src/AADHelper.ts new file mode 100644 index 00000000000..b15aec32697 --- /dev/null +++ b/extensions/vscode-account/src/AADHelper.ts @@ -0,0 +1,245 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as crypto from 'crypto'; +import * as vscode from 'vscode'; +import * as https from 'https'; +import * as querystring from 'querystring'; +import { keychain } from './keychain'; +import { toBase64UrlEncoding } from './utils'; +import { createServer, startServer } from './authServer'; + +const redirectUrl = 'https://vscode-redirect.azurewebsites.net/'; +const loginEndpointUrl = 'https://login.microsoftonline.com/'; +const clientId = 'aebc6443-996d-45c2-90f0-388ff96faa56'; +const scope = 'https://management.core.windows.net/.default offline_access'; +const tenant = 'common'; + +interface IToken { + expiresIn: string; // How long access token is valid, in seconds + accessToken: string; + refreshToken: string; +} + +export const onDidChangeAccounts = new vscode.EventEmitter(); + +export class AzureActiveDirectoryService { + private _token: IToken | undefined; + private _refreshTimeout: NodeJS.Timeout | undefined; + + public async initialize(): Promise { + const existingRefreshToken = await keychain.getToken(); + if (existingRefreshToken) { + await this.refreshToken(existingRefreshToken); + } + } + + private tokenToAccount(token: IToken): vscode.Account { + return { + id: '', + accessToken: token.accessToken, + displayName: this.getDisplayNameFromToken(token.accessToken) + }; + } + + private getDisplayNameFromToken(accessToken: string): string { + let displayName = 'user@example.com'; + try { + // TODO fixme + displayName = JSON.parse(atob(accessToken.split('.')[1])); + } catch (e) { + // Fall back to example display name + } + + return displayName; + } + + get accounts(): vscode.Account[] { + return this._token ? [this.tokenToAccount(this._token)] : []; + } + + public async login(): Promise { + const nonce = crypto.randomBytes(16).toString('base64'); + const { server, redirectPromise, codePromise } = createServer(nonce); + + let token: IToken | undefined; + try { + const port = await startServer(server); + vscode.env.openExternal(vscode.Uri.parse(`http://localhost:${port}/signin?nonce=${encodeURIComponent(nonce)}`)); + + const redirectReq = await redirectPromise; + if ('err' in redirectReq) { + const { err, res } = redirectReq; + res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` }); + res.end(); + throw err; + } + + const host = redirectReq.req.headers.host || ''; + const updatedPortStr = (/^[^:]+:(\d+)$/.exec(Array.isArray(host) ? host[0] : host) || [])[1]; + const updatedPort = updatedPortStr ? parseInt(updatedPortStr, 10) : port; + + const state = `${updatedPort},${encodeURIComponent(nonce)}`; + + const codeVerifier = toBase64UrlEncoding(crypto.randomBytes(32).toString('base64')); + const codeChallenge = toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64')); + const loginUrl = `${loginEndpointUrl}${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${state}&scope=${encodeURIComponent(scope)}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}`; + + await redirectReq.res.writeHead(302, { Location: loginUrl }); + redirectReq.res.end(); + + const codeRes = await codePromise; + const res = codeRes.res; + + try { + if ('err' in codeRes) { + throw codeRes.err; + } + token = await this.exchangeCodeForToken(codeRes.code, codeVerifier); + this.setToken(token); + res.writeHead(302, { Location: '/' }); + res.end(); + } catch (err) { + res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` }); + res.end(); + } + } finally { + setTimeout(() => { + server.close(); + }, 5000); + } + } + + private async setToken(token: IToken): Promise { + this._token = token; + + if (this._refreshTimeout) { + clearTimeout(this._refreshTimeout); + } + + this._refreshTimeout = setTimeout(async () => { + try { + await this.refreshToken(token.refreshToken); + } catch (e) { + vscode.window.showErrorMessage(`You have been signed out.`); + this._token = undefined; + } finally { + onDidChangeAccounts.fire(this.accounts); + } + }, 1000 * (parseInt(token.expiresIn) - 10)); + + await keychain.setToken(token.refreshToken); + } + + private async exchangeCodeForToken(code: string, codeVerifier: string): Promise { + return new Promise((resolve: (value: IToken) => void, reject) => { + try { + const postData = querystring.stringify({ + grant_type: 'authorization_code', + code: code, + client_id: clientId, + scope: scope, + code_verifier: codeVerifier, + redirect_uri: redirectUrl + }); + + const tokenUrl = vscode.Uri.parse(`${loginEndpointUrl}${tenant}/oauth2/v2.0/token`); + + const post = https.request({ + host: tokenUrl.authority, + path: tokenUrl.path, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': postData.length + } + }, 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({ + expiresIn: json.expires_in, + accessToken: json.access_token, + refreshToken: json.refresh_token + }); + } else { + reject(new Error('Unable to login.')); + } + }); + }); + + post.write(postData); + + post.end(); + post.on('error', err => { + reject(err); + }); + + } catch (e) { + reject(e); + } + }); + } + + private async refreshToken(refreshToken: string): Promise { + return new Promise((resolve: (value: IToken) => void, reject) => { + const postData = querystring.stringify({ + refresh_token: refreshToken, + client_id: clientId, + grant_type: 'refresh_token', + scope: scope + }); + + const post = https.request({ + host: 'login.microsoftonline.com', + path: `/${tenant}/oauth2/v2.0/token`, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': postData.length + } + }, 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()); + const token = { + expiresIn: json.expires_in, + accessToken: json.access_token, + refreshToken: json.refresh_token + }; + this.setToken(token); + resolve(token); + } else { + vscode.window.showInformationMessage(`error`); + reject(new Error('Bad!')); + } + }); + }); + + post.write(postData); + + post.end(); + post.on('error', err => { + reject(err); + }); + }); + } + + public async logout() { + delete this._token; + await keychain.deleteToken(); + if (this._refreshTimeout) { + clearTimeout(this._refreshTimeout); + } + } +} diff --git a/src/vs/platform/auth/electron-browser/authServer.ts b/extensions/vscode-account/src/authServer.ts similarity index 81% rename from src/vs/platform/auth/electron-browser/authServer.ts rename to extensions/vscode-account/src/authServer.ts index 57bd4fdfb5f..b78803cec16 100644 --- a/src/vs/platform/auth/electron-browser/authServer.ts +++ b/extensions/vscode-account/src/authServer.ts @@ -7,14 +7,46 @@ import * as http from 'http'; import * as url from 'url'; import * as fs from 'fs'; import * as net from 'net'; -import { getPathFromAmdModule } from 'vs/base/common/amd'; -import { assertIsDefined } from 'vs/base/common/types'; +import * as path from 'path'; interface Deferred { resolve: (result: T | Promise) => void; reject: (reason: any) => void; } +const _typeof = { + number: 'number', + string: 'string', + undefined: 'undefined', + object: 'object', + function: 'function' +}; + +/** + * @returns whether the provided parameter is undefined. + */ +export function isUndefined(obj: any): obj is undefined { + return typeof (obj) === _typeof.undefined; +} + +/** + * @returns whether the provided parameter is undefined or null. + */ +export function isUndefinedOrNull(obj: any): obj is undefined | null { + return isUndefined(obj) || obj === null; +} + +/** + * Asserts that the argument passed in is neither undefined nor null. + */ +export function assertIsDefined(arg: T | null | undefined): T { + if (isUndefinedOrNull(arg)) { + throw new Error('Assertion Failed: argument is undefined or null'); + } + + return arg; +} + export function createTerminateServer(server: http.Server) { const sockets: Record = {}; let socketCount = 0; @@ -140,10 +172,10 @@ export function createServer(nonce: string) { } break; case '/': - sendFile(res, getPathFromAmdModule(require, '../common/auth.html'), 'text/html; charset=utf-8'); + sendFile(res, path.join(__dirname, '../media/auth.html'), 'text/html; charset=utf-8'); break; case '/auth.css': - sendFile(res, getPathFromAmdModule(require, '../common/auth.css'), 'text/css; charset=utf-8'); + sendFile(res, path.join(__dirname, '../media/auth.css'), 'text/css; charset=utf-8'); break; case '/callback': deferredCode.resolve(callback(nonce, reqUrl) diff --git a/extensions/vscode-account/src/extension.ts b/extensions/vscode-account/src/extension.ts new file mode 100644 index 00000000000..53793dc4c43 --- /dev/null +++ b/extensions/vscode-account/src/extension.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { AzureActiveDirectoryService, onDidChangeAccounts } from './AADHelper'; + +export async function activate(context: vscode.ExtensionContext) { + + const loginService = new AzureActiveDirectoryService(); + + await loginService.initialize(); + + vscode.authentication.registerAuthenticationProvider({ + id: 'MSA', + displayName: 'Microsoft Account', // TODO localize + onDidChangeAccounts: onDidChangeAccounts.event, + accounts: loginService.accounts, + login: async () => { + try { + await loginService.login(); + return loginService.accounts[0]!; + } catch (e) { + vscode.window.showErrorMessage(`Logging in failed: ${e}`); + throw e; + } + }, + logout: async (id: string) => { + return loginService.logout(); + } + }); + + return; +} + +// this method is called when your extension is deactivated +export function deactivate() { } diff --git a/extensions/vscode-account/src/keychain.ts b/extensions/vscode-account/src/keychain.ts new file mode 100644 index 00000000000..a53d848c76c --- /dev/null +++ b/extensions/vscode-account/src/keychain.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as keytarType from 'keytar'; + +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 = 'vscode.login'; +const ACCOUNT_ID = 'account'; + +export class Keychain { + private keytar: Keytar; + + constructor() { + const keytar = getKeytar(); + if (!keytar) { + throw new Error('System keychain unavailable'); + } + + this.keytar = keytar; + } + + async setToken(token: string): Promise { + try { + return await this.keytar.setPassword(SERVICE_ID, ACCOUNT_ID, token); + } catch (e) { + // Ignore + } + } + + async getToken() { + try { + return await this.keytar.getPassword(SERVICE_ID, ACCOUNT_ID); + } catch (e) { + // Ignore + } + } + + async deleteToken() { + try { + return await this.keytar.deletePassword(SERVICE_ID, ACCOUNT_ID); + } catch (e) { + // Ignore + } + } +} + +export const keychain = new Keychain(); diff --git a/extensions/vscode-account/src/utils.ts b/extensions/vscode-account/src/utils.ts new file mode 100644 index 00000000000..164f2236221 --- /dev/null +++ b/extensions/vscode-account/src/utils.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function toBase64UrlEncoding(base64string: string) { + return base64string.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); // Need to use base64url encoding +} diff --git a/extensions/vscode-account/src/vscode.proposed.d.ts b/extensions/vscode-account/src/vscode.proposed.d.ts new file mode 100644 index 00000000000..0f4a53fee1a --- /dev/null +++ b/extensions/vscode-account/src/vscode.proposed.d.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * This is the place for API experiments and proposals. + * These API are NOT stable and subject to change. They are only available in the Insiders + * distribution and CANNOT be used in published extensions. + * + * To test these API in local environment: + * - Use Insiders release of VS Code. + * - Add `"enableProposedApi": true` to your package.json. + * - Copy this file to your project. + */ + +declare module 'vscode' { + + export interface Account { + readonly id: string; + readonly accessToken: string; + readonly displayName: string; + } + + export interface AuthenticationProvider { + readonly id: string; + readonly displayName: string; + + readonly accounts: ReadonlyArray; + readonly onDidChangeAccounts: Event>; + + login(): Promise; + logout(accountId: string): Promise; + } + + export namespace authentication { + export function registerAuthenticationProvider(provider: AuthenticationProvider): Disposable; + } + + // #region Ben - extension auth flow (desktop+web) + + export namespace env { + + export function asExternalUri(target: Uri): Thenable + } +} diff --git a/extensions/vscode-account/tsconfig.json b/extensions/vscode-account/tsconfig.json new file mode 100644 index 00000000000..46be6dc9581 --- /dev/null +++ b/extensions/vscode-account/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "out", + "lib": [ + "es6", + "es2016", + "dom" + ], + "typeRoots": [ + "node_modules/@types", + "src/typings" + ], + "sourceMap": true, + "rootDir": "src", + "strict": true, + "noImplicitAny": true + }, + "exclude": [ + "node_modules", + ".vscode-test" + ] +} diff --git a/extensions/vscode-account/yarn.lock b/extensions/vscode-account/yarn.lock new file mode 100644 index 00000000000..4fc295de4b9 --- /dev/null +++ b/extensions/vscode-account/yarn.lock @@ -0,0 +1,658 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" + integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== + dependencies: + "@babel/highlight" "^7.8.3" + +"@babel/highlight@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.8.3.tgz#28f173d04223eaaa59bc1d439a3836e6d1265797" + integrity sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg== + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" + +"@types/keytar@^4.0.1": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@types/keytar/-/keytar-4.4.2.tgz#49ef917d6cbb4f19241c0ab50cd35097b5729b32" + integrity sha512-xtQcDj9ruGnMwvSu1E2BH4SFa5Dv2PvSPd0CKEBLN5hEj/v5YpXJY+B6hAfuKIbvEomD7vJTc/P1s1xPNh2kRw== + dependencies: + keytar "*" + +"@types/node@^10.12.21": + version "10.17.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c" + integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg== + +"@types/vscode@^1.41.0": + version "1.41.0" + resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.41.0.tgz#b0d75920220f84e07093285e59180c0f11d336cd" + integrity sha512-7SfeY5u9jgiELwxyLB3z7l6l/GbN9CqpCQGkcRlB7tKRFBxzbz2PoBfGrLxI1vRfUCIq5+hg5vtDHExwq5j3+A== + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +bl@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88" + integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A== + dependencies: + readable-stream "^3.0.1" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +builtin-modules@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= + +chalk@^2.0.0, chalk@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chownr@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142" + integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw== + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +commander@^2.12.1: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +decompress-response@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" + integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== + dependencies: + mimic-response "^2.0.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= + +glob@^7.1.1: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +keytar@*: + version "5.0.0" + resolved "https://registry.yarnpkg.com/keytar/-/keytar-5.0.0.tgz#c89b6b7a4608fd7af633d9f8474b1a7eb97cbe6f" + integrity sha512-a5UheK59YOlJf9i+2Osaj/kkH6mK0RCHVMtJ84u6ZfbfRIbOJ/H4b5VlOF/LgNHF6s78dRSBzZnvIuPiBKv6wg== + dependencies: + nan "2.14.0" + prebuild-install "5.3.3" + +mimic-response@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.0.0.tgz#996a51c60adf12cb8a87d7fb8ef24c2f3d5ebb46" + integrity sha512-8ilDoEapqA4uQ3TwS0jakGONKXVJqpy+RpM+3b7pLdOjghCrEiGp9SRkFbUHAmZW9vdnrENWHjaweIoTIJExSQ== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + +mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +nan@2.14.0: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + +napi-build-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.1.tgz#1381a0f92c39d66bf19852e7873432fc2123e508" + integrity sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA== + +node-abi@^2.7.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.13.0.tgz#e2f2ec444d0aca3ea1b3874b6de41d1665828f63" + integrity sha512-9HrZGFVTR5SOu3PZAnAY2hLO36aW1wmA+FDsVkr85BTST32TLCA1H/AEcatVRAsWLyXS3bqUDYCAjq5/QGuSTA== + dependencies: + semver "^5.4.1" + +noop-logger@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" + integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI= + +npmlog@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +prebuild-install@5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.3.tgz#ef4052baac60d465f5ba6bf003c9c1de79b9da8e" + integrity sha512-GV+nsUXuPW2p8Zy7SarF/2W/oiK8bFQgJcncoJ0d7kRpekEA0ftChjfEaF9/Y+QJEc/wFR7RAEa8lYByuUIe2g== + dependencies: + detect-libc "^1.0.3" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.0" + mkdirp "^0.5.1" + napi-build-utils "^1.0.1" + node-abi "^2.7.0" + noop-logger "^0.1.1" + npmlog "^4.0.1" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^3.0.3" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + which-pm-runs "^1.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@^2.0.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.1, readable-stream@^3.1.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" + integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +resolve@^1.3.2: + version "1.14.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.14.2.tgz#dbf31d0fa98b1f29aa5169783b9c290cb865fea2" + integrity sha512-EjlOBLBO1kxsUxsKjLt7TAECyKW6fOh1VRkykQkKGzcBbjjPIxBqGh0jf7GJ3k/f5mxMqW3htMD3WdTUVtW8HQ== + dependencies: + path-parse "^1.0.6" + +safe-buffer@^5.0.1, safe-buffer@~5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +semver@^5.3.0, semver@^5.4.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +simple-concat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6" + integrity sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY= + +simple-get@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" + integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== + dependencies: + decompress-response "^4.2.0" + once "^1.3.1" + simple-concat "^1.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +tar-fs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.0.0.tgz#677700fc0c8b337a78bee3623fdc235f21d7afad" + integrity sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA== + dependencies: + chownr "^1.1.1" + mkdirp "^0.5.1" + pump "^3.0.0" + tar-stream "^2.0.0" + +tar-stream@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.0.tgz#d1aaa3661f05b38b5acc9b7020efdca5179a2cc3" + integrity sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw== + dependencies: + bl "^3.0.0" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +tslib@^1.8.0, tslib@^1.8.1: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + +tslint@^5.12.1: + version "5.20.1" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.20.1.tgz#e401e8aeda0152bc44dd07e614034f3f80c67b7d" + integrity sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg== + dependencies: + "@babel/code-frame" "^7.0.0" + builtin-modules "^1.1.1" + chalk "^2.3.0" + commander "^2.12.1" + diff "^4.0.1" + glob "^7.1.1" + js-yaml "^3.13.1" + minimatch "^3.0.4" + mkdirp "^0.5.1" + resolve "^1.3.2" + semver "^5.3.0" + tslib "^1.8.0" + tsutils "^2.29.0" + +tsutils@^2.29.0: + version "2.29.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" + integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== + dependencies: + tslib "^1.8.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +typescript@^3.7.4: + version "3.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.4.tgz#1743a5ec5fef6a1fa9f3e4708e33c81c73876c19" + integrity sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw== + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +which-pm-runs@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" + integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= + +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index ee7fab8ba62..4db9464573e 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -50,20 +50,18 @@ import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/electron-browser/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, ISettingsSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, ISettingsSyncService, IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; -import { UserDataSyncChannel, UserDataSyncUtilServiceClient, SettingsSyncChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; +import { UserDataSyncChannel, UserDataSyncUtilServiceClient, SettingsSyncChannel, UserDataAuthTokenServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; -import { IAuthTokenService } from 'vs/platform/auth/common/auth'; -import { AuthTokenService } from 'vs/platform/auth/electron-browser/authTokenService'; -import { AuthTokenChannel } from 'vs/platform/auth/common/authTokenIpc'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { KeytarCredentialsService } from 'vs/platform/credentials/node/credentialsService'; import { UserDataAutoSync } from 'vs/platform/userDataSync/electron-browser/userDataAutoSync'; import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; +import { UserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataAuthTokenService'; export interface ISharedProcessConfiguration { readonly machineId: string; @@ -183,7 +181,7 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(IDiagnosticsService, new SyncDescriptor(DiagnosticsService)); services.set(ICredentialsService, new SyncDescriptor(KeytarCredentialsService)); - services.set(IAuthTokenService, new SyncDescriptor(AuthTokenService)); + services.set(IUserDataAuthTokenService, new SyncDescriptor(UserDataAuthTokenService)); services.set(IUserDataSyncLogService, new SyncDescriptor(UserDataSyncLogService)); services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(server.getChannel('userDataSyncUtil', activeWindowRouter))); services.set(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService)); @@ -207,8 +205,8 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat const diagnosticsChannel = new DiagnosticsChannel(diagnosticsService); server.registerChannel('diagnostics', diagnosticsChannel); - const authTokenService = accessor.get(IAuthTokenService); - const authTokenChannel = new AuthTokenChannel(authTokenService); + const authTokenService = accessor.get(IUserDataAuthTokenService); + const authTokenChannel = new UserDataAuthTokenServiceChannel(authTokenService); server.registerChannel('authToken', authTokenChannel); const settingsSyncService = accessor.get(ISettingsSyncService); diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 66f65392bd6..52c783c9117 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1280,6 +1280,25 @@ export interface RenameProvider { resolveRenameLocation?(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult; } +/** + * @internal + */ +export interface Account { + id: string; + accessToken: string; + displayName: string; +} + +/** + * @internal + */ +export interface AuthenticationProvider { + getAccount(): Promise; + onDidChangeAccount: Event; + login(): Promise; + logout(accountId: string): Promise; +} + export interface Command { id: string; diff --git a/src/vs/platform/auth/common/auth.ts b/src/vs/platform/auth/common/auth.ts deleted file mode 100644 index 81af0bbf0a4..00000000000 --- a/src/vs/platform/auth/common/auth.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { Event, Emitter } from 'vs/base/common/event'; -import { URI } from 'vs/base/common/uri'; - -export const enum AuthTokenStatus { - Initializing = 'Initializing', - SignedOut = 'SignedOut', - SignedIn = 'SignedIn', - SigningIn = 'SigningIn', - RefreshingToken = 'RefreshingToken' -} - -export const IAuthTokenService = createDecorator('IAuthTokenService'); - -export interface IAuthTokenService { - _serviceBrand: undefined; - - readonly status: AuthTokenStatus; - readonly onDidChangeStatus: Event; - readonly _onDidGetCallback: Emitter; - - getToken(): Promise; - refreshToken(): Promise; - login(): Promise; - logout(): Promise; -} diff --git a/src/vs/platform/auth/common/authTokenIpc.ts b/src/vs/platform/auth/common/authTokenIpc.ts deleted file mode 100644 index eff088c1114..00000000000 --- a/src/vs/platform/auth/common/authTokenIpc.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { Event } from 'vs/base/common/event'; -import { IAuthTokenService } from 'vs/platform/auth/common/auth'; - -export class AuthTokenChannel implements IServerChannel { - - constructor(private readonly service: IAuthTokenService) { } - - listen(_: unknown, event: string): Event { - switch (event) { - case 'onDidChangeStatus': return this.service.onDidChangeStatus; - } - throw new Error(`Event not found: ${event}`); - } - - call(context: any, command: string, args?: any): Promise { - switch (command) { - case '_getInitialStatus': return Promise.resolve(this.service.status); - case 'getToken': return this.service.getToken(); - case 'refreshToken': return this.service.refreshToken(); - case 'login': return this.service.login(); - case 'logout': return this.service.logout(); - } - throw new Error('Invalid call'); - } -} diff --git a/src/vs/platform/auth/electron-browser/authTokenService.ts b/src/vs/platform/auth/electron-browser/authTokenService.ts deleted file mode 100644 index 971e8f7a967..00000000000 --- a/src/vs/platform/auth/electron-browser/authTokenService.ts +++ /dev/null @@ -1,276 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as crypto from 'crypto'; -import * as https from 'https'; -import { Event, Emitter } from 'vs/base/common/event'; -import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth'; -import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; -import { shell } from 'electron'; -import { createServer, startServer } from 'vs/platform/auth/electron-browser/authServer'; -import { IProductService } from 'vs/platform/product/common/productService'; - -const SERVICE_NAME = 'VS Code'; -const ACCOUNT = 'MyAccount'; - -const activeDirectoryResourceId = 'https://management.core.windows.net/'; - -function toQuery(obj: any): string { - return Object.keys(obj).map(key => `${key}=${obj[key]}`).join('&'); -} - -function toBase64UrlEncoding(base64string: string) { - return base64string.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); // Need to use base64url encoding -} - -export interface IToken { - expiresIn: string; // How long access token is valid, in seconds - expiresOn: string; // When the access token expires in epoch time - accessToken: string; - refreshToken: string; -} - -export class AuthTokenService extends Disposable implements IAuthTokenService { - _serviceBrand: undefined; - - private _status: AuthTokenStatus = AuthTokenStatus.Initializing; - get status(): AuthTokenStatus { return this._status; } - private _onDidChangeStatus: Emitter = this._register(new Emitter()); - readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; - - public readonly _onDidGetCallback: Emitter = this._register(new Emitter()); - readonly onDidGetCallback: Event = this._onDidGetCallback.event; - - private _activeToken: IToken | undefined; - - constructor( - @ICredentialsService private readonly credentialsService: ICredentialsService, - @IProductService private readonly productService: IProductService - ) { - super(); - if (!this.productService.auth) { - return; - } - - this.credentialsService.getPassword(SERVICE_NAME, ACCOUNT).then(storedRefreshToken => { - if (storedRefreshToken) { - this.refresh(storedRefreshToken); - } else { - this.setStatus(AuthTokenStatus.SignedOut); - } - }); - } - - public async login(): Promise { - if (!this.productService.auth) { - throw new Error('Authentication is not configured.'); - } - - this.setStatus(AuthTokenStatus.SigningIn); - - const nonce = generateUuid(); - const { server, redirectPromise, codePromise } = createServer(nonce); - - try { - const port = await startServer(server); - shell.openExternal(`http://localhost:${port}/signin?nonce=${encodeURIComponent(nonce)}`); - - const redirectReq = await redirectPromise; - if ('err' in redirectReq) { - const { err, res } = redirectReq; - res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unkown error')}` }); - res.end(); - throw err; - } - - const host = redirectReq.req.headers.host || ''; - const updatedPortStr = (/^[^:]+:(\d+)$/.exec(Array.isArray(host) ? host[0] : host) || [])[1]; - const updatedPort = updatedPortStr ? parseInt(updatedPortStr, 10) : port; - - const state = `${updatedPort},${encodeURIComponent(nonce)}`; - - const codeVerifier = toBase64UrlEncoding(crypto.randomBytes(32).toString('base64')); - const codeChallenge = toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64')); - - let uri = URI.parse(this.productService.auth.loginUrl); - uri = uri.with({ - query: `response_type=code&client_id=${encodeURIComponent(this.productService.auth.clientId)}&redirect_uri=${this.productService.auth.redirectUrl}&state=${encodeURIComponent(state)}&resource=${activeDirectoryResourceId}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}` - }); - - await redirectReq.res.writeHead(302, { Location: uri.toString(true) }); - redirectReq.res.end(); - - const codeRes = await codePromise; - const res = codeRes.res; - - try { - if ('err' in codeRes) { - throw codeRes.err; - } - const token = await this.exchangeCodeForToken(codeRes.code, codeVerifier); - this.setToken(token); - res.writeHead(302, { Location: '/' }); - res.end(); - } catch (err) { - res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unkown error')}` }); - res.end(); - } - } finally { - setTimeout(() => { - server.close(); - }, 5000); - } - - } - - public getToken(): Promise { - return Promise.resolve(this._activeToken?.accessToken); - } - - public async refreshToken(): Promise { - if (!this._activeToken) { - throw new Error('No token to refresh'); - } - - this.refresh(this._activeToken.refreshToken); - } - - private setToken(token: IToken) { - this._activeToken = token; - this.credentialsService.setPassword(SERVICE_NAME, ACCOUNT, token.refreshToken); - this.setStatus(AuthTokenStatus.SignedIn); - } - - private exchangeCodeForToken(code: string, codeVerifier: string): Promise { - return new Promise((resolve: (value: IToken) => void, reject) => { - try { - if (!this.productService.auth) { - throw new Error('Authentication is not configured.'); - } - - const postData = toQuery({ - grant_type: 'authorization_code', - code: code, - client_id: this.productService.auth?.clientId, - code_verifier: codeVerifier, - redirect_uri: this.productService.auth?.redirectUrl - }); - - const tokenUrl = URI.parse(this.productService.auth.tokenUrl); - - const post = https.request({ - host: tokenUrl.authority, - path: tokenUrl.path, - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': postData.length - } - }, 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({ - expiresIn: json.access_token, - expiresOn: json.expires_on, - accessToken: json.access_token, - refreshToken: json.refresh_token - }); - } else { - reject(new Error('Bad!')); - } - }); - }); - - post.write(postData); - - post.end(); - post.on('error', err => { - reject(err); - }); - - } catch (e) { - reject(e); - } - }); - } - - private async refresh(refreshToken: string): Promise { - return new Promise((resolve, reject) => { - if (!this.productService.auth) { - throw new Error('Authentication is not configured.'); - } - - this.setStatus(AuthTokenStatus.RefreshingToken); - const postData = toQuery({ - refresh_token: refreshToken, - client_id: this.productService.auth?.clientId, - grant_type: 'refresh_token', - resource: activeDirectoryResourceId - }); - - const tokenUrl = URI.parse(this.productService.auth.tokenUrl); - - const post = https.request({ - host: tokenUrl.authority, - path: tokenUrl.path, - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': postData.length - } - }, 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()); - this.setToken({ - expiresIn: json.access_token, - expiresOn: json.expires_on, - accessToken: json.access_token, - refreshToken: json.refresh_token - }); - resolve(); - } else { - reject(new Error('Refreshing token failed.')); - } - }); - }); - - post.write(postData); - - post.end(); - post.on('error', err => { - this.setStatus(AuthTokenStatus.SignedOut); - reject(err); - }); - }); - } - - async logout(): Promise { - await this.credentialsService.deletePassword(SERVICE_NAME, ACCOUNT); - this._activeToken = undefined; - this.setStatus(AuthTokenStatus.SignedOut); - } - - private setStatus(status: AuthTokenStatus): void { - if (this._status !== status) { - this._status = status; - this._onDidChangeStatus.fire(status); - } - } - -} - diff --git a/src/vs/platform/product/common/productService.ts b/src/vs/platform/product/common/productService.ts index 1c37427fd5d..120fd666444 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts @@ -102,13 +102,6 @@ export interface IProductConfiguration { readonly msftInternalDomains?: string[]; readonly linkProtectionTrustedDomains?: readonly string[]; - - readonly auth?: { - loginUrl: string; - tokenUrl: string; - redirectUrl: string; - clientId: string; - }; } export interface IExeBasedExtensionTip { diff --git a/src/vs/platform/userDataSync/common/userDataAuthTokenService.ts b/src/vs/platform/userDataSync/common/userDataAuthTokenService.ts new file mode 100644 index 00000000000..03ba0c45dfe --- /dev/null +++ b/src/vs/platform/userDataSync/common/userDataAuthTokenService.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync'; + +export class UserDataAuthTokenService extends Disposable implements IUserDataAuthTokenService { + + _serviceBrand: any; + + private _onDidChangeToken: Emitter = this._register(new Emitter()); + readonly onDidChangeToken: Event = this._onDidChangeToken.event; + + private _token: string | undefined; + + constructor() { + super(); + } + + async getToken(): Promise { + return this._token; + } + + async setToken(token: string | undefined): Promise { + if (token !== this._token) { + this._token = token; + this._onDidChangeToken.fire(token); + } + } +} diff --git a/src/vs/platform/userDataSync/common/userDataAutoSync.ts b/src/vs/platform/userDataSync/common/userDataAutoSync.ts index 75123459141..e6a3d612dee 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSync.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSync.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, SyncStatus, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { Event } from 'vs/base/common/event'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { timeout } from 'vs/base/common/async'; -import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth'; +import { Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IUserDataSyncLogService, IUserDataSyncService, SyncStatus, IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync'; export class UserDataAutoSync extends Disposable { @@ -18,16 +17,17 @@ export class UserDataAutoSync extends Disposable { @IConfigurationService private readonly configurationService: IConfigurationService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, - @IAuthTokenService private readonly authTokenService: IAuthTokenService, + @IUserDataAuthTokenService private readonly userDataAuthTokenService: IUserDataAuthTokenService, ) { super(); this.updateEnablement(false); - this._register(Event.any(authTokenService.onDidChangeStatus, userDataSyncService.onDidChangeStatus)(() => this.updateEnablement(true))); + this._register(Event.any(userDataAuthTokenService.onDidChangeToken)(() => this.updateEnablement(true))); + this._register(Event.any(userDataSyncService.onDidChangeStatus)(() => this.updateEnablement(true))); this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('sync.enable'))(() => this.updateEnablement(true))); } - private updateEnablement(stopIfDisabled: boolean): void { - const enabled = this.isSyncEnabled(); + private async updateEnablement(stopIfDisabled: boolean): Promise { + const enabled = await this.isSyncEnabled(); if (this.enabled === enabled) { return; } @@ -60,10 +60,10 @@ export class UserDataAutoSync extends Disposable { } } - private isSyncEnabled(): boolean { + private async isSyncEnabled(): Promise { return this.configurationService.getValue('sync.enable') && this.userDataSyncService.status !== SyncStatus.Uninitialized - && this.authTokenService.status === AuthTokenStatus.SignedIn; + && !!(await this.userDataAuthTokenService.getToken()); } } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index eacd383b650..71c11fdd645 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -206,6 +206,17 @@ export interface IUserDataSyncUtilService { resolveFormattingOptions(resource: URI): Promise; } +export const IUserDataAuthTokenService = createDecorator('IUserDataAuthTokenService'); + +export interface IUserDataAuthTokenService { + _serviceBrand: undefined; + + readonly onDidChangeToken: Event; + + getToken(): Promise; + setToken(accessToken: string | undefined): Promise; +} + export const IUserDataSyncLogService = createDecorator('IUserDataSyncLogService'); export interface IUserDataSyncLogService extends ILogService { } diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 61f634471ba..8e647cfef99 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -5,7 +5,7 @@ import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event } from 'vs/base/common/event'; -import { IUserDataSyncService, IUserDataSyncUtilService, ISettingsSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncUtilService, ISettingsSyncService, IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync'; import { URI } from 'vs/base/common/uri'; import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; @@ -67,6 +67,25 @@ export class SettingsSyncChannel implements IServerChannel { } } +export class UserDataAuthTokenServiceChannel implements IServerChannel { + constructor(private readonly service: IUserDataAuthTokenService) { } + + listen(_: unknown, event: string): Event { + switch (event) { + case 'onDidChangeToken': return this.service.onDidChangeToken; + } + throw new Error(`Event not found: ${event}`); + } + + call(context: any, command: string, args?: any): Promise { + switch (command) { + case 'setToken': return this.service.setToken(args); + case 'getToken': return this.service.getToken(); + } + throw new Error('Invalid call'); + } +} + export class UserDataSycnUtilServiceChannel implements IServerChannel { constructor(private readonly service: IUserDataSyncUtilService) { } diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 9c4aaf3cb0c..cf1b0d10aae 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -3,14 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource, ISettingsSyncService, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource, ISettingsSyncService, IUserDataSyncLogService, IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync'; import { Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; import { Emitter, Event } from 'vs/base/common/event'; import { ExtensionsSynchroniser } from 'vs/platform/userDataSync/common/extensionsSync'; import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth'; import { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync'; import { GlobalStateSynchroniser } from 'vs/platform/userDataSync/common/globalStateSync'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -38,9 +37,9 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ constructor( @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IAuthTokenService private readonly authTokenService: IAuthTokenService, @ISettingsSyncService private readonly settingsSynchroniser: ISettingsSyncService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, + @IUserDataAuthTokenService private readonly userDataAuthTokenService: IUserDataAuthTokenService, ) { super(); this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser)); @@ -51,6 +50,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ if (this.userDataSyncStoreService.userDataSyncStore) { this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus())); + this._register(this.userDataAuthTokenService.onDidChangeToken(e => this.onDidChangeAuthTokenStatus(e))); } this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => s.onDidChangeLocal)); @@ -60,7 +60,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ if (!this.userDataSyncStoreService.userDataSyncStore) { throw new Error('Not enabled'); } - if (this.authTokenService.status === AuthTokenStatus.SignedOut) { + if (!(await this.userDataAuthTokenService.getToken())) { throw new Error('Not Authenticated. Please sign in to start sync.'); } for (const synchroniser of this.synchronisers) { @@ -76,7 +76,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ if (!this.userDataSyncStoreService.userDataSyncStore) { throw new Error('Not enabled'); } - if (this.authTokenService.status === AuthTokenStatus.SignedOut) { + if (!(await this.userDataAuthTokenService.getToken())) { throw new Error('Not Authenticated. Please sign in to start sync.'); } for (const synchroniser of this.synchronisers) { @@ -92,7 +92,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ if (!this.userDataSyncStoreService.userDataSyncStore) { throw new Error('Not enabled'); } - if (this.authTokenService.status === AuthTokenStatus.SignedOut) { + if (!(await this.userDataAuthTokenService.getToken())) { throw new Error('Not Authenticated. Please sign in to start sync.'); } for (const synchroniser of this.synchronisers) { @@ -120,7 +120,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ if (!this.userDataSyncStoreService.userDataSyncStore) { throw new Error('Not enabled'); } - if (this.authTokenService.status === AuthTokenStatus.SignedOut) { + if (!!(await this.userDataAuthTokenService.getToken())) { throw new Error('Not Authenticated. Please sign in to start sync.'); } for (const synchroniser of this.synchronisers) { @@ -135,7 +135,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ if (!this.userDataSyncStoreService.userDataSyncStore) { throw new Error('Not enabled'); } - if (this.authTokenService.status === AuthTokenStatus.SignedOut) { + if (!!(await this.userDataAuthTokenService.getToken())) { throw new Error('Not Authenticated. Please sign in to start sync.'); } for (const synchroniser of this.synchronisers) { @@ -192,4 +192,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } return SyncSource.UIState; } + + private onDidChangeAuthTokenStatus(token: string | undefined): void { + if (!token) { + this.stop(); + } + } } diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index bc499a7c48a..a77449c0577 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -4,13 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, } from 'vs/base/common/lifecycle'; -import { IUserData, IUserDataSyncStoreService, UserDataSyncStoreErrorCode, UserDataSyncStoreError, IUserDataSyncStore, getUserDataSyncStore } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, IUserDataSyncStoreService, UserDataSyncStoreErrorCode, UserDataSyncStoreError, IUserDataSyncStore, getUserDataSyncStore, IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync'; import { IRequestService, asText, isSuccess } from 'vs/platform/request/common/request'; import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request'; -import { IAuthTokenService } from 'vs/platform/auth/common/auth'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class UserDataSyncStoreService extends Disposable implements IUserDataSyncStoreService { @@ -22,7 +21,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn constructor( @IConfigurationService configurationService: IConfigurationService, @IRequestService private readonly requestService: IRequestService, - @IAuthTokenService private readonly authTokenService: IAuthTokenService, + @IUserDataAuthTokenService private readonly authTokenService: IUserDataAuthTokenService, ) { super(); this.userDataSyncStore = getUserDataSyncStore(configurationService); @@ -98,7 +97,6 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn const context = await this.requestService.request(options, token); if (context.res.statusCode === 401) { - this.authTokenService.refreshToken(); // Throw Unauthorized Error throw new UserDataSyncStoreError('Unauthorized', UserDataSyncStoreErrorCode.Unauthroized); } diff --git a/src/vs/platform/userDataSync/electron-browser/userDataAutoSync.ts b/src/vs/platform/userDataSync/electron-browser/userDataAutoSync.ts index 90ec9533ecd..821d8a05ed5 100644 --- a/src/vs/platform/userDataSync/electron-browser/userDataAutoSync.ts +++ b/src/vs/platform/userDataSync/electron-browser/userDataAutoSync.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncLogService, IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync'; import { Event } from 'vs/base/common/event'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { UserDataAutoSync as BaseUserDataAutoSync } from 'vs/platform/userDataSync/common/userDataAutoSync'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IAuthTokenService } from 'vs/platform/auth/common/auth'; export class UserDataAutoSync extends BaseUserDataAutoSync { @@ -17,7 +16,7 @@ export class UserDataAutoSync extends BaseUserDataAutoSync { @IElectronService electronService: IElectronService, @IConfigurationService configurationService: IConfigurationService, @IUserDataSyncLogService logService: IUserDataSyncLogService, - @IAuthTokenService authTokenService: IAuthTokenService, + @IUserDataAuthTokenService authTokenService: IUserDataAuthTokenService, ) { super(configurationService, userDataSyncService, logService, authTokenService); diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 27a17629b13..be480f96639 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -16,7 +16,28 @@ declare module 'vscode' { - //#region Alex - resolvers, AlexR - ports + export interface Account { + readonly id: string; + readonly accessToken: string; + readonly displayName: string; + } + + export interface AuthenticationProvider { + readonly id: string; + readonly displayName: string; + + readonly accounts: ReadonlyArray; + readonly onDidChangeAccounts: Event>; + + login(): Promise; + logout(accountId: string): Promise; + } + + export namespace authentication { + export function registerAuthenticationProvider(provider: AuthenticationProvider): Disposable; + } + + //#region Alex - resolvers export interface RemoteAuthorityResolverContext { resolveAttempt: number; diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 5560678f607..d77e37c35d8 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -59,6 +59,7 @@ import './mainThreadComments'; import './mainThreadTask'; import './mainThreadLabelService'; import './mainThreadTunnelService'; +import './mainThreadAuthentication'; import 'vs/workbench/api/common/apiCommands'; export class ExtensionPoints implements IWorkbenchContribution { diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts new file mode 100644 index 00000000000..c9fbc6e3e50 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import * as modes from 'vs/editor/common/modes'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { ExtHostAuthenticationShape, ExtHostContext, IExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol'; + +export class MainThreadAuthenticationProvider { + public readonly handle: number; + constructor( + private readonly _proxy: ExtHostAuthenticationShape, + public readonly id: string, + handle: number + ) { + this.handle = handle; + } + + accounts(): Promise> { + return this._proxy.$accounts(this.handle); + } + + login(): Promise { + return this._proxy.$login(this.handle); + } + + logout(accountId: string): Promise { + return this._proxy.$logout(this.handle, accountId); + } +} + +@extHostNamedCustomer(MainContext.MainThreadAuthentication) +export class MainThreadAuthentication extends Disposable implements MainThreadAuthenticationShape { + private readonly _proxy: ExtHostAuthenticationShape; + private _handlers = new Map(); + + constructor( + extHostContext: IExtHostContext, + @IAuthenticationService private readonly authenticationService: IAuthenticationService, + ) { + super(); + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication); + } + + $registerAuthenticationProvider(handle: number, id: string): void { + const provider = new MainThreadAuthenticationProvider(this._proxy, id, handle); + this._handlers.set(handle, id); + this.authenticationService.registerAuthenticationProvider(id, provider); + } + + $unregisterAuthenticationProvider(handle: number): void { + const id = this._handlers.get(handle); + if (!id) { + throw new Error(`No authentication provider registered with id ${id}`); + } + + this.authenticationService.unregisterAuthenticationProvider(id); + } + + $onDidChangeAccounts(handle: number, accounts: ReadonlyArray) { + const id = this._handlers.get(handle); + if (id) { + this.authenticationService.accountsUpdate(id, accounts); + } + } +} diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index d38277ed35b..0438ad43777 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -69,6 +69,7 @@ import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming'; import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; +import { ExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -128,6 +129,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress))); const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHostLabelService, new ExtHostLabelService(rpcProtocol)); const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol)); + const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol)); // Check that no named customers are missing const expected: ProxyIdentifier[] = values(ExtHostContext); @@ -175,6 +177,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }; })(); + const authentication: typeof vscode.authentication = { + registerAuthenticationProvider(provider: vscode.AuthenticationProvider): vscode.Disposable { + return extHostAuthentication.registerAuthenticationProvider(provider); + } + }; // namespace: commands const commands: typeof vscode.commands = { @@ -830,6 +837,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return { version: initData.version, // namespaces + authentication, commands, debug, env, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 227a885f876..12081788f04 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -147,6 +147,12 @@ export interface MainThreadCommentsShape extends IDisposable { $onDidCommentThreadsChange(handle: number, event: modes.CommentThreadChangedEvent): void; } +export interface MainThreadAuthenticationShape extends IDisposable { + $registerAuthenticationProvider(handle: number, id: string): void; + $unregisterAuthenticationProvider(handle: number): void; + $onDidChangeAccounts(handle: number, accounts: ReadonlyArray): void; +} + export interface MainThreadConfigurationShape extends IDisposable { $updateConfigurationOption(target: ConfigurationTarget | null, key: string, value: any, overrides: IConfigurationOverrides | undefined, scopeToLanguage: boolean | undefined): Promise; $removeConfigurationOption(target: ConfigurationTarget | null, key: string, overrides: IConfigurationOverrides | undefined, scopeToLanguage: boolean | undefined): Promise; @@ -891,6 +897,13 @@ export interface ExtHostLabelServiceShape { $registerResourceLabelFormatter(formatter: ResourceLabelFormatter): IDisposable; } +export interface ExtHostAuthenticationShape { + $accounts(handle: number): Promise>; + $login(handle: number): Promise; + $logout(handle: number, accountId: string): Promise; + // TODO rmacfarlane +} + export interface ExtHostSearchShape { $provideFileSearchResults(handle: number, session: number, query: search.IRawQuery, token: CancellationToken): Promise; $provideTextSearchResults(handle: number, session: number, query: search.IRawTextQuery, token: CancellationToken): Promise; @@ -1412,6 +1425,7 @@ export interface ExtHostTunnelServiceShape { // --- proxy identifiers export const MainContext = { + MainThreadAuthentication: createMainId('MainThreadAuthentication'), MainThreadClipboard: createMainId('MainThreadClipboard'), MainThreadCommands: createMainId('MainThreadCommands'), MainThreadComments: createMainId('MainThreadComments'), @@ -1487,5 +1501,6 @@ export const ExtHostContext = { ExtHostOutputService: createMainId('ExtHostOutputService'), ExtHostLabelService: createMainId('ExtHostLabelService'), ExtHostTheming: createMainId('ExtHostTheming'), - ExtHostTunnelService: createMainId('ExtHostTunnelService') + ExtHostTunnelService: createMainId('ExtHostTunnelService'), + ExtHostAuthentication: createMainId('ExtHostAuthentication') }; diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts new file mode 100644 index 00000000000..1ab42296c86 --- /dev/null +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as modes from 'vs/editor/common/modes'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IMainContext, MainContext, MainThreadAuthenticationShape, ExtHostAuthenticationShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IDisposable } from 'vs/base/common/lifecycle'; + +export class ExtHostAuthenticationProvider implements IDisposable { + constructor(private _provider: vscode.AuthenticationProvider, + private readonly _handle: number, + private _proxy: MainThreadAuthenticationShape) { + this._provider.onDidChangeAccounts(x => this._proxy.$onDidChangeAccounts(this._handle, this._provider.accounts)); + } + + get accounts(): ReadonlyArray { + return this._provider.accounts; + } + + login(): Promise { + return this._provider.login(); + } + + logout(accountId: string): Promise { + return this._provider.logout(accountId); + } + + dispose(): void { + this._proxy.$unregisterAuthenticationProvider(this._handle); + } +} + +export class ExtHostAuthentication implements ExtHostAuthenticationShape { + public static _handlePool: number = 0; + private _proxy: MainThreadAuthenticationShape; + private _authenticationProviders: Map = new Map(); + + constructor(mainContext: IMainContext) { + this._proxy = mainContext.getProxy(MainContext.MainThreadAuthentication); + } + + private readonly _onDidRefreshToken = new Emitter(); + readonly onDidRefreshToken: Event = this._onDidRefreshToken.event; + + registerAuthenticationProvider(provider: vscode.AuthenticationProvider) { + const handle = ExtHostAuthentication._handlePool++; + const authenticationProvider = new ExtHostAuthenticationProvider(provider, handle, this._proxy); + this._authenticationProviders.set(handle, authenticationProvider); + + this._proxy.$registerAuthenticationProvider(handle, provider.id); + return authenticationProvider; + } + + $accounts(handle: number): Promise> { + const authProvider = this._authenticationProviders.get(handle); + if (authProvider) { + return Promise.resolve(authProvider.accounts); + } + + throw new Error(`Unable to find authentication provider with handle: ${handle}`); + } + + $login(handle: number): Promise { + const authProvider = this._authenticationProviders.get(handle); + if (authProvider) { + return authProvider.login(); + } + + throw new Error(`Unable to find authentication provider with handle: ${handle}`); + } + + $logout(handle: number, accountId: string): Promise { + const authProvider = this._authenticationProviders.get(handle); + if (authProvider) { + return authProvider.logout(accountId); + } + + throw new Error(`Unable to find authentication provider with handle: ${handle}`); + } +} diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSync.ts index d0d54c3c403..31ec8de3166 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSync.ts @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncLogService, IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IAuthTokenService } from 'vs/platform/auth/common/auth'; import { Event } from 'vs/base/common/event'; import { UserDataAutoSync as BaseUserDataAutoSync } from 'vs/platform/userDataSync/common/userDataAutoSync'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -18,7 +17,7 @@ export class UserDataAutoSync extends BaseUserDataAutoSync { @IUserDataSyncService userDataSyncService: IUserDataSyncService, @IConfigurationService configurationService: IConfigurationService, @IUserDataSyncLogService logService: IUserDataSyncLogService, - @IAuthTokenService authTokenService: IAuthTokenService, + @IUserDataAuthTokenService authTokenService: IUserDataAuthTokenService, @IInstantiationService instantiationService: IInstantiationService, @IHostService hostService: IHostService, ) { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 222643aa7b4..5d87233cb66 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IUserDataSyncService, SyncStatus, SyncSource, CONTEXT_SYNC_STATE, IUserDataSyncStore, registerConfiguration, getUserDataSyncStore, ISyncConfiguration } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, SyncStatus, SyncSource, CONTEXT_SYNC_STATE, IUserDataSyncStore, registerConfiguration, getUserDataSyncStore, ISyncConfiguration, IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync'; import { localize } from 'vs/nls'; import { Disposable, MutableDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; @@ -24,9 +24,7 @@ import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { isEqual } from 'vs/base/common/resources'; import { IEditorInput } from 'vs/workbench/common/editor'; -import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { FalseContext } from 'vs/platform/contextkey/common/contextkeys'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { isWeb } from 'vs/base/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -35,25 +33,35 @@ import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/u import { timeout } from 'vs/base/common/async'; import { IOutputService } from 'vs/workbench/contrib/output/common/output'; import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; +import { IAuthenticationService, ChangeAccountEventData } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { Account } from 'vs/editor/common/modes'; -const CONTEXT_AUTH_TOKEN_STATE = new RawContextKey('authTokenStatus', AuthTokenStatus.Initializing); +const enum MSAAuthStatus { + Initializing = 'Initializing', + SignedIn = 'SignedIn', + SignedOut = 'SignedOut' +} +const CONTEXT_AUTH_TOKEN_STATE = new RawContextKey('authTokenStatus', MSAAuthStatus.Initializing); const SYNC_PUSH_LIGHT_ICON_URI = URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/userDataSync/browser/media/check-light.svg`)); const SYNC_PUSH_DARK_ICON_URI = URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/userDataSync/browser/media/check-dark.svg`)); +const MSA = 'MSA'; + export class UserDataSyncWorkbenchContribution extends Disposable implements IWorkbenchContribution { private static readonly ENABLEMENT_SETTING = 'sync.enable'; private readonly userDataSyncStore: IUserDataSyncStore | undefined; private readonly syncStatusContext: IContextKey; - private readonly authTokenContext: IContextKey; + private readonly authenticationState: IContextKey; private readonly badgeDisposable = this._register(new MutableDisposable()); private readonly conflictsWarningDisposable = this._register(new MutableDisposable()); private readonly signInNotificationDisposable = this._register(new MutableDisposable()); + private _activeAccount: Account | undefined; constructor( @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, - @IAuthTokenService private readonly authTokenService: IAuthTokenService, + @IAuthenticationService private readonly authenticationService: IAuthenticationService, @IContextKeyService contextKeyService: IContextKeyService, @IActivityService private readonly activityService: IActivityService, @INotificationService private readonly notificationService: INotificationService, @@ -66,45 +74,111 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo @IQuickInputService private readonly quickInputService: IQuickInputService, @IInstantiationService instantiationService: IInstantiationService, @IOutputService private readonly outputService: IOutputService, + @IUserDataAuthTokenService private readonly userDataAuthTokenService: IUserDataAuthTokenService, ) { super(); this.userDataSyncStore = getUserDataSyncStore(configurationService); this.syncStatusContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService); - this.authTokenContext = CONTEXT_AUTH_TOKEN_STATE.bindTo(contextKeyService); - + this.authenticationState = CONTEXT_AUTH_TOKEN_STATE.bindTo(contextKeyService); if (this.userDataSyncStore) { registerConfiguration(); - this.onDidChangeAuthTokenStatus(this.authTokenService.status); this.onDidChangeSyncStatus(this.userDataSyncService.status); - this._register(Event.debounce(authTokenService.onDidChangeStatus, () => undefined, 500)(() => this.onDidChangeAuthTokenStatus(this.authTokenService.status))); this._register(Event.debounce(userDataSyncService.onDidChangeStatus, () => undefined, 500)(() => this.onDidChangeSyncStatus(this.userDataSyncService.status))); this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING))(() => this.onDidChangeEnablement())); + this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => this.onDidRegisterAuthenticationProvider(e))); + this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => this.onDidUnregisterAuthenticationProvider(e))); + this._register(this.authenticationService.onDidChangeAccounts(e => this.onDidChangeAccounts(e))); this.registerActions(); - - if (isWeb) { - this._register(instantiationService.createInstance(UserDataAutoSync)); - } else { - this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(() => this.triggerSync())); - } + this.initializeActiveAccount().then(_ => { + if (isWeb) { + this._register(instantiationService.createInstance(UserDataAutoSync)); + } else { + this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(() => this.triggerSync())); + } + }); } } private triggerSync(): void { if (this.configurationService.getValue('sync.enable') && this.userDataSyncService.status !== SyncStatus.Uninitialized - && this.authTokenService.status === AuthTokenStatus.SignedIn) { + && this.authenticationState.get() === MSAAuthStatus.SignedIn) { this.userDataSyncService.sync(); } } - private onDidChangeAuthTokenStatus(status: AuthTokenStatus) { - this.authTokenContext.set(status); - if (status === AuthTokenStatus.SignedIn) { - this.signInNotificationDisposable.clear(); + private async initializeActiveAccount(): Promise { + const accounts = await this.authenticationService.getAccounts(MSA); + // MSA provider has not yet been registered + if (!accounts) { + return; } + + if (accounts.length === 0) { + this.activeAccount = undefined; + return; + } + + if (accounts.length === 1) { + this.activeAccount = accounts[0]; + return; + } + + const selectedAccount = await this.quickInputService.pick(accounts.map(account => { + return { + id: account.id, + label: account.displayName + }; + }), { canPickMany: false }); + + if (selectedAccount) { + this.activeAccount = accounts.filter(account => selectedAccount.id === account.id)[0]; + } + } + + get activeAccount(): Account | undefined { + return this._activeAccount; + } + + set activeAccount(account: Account | undefined) { + this._activeAccount = account; + + if (account) { + this.userDataAuthTokenService.setToken(account.accessToken); + this.authenticationState.set(MSAAuthStatus.SignedIn); + } else { + this.userDataAuthTokenService.setToken(undefined); + this.authenticationState.set(MSAAuthStatus.SignedOut); + } + this.updateBadge(); } + private onDidChangeAccounts(event: ChangeAccountEventData): void { + if (event.providerId === MSA) { + if (this.activeAccount) { + // Try to update existing account, case where access token has been refreshed + const matchingAccount = event.accounts.filter(a => a.id === this.activeAccount?.id)[0]; + this.activeAccount = matchingAccount; + } else { + this.initializeActiveAccount(); + } + } + } + + private async onDidRegisterAuthenticationProvider(providerId: string) { + if (providerId === MSA) { + await this.initializeActiveAccount(); + } + } + + private onDidUnregisterAuthenticationProvider(providerId: string) { + if (providerId === MSA) { + this.activeAccount = undefined; + this.authenticationState.reset(); + } + } + private onDidChangeSyncStatus(status: SyncStatus) { this.syncStatusContext.set(status); @@ -140,7 +214,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.updateBadge(); const enabled = this.configurationService.getValue(UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING); if (enabled) { - if (this.authTokenService.status === AuthTokenStatus.SignedOut) { + if (this.authenticationState.get() === MSAAuthStatus.SignedOut) { const handle = this.notificationService.prompt(Severity.Info, this.getSignInAndTurnOnDetailString(), [ { @@ -156,19 +230,15 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } - private updateBadge(): void { + private async updateBadge(): Promise { this.badgeDisposable.clear(); let badge: IBadge | undefined = undefined; let clazz: string | undefined; let priority: number | undefined = undefined; - if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.configurationService.getValue(UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING) && this.authTokenService.status === AuthTokenStatus.SignedOut) { + if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.configurationService.getValue(UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING) && this.authenticationState.get() === MSAAuthStatus.SignedOut) { badge = new NumberBadge(1, () => localize('sign in to sync', "Sign in to Sync")); - } else if (this.authTokenService.status === AuthTokenStatus.SigningIn) { - badge = new ProgressBadge(() => localize('signing in', "Signing in...")); - clazz = 'progress-badge'; - priority = 1; } else if (this.userDataSyncService.status === SyncStatus.HasConflicts) { badge = new NumberBadge(1, () => localize('resolve conflicts', "Resolve Conflicts")); } else if (this.userDataSyncService.status === SyncStatus.Syncing) { @@ -279,7 +349,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private async turnOn(): Promise { const message = localize('turn on sync', "Turn on Sync"); let detail: string, primaryButton: string; - if (this.authTokenService.status === AuthTokenStatus.SignedIn) { + if (this.authenticationState.get() === MSAAuthStatus.SignedIn) { detail = this.getTurnOnDetailString(); primaryButton = localize('turn on', "Turn on"); } else { @@ -291,7 +361,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo case 1: return; case 2: await this.configureSyncOptions(); return this.turnOn(); } - if (this.authTokenService.status !== AuthTokenStatus.SignedIn) { + if (this.authenticationState.get() === MSAAuthStatus.SignedOut) { await this.signIn(); } await this.configurationService.updateValue(UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING, true); @@ -357,7 +427,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private async signIn(): Promise { try { - await this.authTokenService.login(); + this.activeAccount = await this.authenticationService.login(MSA); } catch (e) { this.notificationService.error(e); throw e; @@ -365,7 +435,10 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private async signOut(): Promise { - await this.authTokenService.logout(); + if (this.activeAccount) { + await this.authenticationService.logout(MSA, this.activeAccount.id); + this.activeAccount = undefined; + } } private async continueSync(): Promise { @@ -435,7 +508,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private registerActions(): void { const turnOnSyncCommandId = 'workbench.userData.actions.syncStart'; - const turnOnSyncWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), ContextKeyExpr.not(`config.${UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING}`), CONTEXT_AUTH_TOKEN_STATE.notEqualsTo(AuthTokenStatus.SigningIn)); + const turnOnSyncWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), ContextKeyExpr.not(`config.${UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING}`), CONTEXT_AUTH_TOKEN_STATE.notEqualsTo(MSAAuthStatus.Initializing)); CommandsRegistry.registerCommand(turnOnSyncCommandId, () => this.turnOn()); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { group: '5_sync', @@ -454,7 +527,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); const signInCommandId = 'workbench.userData.actions.signin'; - const signInWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), ContextKeyExpr.has(`config.${UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING}`), CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthTokenStatus.SignedOut)); + const signInWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), ContextKeyExpr.has(`config.${UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING}`), CONTEXT_AUTH_TOKEN_STATE.isEqualTo(MSAAuthStatus.SignedOut)); CommandsRegistry.registerCommand(signInCommandId, () => this.signIn()); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { group: '5_sync', @@ -472,18 +545,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo when: signInWhenContext, }); - const signingInCommandId = 'workbench.userData.actions.signingin'; - CommandsRegistry.registerCommand(signingInCommandId, () => null); - MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - group: '5_sync', - command: { - id: signingInCommandId, - title: localize('signinig in', "Signing in..."), - precondition: FalseContext - }, - when: CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthTokenStatus.SigningIn) - }); - const stopSyncCommandId = 'workbench.userData.actions.stopSync'; CommandsRegistry.registerCommand(stopSyncCommandId, () => this.turnOff()); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { @@ -492,7 +553,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo id: stopSyncCommandId, title: localize('global activity stop sync', "Turn off sync") }, - when: ContextKeyExpr.and(ContextKeyExpr.has(`config.${UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING}`), CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthTokenStatus.SignedIn), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.HasConflicts)) + when: ContextKeyExpr.and(ContextKeyExpr.has(`config.${UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING}`), CONTEXT_AUTH_TOKEN_STATE.isEqualTo(MSAAuthStatus.SignedIn), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.HasConflicts)) }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { @@ -563,7 +624,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo id: 'workbench.userData.actions.signout', title: localize('sign out', "Sync: Sign out") }, - when: ContextKeyExpr.and(CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthTokenStatus.SignedIn)), + when: ContextKeyExpr.and(CONTEXT_AUTH_TOKEN_STATE.isEqualTo(MSAAuthStatus.SignedIn)), }; CommandsRegistry.registerCommand(signOutMenuItem.command.id, () => this.signOut()); MenuRegistry.appendMenuItem(MenuId.CommandPalette, signOutMenuItem); diff --git a/src/vs/workbench/services/authToken/browser/authTokenService.ts b/src/vs/workbench/services/authToken/browser/authTokenService.ts deleted file mode 100644 index ef7af7506a2..00000000000 --- a/src/vs/workbench/services/authToken/browser/authTokenService.ts +++ /dev/null @@ -1,74 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from 'vs/nls'; -import { Event, Emitter } from 'vs/base/common/event'; -import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth'; -import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { URI } from 'vs/base/common/uri'; - -const SERVICE_NAME = 'VS Code'; -const ACCOUNT = 'MyAccount'; - -export class AuthTokenService extends Disposable implements IAuthTokenService { - _serviceBrand: undefined; - - private _status: AuthTokenStatus = AuthTokenStatus.Initializing; - get status(): AuthTokenStatus { return this._status; } - private _onDidChangeStatus: Emitter = this._register(new Emitter()); - readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; - - readonly _onDidGetCallback: Emitter = this._register(new Emitter()); - - constructor( - @ICredentialsService private readonly credentialsService: ICredentialsService, - @IQuickInputService private readonly quickInputService: IQuickInputService - ) { - super(); - this.getToken().then(token => { - if (token) { - this.setStatus(AuthTokenStatus.SignedIn); - } else { - this.setStatus(AuthTokenStatus.SignedOut); - } - }); - } - - async getToken(): Promise { - const token = await this.credentialsService.getPassword(SERVICE_NAME, ACCOUNT); - if (token) { - return token; - } - - return; - } - - async login(): Promise { - const token = await this.quickInputService.input({ placeHolder: localize('enter token', "Please provide the auth bearer token"), ignoreFocusLost: true, }); - if (token) { - await this.credentialsService.setPassword(SERVICE_NAME, ACCOUNT, token); - this.setStatus(AuthTokenStatus.SignedIn); - } - } - - async refreshToken(): Promise { - await this.logout(); - } - - async logout(): Promise { - await this.credentialsService.deletePassword(SERVICE_NAME, ACCOUNT); - this.setStatus(AuthTokenStatus.SignedOut); - } - - private setStatus(status: AuthTokenStatus): void { - if (this._status !== status) { - this._status = status; - this._onDidChangeStatus.fire(status); - } - } - -} diff --git a/src/vs/workbench/services/authToken/electron-browser/authTokenService.ts b/src/vs/workbench/services/authToken/electron-browser/authTokenService.ts deleted file mode 100644 index ad7abd58826..00000000000 --- a/src/vs/workbench/services/authToken/electron-browser/authTokenService.ts +++ /dev/null @@ -1,61 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { Emitter, Event } from 'vs/base/common/event'; -import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth'; -import { URI } from 'vs/base/common/uri'; - -export class AuthTokenService extends Disposable implements IAuthTokenService { - - _serviceBrand: undefined; - - private readonly channel: IChannel; - - private _status: AuthTokenStatus = AuthTokenStatus.Initializing; - get status(): AuthTokenStatus { return this._status; } - private _onDidChangeStatus: Emitter = this._register(new Emitter()); - readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; - - readonly _onDidGetCallback: Emitter = this._register(new Emitter()); - - constructor( - @ISharedProcessService sharedProcessService: ISharedProcessService, - ) { - super(); - this.channel = sharedProcessService.getChannel('authToken'); - this._register(this.channel.listen('onDidChangeStatus')(status => this.updateStatus(status))); - this.channel.call('_getInitialStatus').then(status => this.updateStatus(status)); - } - - getToken(): Promise { - return this.channel.call('getToken'); - } - - login(): Promise { - return this.channel.call('login'); - } - - refreshToken(): Promise { - return this.channel.call('getToken'); - } - - logout(): Promise { - return this.channel.call('logout'); - } - - private async updateStatus(status: AuthTokenStatus): Promise { - if (status !== AuthTokenStatus.Initializing) { - this._status = status; - this._onDidChangeStatus.fire(status); - } - } - -} - -registerSingleton(IAuthTokenService, AuthTokenService); diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts new file mode 100644 index 00000000000..ca2a2fd8e1a --- /dev/null +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Account } from 'vs/editor/common/modes'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { MainThreadAuthenticationProvider } from 'vs/workbench/api/browser/mainThreadAuthentication'; + +export const IAuthenticationService = createDecorator('IAuthenticationService'); + +export interface ChangeAccountEventData { + providerId: string; + accounts: ReadonlyArray; +} + +export interface IAuthenticationService { + _serviceBrand: undefined; + + registerAuthenticationProvider(id: string, provider: MainThreadAuthenticationProvider): void; + unregisterAuthenticationProvider(id: string): void; + accountsUpdate(providerId: string, accounts: ReadonlyArray): void; + + readonly onDidRegisterAuthenticationProvider: Event; + readonly onDidUnregisterAuthenticationProvider: Event; + + readonly onDidChangeAccounts: Event; + getAccounts(providerId: string): Promise | undefined>; + login(providerId: string): Promise; + logout(providerId: string, accountId: string): Promise; +} + +export class AuthenticationService extends Disposable implements IAuthenticationService { + _serviceBrand: undefined; + + private _authenticationProviders: Map = new Map(); + + private _onDidRegisterAuthenticationProvider: Emitter = this._register(new Emitter()); + readonly onDidRegisterAuthenticationProvider: Event = this._onDidRegisterAuthenticationProvider.event; + + private _onDidUnregisterAuthenticationProvider: Emitter = this._register(new Emitter()); + readonly onDidUnregisterAuthenticationProvider: Event = this._onDidUnregisterAuthenticationProvider.event; + + private _onDidChangeAccounts: Emitter = this._register(new Emitter()); + readonly onDidChangeAccounts: Event = this._onDidChangeAccounts.event; + + constructor() { + super(); + } + + registerAuthenticationProvider(id: string, authenticationProvider: MainThreadAuthenticationProvider): void { + this._authenticationProviders.set(id, authenticationProvider); + this._onDidRegisterAuthenticationProvider.fire(id); + } + + unregisterAuthenticationProvider(id: string): void { + this._authenticationProviders.delete(id); + this._onDidUnregisterAuthenticationProvider.fire(id); + } + + accountsUpdate(providerId: string, accounts: ReadonlyArray): void { + this._onDidChangeAccounts.fire({ providerId, accounts }); + } + + async getAccounts(id: string): Promise | undefined> { + const authProvider = this._authenticationProviders.get(id); + if (authProvider) { + return await authProvider.accounts(); + } + + return undefined; + } + + async login(id: string): Promise { + const authProvider = this._authenticationProviders.get(id); + if (authProvider) { + return authProvider.login(); + } else { + throw new Error(`No authentication provider '${id}' is currently registered.`); + } + } + + async logout(id: string, accountId: string): Promise { + const authProvider = this._authenticationProviders.get(id); + if (authProvider) { + return authProvider.logout(accountId); + } else { + throw new Error(`No authentication provider '${id}' is currently registered.`); + } + } +} + +registerSingleton(IAuthenticationService, AuthenticationService); diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataAuthTokenService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataAuthTokenService.ts new file mode 100644 index 00000000000..998630e1ef9 --- /dev/null +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataAuthTokenService.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; + +export class UserDataAuthTokenService extends Disposable implements IUserDataAuthTokenService { + + _serviceBrand: undefined; + + private readonly channel: IChannel; + private _onDidChangeToken: Emitter = this._register(new Emitter()); + readonly onDidChangeToken: Event = this._onDidChangeToken.event; + + constructor( + @ISharedProcessService sharedProcessService: ISharedProcessService, + ) { + super(); + this.channel = sharedProcessService.getChannel('authToken'); + } + + getToken(): Promise { + return this.channel.call('getToken'); + } + + setToken(token: string | undefined): Promise { + return this.channel.call('setToken', token); + } +} + +registerSingleton(IUserDataAuthTokenService, UserDataAuthTokenService); diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index bd8b76dad8f..f9656900129 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -51,7 +51,8 @@ import 'vs/workbench/services/workspaces/electron-browser/workspacesService'; import 'vs/workbench/services/workspaces/electron-browser/workspaceEditingService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncService'; import 'vs/workbench/services/userDataSync/electron-browser/settingsSyncService'; -import 'vs/workbench/services/authToken/electron-browser/authTokenService'; +import 'vs/workbench/services/userDataSync/electron-browser/userDataAuthTokenService'; +import 'vs/workbench/services/authentication/browser/authenticationService'; import 'vs/workbench/services/host/electron-browser/desktopHostService'; import 'vs/workbench/services/request/electron-browser/requestService'; import 'vs/workbench/services/lifecycle/electron-browser/lifecycleService'; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index e6c3807eeff..9db377b87dd 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -63,9 +63,8 @@ import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { TunnelService } from 'vs/workbench/services/remote/common/tunnelService'; import { ILoggerService } from 'vs/platform/log/common/log'; import { FileLoggerService } from 'vs/platform/log/common/fileLogService'; -import { IAuthTokenService } from 'vs/platform/auth/common/auth'; -import { AuthTokenService } from 'vs/workbench/services/authToken/browser/authTokenService'; import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataSyncLogService, ISettingsSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { AuthenticationService, IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; @@ -77,7 +76,7 @@ registerSingleton(IAccessibilityService, BrowserAccessibilityService, true); registerSingleton(IContextMenuService, ContextMenuService); registerSingleton(ITunnelService, TunnelService, true); registerSingleton(ILoggerService, FileLoggerService); -registerSingleton(IAuthTokenService, AuthTokenService); +registerSingleton(IAuthenticationService, AuthenticationService); registerSingleton(IUserDataSyncLogService, UserDataSyncLogService); registerSingleton(IUserDataSyncStoreService, UserDataSyncStoreService); registerSingleton(ISettingsSyncService, SettingsSynchroniser);