Adopt the MSAL broker to talk to the OS for Microsoft auth (#233739)
This adopts the `NativeBrokerPlugin` provided by `@azure/msal-node-extensions` to provide the ability to use auth state from the OS, and show native auth dialogs instead of going to the browser. This has several pieces: * The adoption of the broker in the microsoft-authentication extension: * Adding `NativeBrokerPlugin` to our PCAs * Using the proposed handle API to pass the native window handle down to MSAL calls (btw, this API will change in a follow up PR) * Adopting an AccountAccess layer to handle: * giving the user control of which accounts VS Code uses * an eventing layer so that auth state can be updated across multiple windows * Getting the extension to build properly and only build what it really needs. This required several package.json/webpack hacks: * Use a fake keytar since we don't use the feature in `@azure/msal-node-extensions` that uses keytar * Use a fake dpapi layer since we don't use the feature in `@azure/msal-node-extensions` that uses it * Ensure the msal runtime `.node` and `.dll` files are included in the bundle * Get the VS Code build to allow a native node module in an extension: by having a list of native extensions that will be built in the "ci" part of the build - in other words when VS Code is building on the target platform There are a couple of followups: * Refactor the `handle` API to handle (heh) Auxiliary Windows https://github.com/microsoft/vscode/issues/233106 * Separate the call to `acquireTokenSilent` and `acquireTokenInteractive` and all the usage of this native node module into a separate process or maybe in Core... we'll see. Something to experiment with after we have something working. NEEDS FOLLOW UP ISSUE Fixes https://github.com/microsoft/vscode/issues/229431
This commit is contained in:
Родитель
681164aaaa
Коммит
305134296c
|
@ -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,
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/*';
|
||||
|
|
|
@ -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')),
|
||||
));
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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 = (
|
||||
(<string[]>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))
|
||||
|
|
|
@ -12,3 +12,4 @@ vsc-extension-quickstart.md
|
|||
**/tslint.json
|
||||
**/*.map
|
||||
**/*.ts
|
||||
packageMocks/
|
||||
|
|
|
@ -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')
|
||||
)
|
||||
]
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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 };
|
|
@ -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();
|
|
@ -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"
|
||||
}
|
|
@ -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<void>;
|
||||
isAllowedAccess(account: AccountInfo): boolean;
|
||||
setAllowedAccess(account: AccountInfo, allowed: boolean): void;
|
||||
}
|
||||
|
||||
export class ScopedAccountAccess implements IAccountAccess {
|
||||
private readonly _onDidAccountAccessChangeEmitter = new EventEmitter<void>();
|
||||
readonly onDidAccountAccessChange = this._onDidAccountAccessChangeEmitter.event;
|
||||
|
||||
private readonly _accountAccessSecretStorage: AccountAccessSecretStorage;
|
||||
|
||||
private value = new Array<string>();
|
||||
|
||||
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<void> {
|
||||
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<void>;
|
||||
readonly onDidChange: Event<void> = 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<string[] | undefined> {
|
||||
const value = await this._secretStorage.get(this._key);
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
return JSON.parse(value);
|
||||
}
|
||||
|
||||
store(value: string[]): Thenable<void> {
|
||||
return this._secretStorage.store(this._key, JSON.stringify(value));
|
||||
}
|
||||
|
||||
delete(): Thenable<void> {
|
||||
return this._secretStorage.delete(this._key);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposable.dispose();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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<void> {
|
||||
return this._update();
|
||||
async initialize(): Promise<void> {
|
||||
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<void> {
|
||||
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<string, number>();
|
||||
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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче