diff --git a/build/darwin/create-universal-app.js b/build/darwin/create-universal-app.js index 1f53369356b..bced5a7166f 100644 --- a/build/darwin/create-universal-app.js +++ b/build/darwin/create-universal-app.js @@ -24,6 +24,8 @@ async function main(buildDir) { const filesToSkip = [ '**/CodeResources', '**/Credits.rtf', + // TODO: Should we consider expanding this to other files in this area? + '**/node_modules/@parcel/node-addon-api/nothing.target.mk' ]; await (0, vscode_universal_bundler_1.makeUniversalApp)({ x64AppPath, diff --git a/build/darwin/create-universal-app.ts b/build/darwin/create-universal-app.ts index 1f19053494d..e05f780b38d 100644 --- a/build/darwin/create-universal-app.ts +++ b/build/darwin/create-universal-app.ts @@ -28,6 +28,8 @@ async function main(buildDir?: string) { const filesToSkip = [ '**/CodeResources', '**/Credits.rtf', + // TODO: Should we consider expanding this to other files in this area? + '**/node_modules/@parcel/node-addon-api/nothing.target.mk' ]; await makeUniversalApp({ diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index 6b8ca04e809..13f27d6db47 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -230,28 +230,62 @@ exports.compileExtensionMediaBuildTask = compileExtensionMediaBuildTask; //#region Azure Pipelines +/** + * Cleans the build directory for extensions + */ const cleanExtensionsBuildTask = task.define('clean-extensions-build', util.rimraf('.build/extensions')); -const compileExtensionsBuildTask = task.define('compile-extensions-build', task.series( - cleanExtensionsBuildTask, - task.define('bundle-marketplace-extensions-build', () => ext.packageMarketplaceExtensionsStream(false).pipe(gulp.dest('.build'))), - task.define('bundle-extensions-build', () => ext.packageLocalExtensionsStream(false, false).pipe(gulp.dest('.build'))), -)); +exports.cleanExtensionsBuildTask = cleanExtensionsBuildTask; -gulp.task(compileExtensionsBuildTask); -gulp.task(task.define('extensions-ci', task.series(compileExtensionsBuildTask, compileExtensionMediaBuildTask))); +/** + * brings in the marketplace extensions for the build + */ +const bundleMarketplaceExtensionsBuildTask = task.define('bundle-marketplace-extensions-build', () => ext.packageMarketplaceExtensionsStream(false).pipe(gulp.dest('.build'))); + +/** + * Compiles the non-native extensions for the build + * @note this does not clean the directory ahead of it. See {@link cleanExtensionsBuildTask} for that. + */ +const compileNonNativeExtensionsBuildTask = task.define('compile-non-native-extensions-build', task.series( + bundleMarketplaceExtensionsBuildTask, + task.define('bundle-non-native-extensions-build', () => ext.packageNonNativeLocalExtensionsStream().pipe(gulp.dest('.build'))) +)); +gulp.task(compileNonNativeExtensionsBuildTask); +exports.compileNonNativeExtensionsBuildTask = compileNonNativeExtensionsBuildTask; + +/** + * Compiles the native extensions for the build + * @note this does not clean the directory ahead of it. See {@link cleanExtensionsBuildTask} for that. + */ +const compileNativeExtensionsBuildTask = task.define('compile-native-extensions-build', () => ext.packageNativeLocalExtensionsStream().pipe(gulp.dest('.build'))); +gulp.task(compileNativeExtensionsBuildTask); +exports.compileNativeExtensionsBuildTask = compileNativeExtensionsBuildTask; + +/** + * Compiles the extensions for the build. + * This is essentially a helper task that combines {@link cleanExtensionsBuildTask}, {@link compileNonNativeExtensionsBuildTask} and {@link compileNativeExtensionsBuildTask} + */ +const compileAllExtensionsBuildTask = task.define('compile-extensions-build', task.series( + cleanExtensionsBuildTask, + bundleMarketplaceExtensionsBuildTask, + task.define('bundle-extensions-build', () => ext.packageAllLocalExtensionsStream(false, false).pipe(gulp.dest('.build'))), +)); +gulp.task(compileAllExtensionsBuildTask); +exports.compileAllExtensionsBuildTask = compileAllExtensionsBuildTask; + +// This task is run in the compilation stage of the CI pipeline. We only compile the non-native extensions since those can be fully built regardless of platform. +// This defers the native extensions to the platform specific stage of the CI pipeline. +gulp.task(task.define('extensions-ci', task.series(compileNonNativeExtensionsBuildTask, compileExtensionMediaBuildTask))); const compileExtensionsBuildPullRequestTask = task.define('compile-extensions-build-pr', task.series( cleanExtensionsBuildTask, - task.define('bundle-marketplace-extensions-build', () => ext.packageMarketplaceExtensionsStream(false).pipe(gulp.dest('.build'))), - task.define('bundle-extensions-build-pr', () => ext.packageLocalExtensionsStream(false, true).pipe(gulp.dest('.build'))), + bundleMarketplaceExtensionsBuildTask, + task.define('bundle-extensions-build-pr', () => ext.packageAllLocalExtensionsStream(false, true).pipe(gulp.dest('.build'))), )); - gulp.task(compileExtensionsBuildPullRequestTask); + +// This task is run in the compilation stage of the PR pipeline. We compile all extensions in it to verify compilation. gulp.task(task.define('extensions-ci-pr', task.series(compileExtensionsBuildPullRequestTask, compileExtensionMediaBuildTask))); - -exports.compileExtensionsBuildTask = compileExtensionsBuildTask; - //#endregion const compileWebExtensionsTask = task.define('compile-web', () => buildWebExtensions(false)); diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index 7ab8b138da8..4f00317173d 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -27,7 +27,7 @@ const File = require('vinyl'); const fs = require('fs'); const glob = require('glob'); const { compileBuildTask } = require('./gulpfile.compile'); -const { compileExtensionsBuildTask, compileExtensionMediaBuildTask } = require('./gulpfile.extensions'); +const { cleanExtensionsBuildTask, compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileExtensionMediaBuildTask } = require('./gulpfile.extensions'); const { vscodeWebResourceIncludes, createVSCodeWebFileContentMapper } = require('./gulpfile.vscode.web'); const cp = require('child_process'); const log = require('fancy-log'); @@ -468,6 +468,7 @@ function tweakProductForServerWeb(product) { const destinationFolderName = `vscode-${type}${dashed(platform)}${dashed(arch)}`; const serverTaskCI = task.define(`vscode-${type}${dashed(platform)}${dashed(arch)}${dashed(minified)}-ci`, task.series( + compileNativeExtensionsBuildTask, gulp.task(`node-${platform}-${arch}`), util.rimraf(path.join(BUILD_ROOT, destinationFolderName)), packageTask(type, platform, arch, sourceFolderName, destinationFolderName) @@ -476,7 +477,8 @@ function tweakProductForServerWeb(product) { const serverTask = task.define(`vscode-${type}${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series( compileBuildTask, - compileExtensionsBuildTask, + cleanExtensionsBuildTask, + compileNonNativeExtensionsBuildTask, compileExtensionMediaBuildTask, minified ? minifyTask : bundleTask, serverTaskCI diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 5dc9437bc2c..030c39a861e 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -31,7 +31,7 @@ const { config } = require('./lib/electron'); const createAsar = require('./lib/asar').createAsar; const minimist = require('minimist'); const { compileBuildTask } = require('./gulpfile.compile'); -const { compileExtensionsBuildTask, compileExtensionMediaBuildTask } = require('./gulpfile.extensions'); +const { compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileAllExtensionsBuildTask, compileExtensionMediaBuildTask, cleanExtensionsBuildTask } = require('./gulpfile.extensions'); const { promisify } = require('util'); const glob = promisify(require('glob')); const rcedit = promisify(require('rcedit')); @@ -487,6 +487,7 @@ BUILD_TARGETS.forEach(buildTarget => { const destinationFolderName = `VSCode${dashed(platform)}${dashed(arch)}`; const tasks = [ + compileNativeExtensionsBuildTask, util.rimraf(path.join(buildRoot, destinationFolderName)), packageTask(platform, arch, sourceFolderName, destinationFolderName, opts) ]; @@ -500,7 +501,8 @@ BUILD_TARGETS.forEach(buildTarget => { const vscodeTask = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series( compileBuildTask, - compileExtensionsBuildTask, + cleanExtensionsBuildTask, + compileNonNativeExtensionsBuildTask, compileExtensionMediaBuildTask, minified ? minifyVSCodeTask : bundleVSCodeTask, vscodeTaskCI @@ -537,7 +539,7 @@ gulp.task(task.define( 'vscode-translations-export', task.series( core, - compileExtensionsBuildTask, + compileAllExtensionsBuildTask, function () { const pathToMetadata = './out-build/nls.metadata.json'; const pathToExtensions = '.build/extensions/*'; diff --git a/build/gulpfile.vscode.web.js b/build/gulpfile.vscode.web.js index 1b498cea837..02b17022fa8 100644 --- a/build/gulpfile.vscode.web.js +++ b/build/gulpfile.vscode.web.js @@ -202,7 +202,7 @@ function packageTask(sourceFolderName, destinationFolderName) { const compileWebExtensionsBuildTask = task.define('compile-web-extensions-build', task.series( task.define('clean-web-extensions-build', util.rimraf('.build/web/extensions')), - task.define('bundle-web-extensions-build', () => extensions.packageLocalExtensionsStream(true, false).pipe(gulp.dest('.build/web'))), + task.define('bundle-web-extensions-build', () => extensions.packageAllLocalExtensionsStream(true, false).pipe(gulp.dest('.build/web'))), task.define('bundle-marketplace-web-extensions-build', () => extensions.packageMarketplaceExtensionsStream(true).pipe(gulp.dest('.build/web'))), task.define('bundle-web-extension-media-build', () => extensions.buildExtensionMedia(false, '.build/web/extensions')), )); diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 4e704bdab6f..8630c8fa061 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -6,7 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.fromMarketplace = fromMarketplace; exports.fromGithub = fromGithub; -exports.packageLocalExtensionsStream = packageLocalExtensionsStream; +exports.packageNonNativeLocalExtensionsStream = packageNonNativeLocalExtensionsStream; +exports.packageNativeLocalExtensionsStream = packageNativeLocalExtensionsStream; +exports.packageAllLocalExtensionsStream = packageAllLocalExtensionsStream; exports.packageMarketplaceExtensionsStream = packageMarketplaceExtensionsStream; exports.scanBuiltinExtensions = scanBuiltinExtensions; exports.translatePackageJSON = translatePackageJSON; @@ -245,6 +247,13 @@ function fromGithub({ name, version, repo, sha256, metadata }) { .pipe(json({ __metadata: metadata })) .pipe(packageJsonFilter.restore); } +/** + * All extensions that are known to have some native component and thus must be built on the + * platform that is being built. + */ +const nativeExtensions = [ + 'microsoft-authentication', +]; const excludedExtensions = [ 'vscode-api-tests', 'vscode-colorize-tests', @@ -289,7 +298,46 @@ function isWebExtension(manifest) { } return true; } -function packageLocalExtensionsStream(forWeb, disableMangle) { +/** + * Package local extensions that are known to not have native dependencies. Mutually exclusive to {@link packageNativeLocalExtensionsStream}. + * @param forWeb build the extensions that have web targets + * @param disableMangle disable the mangler + * @returns a stream + */ +function packageNonNativeLocalExtensionsStream(forWeb, disableMangle) { + return doPackageLocalExtensionsStream(forWeb, disableMangle, false); +} +/** + * Package local extensions that are known to have native dependencies. Mutually exclusive to {@link packageNonNativeLocalExtensionsStream}. + * @note it's possible that the extension does not have native dependencies for the current platform, especially if building for the web, + * but we simplify the logic here by having a flat list of extensions (See {@link nativeExtensions}) that are known to have native + * dependencies on some platform and thus should be packaged on the platform that they are building for. + * @param forWeb build the extensions that have web targets + * @param disableMangle disable the mangler + * @returns a stream + */ +function packageNativeLocalExtensionsStream(forWeb, disableMangle) { + return doPackageLocalExtensionsStream(forWeb, disableMangle, true); +} +/** + * Package all the local extensions... both those that are known to have native dependencies and those that are not. + * @param forWeb build the extensions that have web targets + * @param disableMangle disable the mangler + * @returns a stream + */ +function packageAllLocalExtensionsStream(forWeb, disableMangle) { + return es.merge([ + packageNonNativeLocalExtensionsStream(forWeb, disableMangle), + packageNativeLocalExtensionsStream(forWeb, disableMangle) + ]); +} +/** + * @param forWeb build the extensions that have web targets + * @param disableMangle disable the mangler + * @param native build the extensions that are marked as having native dependencies + */ +function doPackageLocalExtensionsStream(forWeb, disableMangle, native) { + const nativeExtensionsSet = new Set(nativeExtensions); const localExtensionsDescriptions = (glob.sync('extensions/*/package.json') .map(manifestPath => { const absoluteManifestPath = path.join(root, manifestPath); @@ -297,6 +345,7 @@ function packageLocalExtensionsStream(forWeb, disableMangle) { const extensionName = path.basename(extensionPath); return { name: extensionName, path: extensionPath, manifestPath: absoluteManifestPath }; }) + .filter(({ name }) => native ? nativeExtensionsSet.has(name) : !nativeExtensionsSet.has(name)) .filter(({ name }) => excludedExtensions.indexOf(name) === -1) .filter(({ name }) => builtInExtensions.every(b => b.name !== name)) .filter(({ manifestPath }) => (forWeb ? isWebExtension(require(manifestPath)) : true))); diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 14f2de9fef5..a881d3153da 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -277,6 +277,14 @@ export function fromGithub({ name, version, repo, sha256, metadata }: IExtension .pipe(packageJsonFilter.restore); } +/** + * All extensions that are known to have some native component and thus must be built on the + * platform that is being built. + */ +const nativeExtensions = [ + 'microsoft-authentication', +]; + const excludedExtensions = [ 'vscode-api-tests', 'vscode-colorize-tests', @@ -334,7 +342,49 @@ function isWebExtension(manifest: IExtensionManifest): boolean { return true; } -export function packageLocalExtensionsStream(forWeb: boolean, disableMangle: boolean): Stream { +/** + * Package local extensions that are known to not have native dependencies. Mutually exclusive to {@link packageNativeLocalExtensionsStream}. + * @param forWeb build the extensions that have web targets + * @param disableMangle disable the mangler + * @returns a stream + */ +export function packageNonNativeLocalExtensionsStream(forWeb: boolean, disableMangle: boolean): Stream { + return doPackageLocalExtensionsStream(forWeb, disableMangle, false); +} + +/** + * Package local extensions that are known to have native dependencies. Mutually exclusive to {@link packageNonNativeLocalExtensionsStream}. + * @note it's possible that the extension does not have native dependencies for the current platform, especially if building for the web, + * but we simplify the logic here by having a flat list of extensions (See {@link nativeExtensions}) that are known to have native + * dependencies on some platform and thus should be packaged on the platform that they are building for. + * @param forWeb build the extensions that have web targets + * @param disableMangle disable the mangler + * @returns a stream + */ +export function packageNativeLocalExtensionsStream(forWeb: boolean, disableMangle: boolean): Stream { + return doPackageLocalExtensionsStream(forWeb, disableMangle, true); +} + +/** + * Package all the local extensions... both those that are known to have native dependencies and those that are not. + * @param forWeb build the extensions that have web targets + * @param disableMangle disable the mangler + * @returns a stream + */ +export function packageAllLocalExtensionsStream(forWeb: boolean, disableMangle: boolean): Stream { + return es.merge([ + packageNonNativeLocalExtensionsStream(forWeb, disableMangle), + packageNativeLocalExtensionsStream(forWeb, disableMangle) + ]); +} + +/** + * @param forWeb build the extensions that have web targets + * @param disableMangle disable the mangler + * @param native build the extensions that are marked as having native dependencies + */ +function doPackageLocalExtensionsStream(forWeb: boolean, disableMangle: boolean, native: boolean): Stream { + const nativeExtensionsSet = new Set(nativeExtensions); const localExtensionsDescriptions = ( (glob.sync('extensions/*/package.json')) .map(manifestPath => { @@ -343,6 +393,7 @@ export function packageLocalExtensionsStream(forWeb: boolean, disableMangle: boo const extensionName = path.basename(extensionPath); return { name: extensionName, path: extensionPath, manifestPath: absoluteManifestPath }; }) + .filter(({ name }) => native ? nativeExtensionsSet.has(name) : !nativeExtensionsSet.has(name)) .filter(({ name }) => excludedExtensions.indexOf(name) === -1) .filter(({ name }) => builtInExtensions.every(b => b.name !== name)) .filter(({ manifestPath }) => (forWeb ? isWebExtension(require(manifestPath)) : true)) diff --git a/extensions/microsoft-authentication/.vscodeignore b/extensions/microsoft-authentication/.vscodeignore index 98b90d34d82..e7feddb5da8 100644 --- a/extensions/microsoft-authentication/.vscodeignore +++ b/extensions/microsoft-authentication/.vscodeignore @@ -12,3 +12,4 @@ vsc-extension-quickstart.md **/tslint.json **/*.map **/*.ts +packageMocks/ diff --git a/extensions/microsoft-authentication/extension.webpack.config.js b/extensions/microsoft-authentication/extension.webpack.config.js index 45600607fc5..9578763a41d 100644 --- a/extensions/microsoft-authentication/extension.webpack.config.js +++ b/extensions/microsoft-authentication/extension.webpack.config.js @@ -8,10 +8,42 @@ 'use strict'; const withDefaults = require('../shared.webpack.config'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); +const path = require('path'); +const { NormalModuleReplacementPlugin } = require('webpack'); + +const isWindows = process.platform === 'win32'; module.exports = withDefaults({ context: __dirname, entry: { extension: './src/extension.ts' - } + }, + externals: { + // The @azure/msal-node-runtime package requires this native node module (.node). + // It is currently only included on Windows, but the package handles unsupported platforms + // gracefully. + './msal-node-runtime': 'commonjs ./msal-node-runtime' + }, + plugins: [ + ...withDefaults.nodePlugins(__dirname), + new CopyWebpackPlugin({ + patterns: [ + { + // The native files we need to ship with the extension + from: '**/dist/msal*.(node|dll)', + to: '[name][ext]', + // These will only be present on Windows for now + noErrorOnMissing: !isWindows + } + ] + }), + // We don't use the feature that uses Dpapi, so we can just replace it with a mock. + // This is a bit of a hack, but it's the easiest way to do it. Really, msal should + // handle when this native node module is not available. + new NormalModuleReplacementPlugin( + /\.\.\/Dpapi\.mjs/, + path.resolve(__dirname, 'packageMocks', 'dpapi', 'dpapi.js') + ) + ] }); diff --git a/extensions/microsoft-authentication/package-lock.json b/extensions/microsoft-authentication/package-lock.json index 8f05b14f02a..c52e019da9a 100644 --- a/extensions/microsoft-authentication/package-lock.json +++ b/extensions/microsoft-authentication/package-lock.json @@ -11,7 +11,9 @@ "dependencies": { "@azure/ms-rest-azure-env": "^2.0.0", "@azure/msal-node": "^2.13.1", + "@azure/msal-node-extensions": "^1.3.0", "@vscode/extension-telemetry": "^0.9.0", + "keytar": "file:./packageMocks/keytar", "vscode-tas-client": "^0.1.84" }, "devDependencies": { @@ -51,6 +53,40 @@ "node": ">=16" } }, + "node_modules/@azure/msal-node-extensions": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node-extensions/-/msal-node-extensions-1.3.0.tgz", + "integrity": "sha512-7rXN+9hDm3NncIfNnMyoFtsnz2AlUtmK5rsY3P+fhhbH+GOk0W5Y1BASvAB6RCcKdO+qSIK3ZA6VHQYy4iS/1w==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "14.15.0", + "@azure/msal-node-runtime": "^0.17.1", + "keytar": "^7.8.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@azure/msal-node-extensions/node_modules/@azure/msal-common": { + "version": "14.15.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.15.0.tgz", + "integrity": "sha512-ImAQHxmpMneJ/4S8BRFhjt1MZ3bppmpRPYYNyzeQPeFN288YKbb8TmmISQEbtfkQ1BPASvYZU5doIZOPBAqENQ==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node-extensions/packageMocks/keytar": { + "extraneous": true + }, + "node_modules/@azure/msal-node-runtime": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node-runtime/-/msal-node-runtime-0.17.1.tgz", + "integrity": "sha512-qAfTg+iGJsg+XvD9nmknI63+XuoX32oT+SX4wJdFz7CS6ETVpSHoroHVaUmsTU1H7H0+q1/ZkP988gzPRMYRsg==", + "hasInstallScript": true, + "license": "MIT" + }, "node_modules/@microsoft/1ds-core-js": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.0.3.tgz", @@ -315,6 +351,10 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/keytar": { + "resolved": "packageMocks/keytar", + "link": true + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -376,6 +416,9 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/packageMocks/keytar": { + "extraneous": true + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -435,6 +478,9 @@ "engines": { "vscode": "^1.85.0" } + }, + "packageMocks/keytar": { + "version": "7.9.0" } } } diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 9eea07ec024..0f5b707f18a 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -14,7 +14,8 @@ ], "activationEvents": [], "enabledApiProposals": [ - "idToken" + "idToken", + "nativeWindowHandle" ], "capabilities": { "virtualWorkspaces": true, @@ -131,7 +132,9 @@ "dependencies": { "@azure/ms-rest-azure-env": "^2.0.0", "@azure/msal-node": "^2.13.1", + "@azure/msal-node-extensions": "^1.3.0", "@vscode/extension-telemetry": "^0.9.0", + "keytar": "file:./packageMocks/keytar", "vscode-tas-client": "^0.1.84" }, "repository": { diff --git a/extensions/microsoft-authentication/packageMocks/dpapi/dpapi.js b/extensions/microsoft-authentication/packageMocks/dpapi/dpapi.js new file mode 100644 index 00000000000..636112a188f --- /dev/null +++ b/extensions/microsoft-authentication/packageMocks/dpapi/dpapi.js @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +class defaultDpapi { + protectData() { + throw new Error('Dpapi bindings unavailable'); + } + unprotectData() { + throw new Error('Dpapi bindings unavailable'); + } +} +const Dpapi = new defaultDpapi(); +export { Dpapi }; diff --git a/extensions/microsoft-authentication/packageMocks/keytar/index.js b/extensions/microsoft-authentication/packageMocks/keytar/index.js new file mode 100644 index 00000000000..418d592ffdd --- /dev/null +++ b/extensions/microsoft-authentication/packageMocks/keytar/index.js @@ -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. + *--------------------------------------------------------------------------------------------*/ + +exports.setPassword = () => Promise.resolve(); +exports.getPassword = () => Promise.resolve(); +exports.deletePassword = () => Promise.resolve(); diff --git a/extensions/microsoft-authentication/packageMocks/keytar/package.json b/extensions/microsoft-authentication/packageMocks/keytar/package.json new file mode 100644 index 00000000000..0014152ac93 --- /dev/null +++ b/extensions/microsoft-authentication/packageMocks/keytar/package.json @@ -0,0 +1,7 @@ +{ + "name": "keytar", + "version": "7.9.0", + "description": "OVERRIDE Keytar since we don't need the feature", + "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node", + "main": "index.js" +} diff --git a/extensions/microsoft-authentication/src/common/accountAccess.ts b/extensions/microsoft-authentication/src/common/accountAccess.ts new file mode 100644 index 00000000000..a8fdeefef98 --- /dev/null +++ b/extensions/microsoft-authentication/src/common/accountAccess.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, Event, EventEmitter, SecretStorage } from 'vscode'; +import { AccountInfo } from '@azure/msal-node'; + +interface IAccountAccess { + onDidAccountAccessChange: Event; + isAllowedAccess(account: AccountInfo): boolean; + setAllowedAccess(account: AccountInfo, allowed: boolean): void; +} + +export class ScopedAccountAccess implements IAccountAccess { + private readonly _onDidAccountAccessChangeEmitter = new EventEmitter(); + readonly onDidAccountAccessChange = this._onDidAccountAccessChangeEmitter.event; + + private readonly _accountAccessSecretStorage: AccountAccessSecretStorage; + + private value = new Array(); + + constructor( + private readonly _secretStorage: SecretStorage, + private readonly _cloudName: string, + private readonly _clientId: string, + private readonly _authority: string + ) { + this._accountAccessSecretStorage = new AccountAccessSecretStorage(this._secretStorage, this._cloudName, this._clientId, this._authority); + this._accountAccessSecretStorage.onDidChange(() => this.update()); + } + + initialize() { + return this.update(); + } + + isAllowedAccess(account: AccountInfo): boolean { + return this.value.includes(account.homeAccountId); + } + + async setAllowedAccess(account: AccountInfo, allowed: boolean): Promise { + if (allowed) { + if (this.value.includes(account.homeAccountId)) { + return; + } + await this._accountAccessSecretStorage.store([...this.value, account.homeAccountId]); + return; + } + await this._accountAccessSecretStorage.store(this.value.filter(id => id !== account.homeAccountId)); + } + + private async update() { + const current = new Set(this.value); + const value = await this._accountAccessSecretStorage.get(); + + this.value = value ?? []; + if (current.size !== this.value.length || !this.value.every(id => current.has(id))) { + this._onDidAccountAccessChangeEmitter.fire(); + } + } +} + +export class AccountAccessSecretStorage { + private _disposable: Disposable; + + private readonly _onDidChangeEmitter = new EventEmitter; + readonly onDidChange: Event = this._onDidChangeEmitter.event; + + private readonly _key = `accounts-${this._cloudName}-${this._clientId}-${this._authority}`; + + constructor( + private readonly _secretStorage: SecretStorage, + private readonly _cloudName: string, + private readonly _clientId: string, + private readonly _authority: string + ) { + this._disposable = Disposable.from( + this._onDidChangeEmitter, + this._secretStorage.onDidChange(e => { + if (e.key === this._key) { + this._onDidChangeEmitter.fire(); + } + }) + ); + } + + async get(): Promise { + const value = await this._secretStorage.get(this._key); + if (!value) { + return undefined; + } + return JSON.parse(value); + } + + store(value: string[]): Thenable { + return this._secretStorage.store(this._key, JSON.stringify(value)); + } + + delete(): Thenable { + return this._secretStorage.delete(this._key); + } + + dispose() { + this._disposable.dispose(); + } +} diff --git a/extensions/microsoft-authentication/src/node/authProvider.ts b/extensions/microsoft-authentication/src/node/authProvider.ts index 0c285e0cb2b..81c4008d381 100644 --- a/extensions/microsoft-authentication/src/node/authProvider.ts +++ b/extensions/microsoft-authentication/src/node/authProvider.ts @@ -156,6 +156,7 @@ export class MsalAuthProvider implements AuthenticationProvider { let result: AuthenticationResult | undefined; try { + const windowHandle = env.handle ? Buffer.from(env.handle, 'base64') : undefined; result = await cachedPca.acquireTokenInteractive({ openBrowser: async (url: string) => { await env.openExternal(Uri.parse(url)); }, scopes: scopeData.scopesToSend, @@ -167,7 +168,8 @@ export class MsalAuthProvider implements AuthenticationProvider { loginHint: options.account?.label, // If we aren't logging in to a specific account, then we can use the prompt to make sure they get // the option to choose a different account. - prompt: options.account?.label ? undefined : 'select_account' + prompt: options.account?.label ? undefined : 'select_account', + windowHandle }); } catch (e) { if (e instanceof CancellationError) { @@ -196,12 +198,14 @@ export class MsalAuthProvider implements AuthenticationProvider { // The user wants to try the loopback client or we got an error likely due to spinning up the server const loopbackClient = new UriHandlerLoopbackClient(this._uriHandler, redirectUri, this._logger); try { + const windowHandle = env.handle ? Buffer.from(env.handle) : undefined; result = await cachedPca.acquireTokenInteractive({ openBrowser: (url: string) => loopbackClient.openBrowser(url), scopes: scopeData.scopesToSend, loopbackClient, loginHint: options.account?.label, - prompt: options.account?.label ? undefined : 'select_account' + prompt: options.account?.label ? undefined : 'select_account', + windowHandle }); } catch (e) { this._telemetryReporter.sendLoginFailedEvent(); diff --git a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts index c679610466a..e23ffeb3b9c 100644 --- a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts +++ b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts @@ -4,11 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { PublicClientApplication, AccountInfo, Configuration, SilentFlowRequest, AuthenticationResult, InteractiveRequest, LogLevel } from '@azure/msal-node'; +import { NativeBrokerPlugin } from '@azure/msal-node-extensions'; import { Disposable, Memento, SecretStorage, LogOutputChannel, window, ProgressLocation, l10n, EventEmitter } from 'vscode'; import { Delayer, raceCancellationAndTimeoutError } from '../common/async'; import { SecretStorageCachePlugin } from '../common/cachePlugin'; import { MsalLoggerOptions } from '../common/loggerOptions'; import { ICachedPublicClientApplication } from '../common/publicClientCache'; +import { ScopedAccountAccess } from '../common/accountAccess'; export class CachedPublicClientApplication implements ICachedPublicClientApplication { private _pca: PublicClientApplication; @@ -24,19 +26,24 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica // Include the prefix as a differentiator to other secrets `pca:${JSON.stringify({ clientId: this._clientId, authority: this._authority })}` ); + private readonly _accountAccess = new ScopedAccountAccess(this._secretStorage, this._cloudName, this._clientId, this._authority); private readonly _config: Configuration = { auth: { clientId: this._clientId, authority: this._authority }, system: { loggerOptions: { correlationId: `${this._clientId}] [${this._authority}`, loggerCallback: (level, message, containsPii) => this._loggerOptions.loggerCallback(level, message, containsPii), - logLevel: LogLevel.Trace + logLevel: LogLevel.Info } }, + broker: { + nativeBrokerPlugin: new NativeBrokerPlugin() + }, cache: { cachePlugin: this._secretStorageCachePlugin } }; + private readonly _isBrokerAvailable = this._config.broker?.nativeBrokerPlugin?.isBrokerAvailable ?? false; /** * We keep track of the last time an account was removed so we can recreate the PCA if we detect that an account was removed. @@ -59,6 +66,7 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica constructor( private readonly _clientId: string, private readonly _authority: string, + private readonly _cloudName: string, private readonly _globalMemento: Memento, private readonly _secretStorage: SecretStorage, private readonly _logger: LogOutputChannel @@ -76,8 +84,11 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica get clientId(): string { return this._clientId; } get authority(): string { return this._authority; } - initialize(): Promise { - return this._update(); + async initialize(): Promise { + if (this._isBrokerAvailable) { + await this._accountAccess.initialize(); + } + await this._update(); } dispose(): void { @@ -88,7 +99,7 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] starting...`); const result = await this._sequencer.queue(() => this._pca.acquireTokenSilent(request)); this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] got result`); - if (result.account && !result.fromCache) { + if (result.account && !result.fromCache && this._verifyIfUsingBroker(result)) { this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] firing event due to change`); this._setupRefresh(result); this._onDidAccountsChangeEmitter.fire({ added: [], changed: [result.account], deleted: [] }); @@ -111,18 +122,48 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica ) ); this._setupRefresh(result); + if (this._isBrokerAvailable) { + await this._accountAccess.setAllowedAccess(result.account!, true); + } return result; } removeAccount(account: AccountInfo): Promise { this._globalMemento.update(`lastRemoval:${this._clientId}:${this._authority}`, new Date()); + if (this._isBrokerAvailable) { + return this._accountAccess.setAllowedAccess(account, false); + } return this._pca.getTokenCache().removeAccount(account); } private _registerOnSecretStorageChanged() { + if (this._isBrokerAvailable) { + return this._accountAccess.onDidAccountAccessChange(() => this._update()); + } return this._secretStorageCachePlugin.onDidChange(() => this._update()); } + private _lastSeen = new Map(); + private _verifyIfUsingBroker(result: AuthenticationResult): boolean { + // If we're not brokering, we don't need to verify the date + // the cache check will be sufficient + if (!result.fromNativeBroker) { + return true; + } + const key = result.account!.homeAccountId; + const lastSeen = this._lastSeen.get(key); + const lastTimeAuthed = result.account!.idTokenClaims!.iat!; + if (!lastSeen) { + this._lastSeen.set(key, lastTimeAuthed); + return true; + } + if (lastSeen === lastTimeAuthed) { + return false; + } + this._lastSeen.set(key, lastTimeAuthed); + return true; + } + private async _update() { const before = this._accounts; this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update before: ${before.length}`); @@ -134,7 +175,10 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica this._lastCreated = new Date(); } - const after = await this._pca.getAllAccounts(); + let after = await this._pca.getAllAccounts(); + if (this._isBrokerAvailable) { + after = after.filter(a => this._accountAccess.isAllowedAccess(a)); + } this._accounts = after; this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update after: ${after.length}`); @@ -167,8 +211,7 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica this._logger.debug(`[_setupRefresh] [${this._clientId}] [${this._authority}] [${scopes.join(' ')}] [${account.username}] timeToRefresh: ${timeToRefresh}`); this._refreshDelayer.trigger( key, - // This may need the redirectUri when we switch to the broker - () => this.acquireTokenSilent({ account, scopes, redirectUri: undefined, forceRefresh: true }), + () => this.acquireTokenSilent({ account, scopes, redirectUri: 'https://vscode.dev/redirect', forceRefresh: true }), timeToRefresh > 0 ? timeToRefresh : 0 ); } diff --git a/extensions/microsoft-authentication/src/node/publicClientCache.ts b/extensions/microsoft-authentication/src/node/publicClientCache.ts index fc6ce38e975..c6f2508e560 100644 --- a/extensions/microsoft-authentication/src/node/publicClientCache.ts +++ b/extensions/microsoft-authentication/src/node/publicClientCache.ts @@ -28,9 +28,9 @@ export class CachedPublicClientApplicationManager implements ICachedPublicClient private readonly _globalMemento: Memento, private readonly _secretStorage: SecretStorage, private readonly _logger: LogOutputChannel, - cloudName: string + private readonly _cloudName: string ) { - this._pcasSecretStorage = new PublicClientApplicationsSecretStorage(_secretStorage, cloudName); + this._pcasSecretStorage = new PublicClientApplicationsSecretStorage(_secretStorage, _cloudName); this._disposable = Disposable.from( this._pcasSecretStorage, this._registerSecretStorageHandler(), @@ -111,7 +111,7 @@ export class CachedPublicClientApplicationManager implements ICachedPublicClient } private async _doCreatePublicClientApplication(clientId: string, authority: string, pcasKey: string) { - const pca = new CachedPublicClientApplication(clientId, authority, this._globalMemento, this._secretStorage, this._logger); + const pca = new CachedPublicClientApplication(clientId, authority, this._cloudName, this._globalMemento, this._secretStorage, this._logger); this._pcas.set(pcasKey, pca); const disposable = Disposable.from( pca, @@ -160,11 +160,7 @@ export class CachedPublicClientApplicationManager implements ICachedPublicClient // Handle the deleted ones for (const pcaKey of this._pcas.keys()) { if (!pcaKeysFromStorage.delete(pcaKey)) { - // This PCA has been removed in another window - this._pcaDisposables.get(pcaKey)?.dispose(); - this._pcaDisposables.delete(pcaKey); - this._pcas.delete(pcaKey); - this._logger.debug(`[_handleSecretStorageChange] Disposed PCA that was deleted in another window: ${pcaKey}`); + this._logger.debug(`[_handleSecretStorageChange] PCA was deleted in another window: ${pcaKey}`); } } diff --git a/extensions/microsoft-authentication/tsconfig.json b/extensions/microsoft-authentication/tsconfig.json index 4b9d06d1847..b40c2eb8716 100644 --- a/extensions/microsoft-authentication/tsconfig.json +++ b/extensions/microsoft-authentication/tsconfig.json @@ -22,6 +22,7 @@ "include": [ "src/**/*", "../../src/vscode-dts/vscode.d.ts", - "../../src/vscode-dts/vscode.proposed.idToken.d.ts" + "../../src/vscode-dts/vscode.proposed.idToken.d.ts", + "../../src/vscode-dts/vscode.proposed.nativeWindowHandle.d.ts" ] }