Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0b31112
WIP
MicroFish91 Nov 10, 2025
71f4f14
Merge branch 'mwf/v4' of https://github.com/microsoft/vscode-azureres…
MicroFish91 Nov 11, 2025
f188eca
Add auth tests and supporting logic
MicroFish91 Nov 11, 2025
e71785c
Update with better comments
MicroFish91 Nov 12, 2025
3f4cb3a
Revert launch change
MicroFish91 Nov 12, 2025
34d2f1a
Use extensionId var
MicroFish91 Nov 12, 2025
c34d952
Add note
MicroFish91 Nov 12, 2025
5b0d57a
Merge with parent branch
MicroFish91 Nov 12, 2025
4c49c09
Merge branch 'mwf/v4' of https://github.com/microsoft/vscode-azureres…
MicroFish91 Nov 13, 2025
0b1d03e
Update mock credential manager
MicroFish91 Nov 13, 2025
570ecf4
Merge branch 'mwf/v4' of https://github.com/microsoft/vscode-azureres…
MicroFish91 Nov 13, 2025
b446718
Merge branch 'mwf/v4' of https://github.com/microsoft/vscode-azureres…
MicroFish91 Nov 13, 2025
0a7b447
Merge branch 'mwf/v4' of https://github.com/microsoft/vscode-azureres…
MicroFish91 Nov 13, 2025
3cf4fa3
Merge branch 'mwf/v4' of https://github.com/microsoft/vscode-azureres…
MicroFish91 Nov 13, 2025
69051c0
Remove part of a comment
MicroFish91 Nov 14, 2025
e433035
Merge with main + some new changes
MicroFish91 Nov 19, 2025
1718244
Fix formatting
MicroFish91 Nov 19, 2025
06e2b99
Merge branch 'mwf/v4' of https://github.com/microsoft/vscode-azureres…
MicroFish91 Nov 19, 2025
ef7ad23
Match to equal
MicroFish91 Nov 19, 2025
97b1239
Merge branch 'mwf/v4' of https://github.com/microsoft/vscode-azureres…
MicroFish91 Nov 19, 2025
eda6ce6
Escape the symbol
MicroFish91 Nov 19, 2025
b5eb5f9
Merge branch 'mwf/v4-client-tools' of https://github.com/microsoft/vs…
MicroFish91 Nov 20, 2025
81ce320
Merge with new eng package changes
MicroFish91 Nov 20, 2025
5c11b97
Inject more extension vars
MicroFish91 Nov 20, 2025
ff9d912
Merge branch 'mwf/v4-client-tools' of https://github.com/microsoft/vs…
MicroFish91 Nov 20, 2025
1e9269d
Merge with main + add getUI
MicroFish91 Nov 21, 2025
8edafe9
Add type
MicroFish91 Nov 21, 2025
0d63c1c
Add an extra test
MicroFish91 Nov 21, 2025
56385e1
Remove env var
MicroFish91 Dec 9, 2025
1039782
Upgrade utils package
MicroFish91 Dec 11, 2025
0aa6f1c
Remove ui extension vars; leverage testGlobalSetup
MicroFish91 Dec 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion extension.bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ export { LocationListStep } from '@microsoft/vscode-azext-azureutils';
export * from '@microsoft/vscode-azext-utils';
export * from './api/src/AzExtResourceType';
// export * from './api/src';
export * from './api/src/extensionApi';
export * from './api/src/auth/credentialManager/AzExtCredentialManager';
export * from './api/src/auth/credentialManager/AzExtUUIDCredentialManager';
export * from './api/src/resources/azure';
export * from './api/src/resources/base';
export * from './api/src/resources/workspace';
export { apiUtils, AzureExtensionApi } from './api/src/utils/apiUtils';
export * from './api/src/utils/getApi';
export * from './api/src/utils/wrapper';
export { convertV1TreeItemId } from './src/api/compatibility/CompatibleAzExtTreeDataProvider';
Expand All @@ -34,6 +36,8 @@ export { createResourceGroup } from './src/commands/createResourceGroup';
export * from './src/commands/deleteResourceGroup/v2/deleteResourceGroupV2';
export { activate, deactivate } from './src/extension';
// Export for testing only - not part of public API
export * from './api/src/extensionApi';
export * from './src/api/auth/createAzureResourcesAuthApiFactory';
export { AuthAccountStateManager, getAuthAccountStateManager } from './src/exportAuthRecord';
export * from './src/extensionVariables';
export * from './src/hostapi.v2.internal';
Expand Down
3 changes: 2 additions & 1 deletion src/api/auth/authApiInternal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export async function createAzureResourcesApiSessionInternal(context: IActionCon

try {
const clientApi = await apiUtils.getAzureExtensionApi(ext.context, clientExtensionId, clientExtensionVersion);
await clientApi.receiveAzureResourcesApiSession?.(await credentialManager.createCredential(clientExtensionId), clientExtensionCredential);
const azureResourcesCredential: string = await credentialManager.createCredential(clientExtensionId);
await clientApi.receiveAzureResourcesApiSession?.(azureResourcesCredential, clientExtensionCredential);
} catch (err) {
const failed: string = localize('createResourcesApiSession.failed', 'Failed to create Azure Resources API session for extension "{0}".', clientExtensionId);
ext.outputChannel.error(failed);
Expand Down
5 changes: 1 addition & 4 deletions src/api/auth/createAzureResourcesAuthApiFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,12 @@

import { apiUtils, AzureExtensionApiFactory, callWithTelemetryAndErrorHandling, GetApiOptions, IActionContext } from '@microsoft/vscode-azext-utils';
import { AzExtCredentialManager } from '../../../api/src/auth/credentialManager/AzExtCredentialManager';
import { AzExtUUIDCredentialManager } from '../../../api/src/auth/credentialManager/AzExtUUIDCredentialManager';
import { AzureResourcesAuthApiInternal } from '../../hostapi.v4.internal';
import { createAzureResourcesApiSessionInternal, getApiVerifyError, verifyAzureResourcesApiSessionInternal } from './authApiInternal';

const v4: string = '4.0.0';

export function createAzureResourcesAuthApiFactory(coreApiProvider: apiUtils.AzureExtensionApiProvider): AzureExtensionApiFactory<AzureResourcesAuthApiInternal> {
const credentialManager: AzExtCredentialManager = new AzExtUUIDCredentialManager();

export function createAzureResourcesAuthApiFactory(credentialManager: AzExtCredentialManager, coreApiProvider: apiUtils.AzureExtensionApiProvider): AzureExtensionApiFactory<AzureResourcesAuthApiInternal> {
return {
apiVersion: v4,
createApi: (options?: GetApiOptions) => {
Expand Down
6 changes: 5 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { AzureSubscription } from 'api/src';
import { GetApiOptions, apiUtils } from 'api/src/utils/apiUtils';
import * as vscode from 'vscode';
import { AzExtResourceType } from '../api/src/AzExtResourceType';
import { AzExtCredentialManager } from '../api/src/auth/credentialManager/AzExtCredentialManager';
import { AzExtUUIDCredentialManager } from '../api/src/auth/credentialManager/AzExtUUIDCredentialManager';
import { DefaultAzureResourceProvider } from './api/DefaultAzureResourceProvider';
import { ResourceGroupsExtensionManager } from './api/ResourceGroupsExtensionManager';
import { ActivityLogResourceProviderManager, AzureResourceProviderManager, TenantResourceProviderManager, WorkspaceResourceProviderManager } from './api/ResourceProviderManagers';
Expand Down Expand Up @@ -264,6 +266,8 @@ export async function activate(context: vscode.ExtensionContext, perfStats: { lo
v3ApiFactory,
]);

const credentialManager: AzExtCredentialManager = new AzExtUUIDCredentialManager();

return createApiProvider(
[
// Todo: Remove once extension clients finish migrating
Expand All @@ -272,7 +276,7 @@ export async function activate(context: vscode.ExtensionContext, perfStats: { lo
v3ApiFactory,

// This will eventually be the only part of the API exposed publically
createAzureResourcesAuthApiFactory(coreApiProvider),
createAzureResourcesAuthApiFactory(credentialManager, coreApiProvider),
]
);
}
Expand Down
38 changes: 38 additions & 0 deletions test/api/auth/MockUUIDCredentialManager.ts
Original file line number Diff line number Diff line change
@@ -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 { AzExtCredentialManager, maskValue } from "../../../extension.bundle";

/**
* A mock credential manager with the same implementation as `AzExtUUIDCredentialManager`,
* but with a public getter to inspect the UUIDs during test.
Copy link
Contributor Author

@MicroFish91 MicroFish91 Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A less copy and paste mock would be possible if I extended from the AzExtUUIDCredentialManager directly, but the map would need to be protected not private for me to be able to access it.

*/
export class MockUUIDCredentialManager implements AzExtCredentialManager {
#uuidMap: Map<string, string> = new Map();

get uuidMap() {
return this.#uuidMap;
}

createCredential(extensionId: string): string {
const uuid: string = crypto.randomUUID();
this.#uuidMap.set(extensionId, uuid);
return uuid;
}

verifyCredential(credential: string, extensionId: string): boolean {
if (!credential || !extensionId) {
return false;
}
return credential === this.#uuidMap.get(extensionId);
}

maskCredentials(data: string): string {
for (const uuid of this.#uuidMap.values()) {
data = maskValue(data, uuid);
}
return data;
}
}
25 changes: 25 additions & 0 deletions test/api/auth/mockApiProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { apiUtils, AzureExtensionApi, AzureExtensionApiFactory, createApiProvider, GetApiOptions } from "../../../extension.bundle";

/**
* Creates a mock API provider with API factories matching the versions provided.
* Only the values required by the interface will be implemented.
*/
export function createMockApiProvider(versions: string[]): apiUtils.AzureExtensionApiProvider {
const apiFactories: AzureExtensionApiFactory<AzureExtensionApi>[] = versions.map(version => {
return {
apiVersion: version,
createApi: (_options?: GetApiOptions) => {
return {
apiVersion: version,
};
},
};
});

return createApiProvider(apiFactories);
}
109 changes: 109 additions & 0 deletions test/api/auth/v4.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as assert from "assert";
import { apiUtils, AzExtCredentialManager, AzureResourcesExtensionAuthApi, createAzureResourcesAuthApiFactory, nonNullValue, parseError } from "../../../extension.bundle";
import { assertThrowsAsync } from "../../wrapFunctionsInTelemetry.test";
import { MockUUIDCredentialManager } from "./MockUUIDCredentialManager";
import { createMockApiProvider } from "./mockApiProvider";

const extensionId: string = 'ms-azuretools.vscode-azureresourcegroups';
const extensionVersion: string = '^4.0.0';
const coreApiVersions: string[] = ['0.0.1', '2.0.0', '3.0.0'];

suite('v4 API auth tests', async () => {
test('v4 API should be defined', async () => {
const apiProvider = await apiUtils.getExtensionExports<apiUtils.AzureExtensionApiProvider>(extensionId);
assert.ok(apiProvider, 'API provider is undefined');

const v4Api = apiProvider.getApi(extensionVersion, { extensionId: 'ms-azuretools.vscode-azureresourcegroups-tests' });
assert.ok(v4Api);
});

// NOTE: `createAzureResourcesApiSession` is not normally intended to be called directly by Azure Resources itself; however, I've found that it
// kind of still works for testing. It will basically run everything exactly the same except at the end - the exported API for Azure Resources will be missing
// the receiver method so the credential has no way to be passed back to the extension through its API.
// Since we inject and hold a copy of the credential manager during tests, we can simply grab the generated credential from the manager.
// Client side handshake testing should be done separately to ensure that the receiver method is being called and passed the correct credential.

test('createAzureResourcesApiSession should provide a credential but not return it directly', async () => {
const credentialManager = new MockUUIDCredentialManager();
const authApi: AzureResourcesExtensionAuthApi = createAuthApi(credentialManager, coreApiVersions);

const apiSession = await authApi.createAzureResourcesApiSession(extensionId, extensionVersion, crypto.randomUUID());
assert.equal(apiSession, undefined);
assert.ok(credentialManager.uuidMap.get(extensionId));
});

test('createAzureResourcesApiSession should throw if an unallowed extension id is provided', async () => {
const credentialManager = new MockUUIDCredentialManager();
const authApi: AzureResourcesExtensionAuthApi = createAuthApi(credentialManager, coreApiVersions);
assertThrowsAsync(async () => await authApi.createAzureResourcesApiSession('extension1', extensionVersion, crypto.randomUUID()))
});

test('createAzureResourcesApiSession should not spill sensitive extension credentials in errors', async () => {
const credentialManager = new MockUUIDCredentialManager();
credentialManager.createCredential('extension1');
credentialManager.createCredential = () => {
throw new Error(credentialManager.uuidMap.get('extension1'));
}

const authApi: AzureResourcesExtensionAuthApi = createAuthApi(credentialManager, coreApiVersions);

try {
await authApi.createAzureResourcesApiSession(extensionId, extensionVersion, crypto.randomUUID());
assert.fail('We expect the credential manager to throw in this test.');
} catch (err) {
const perr = parseError(err);
assert.doesNotMatch(perr.message, new RegExp(nonNullValue(credentialManager.uuidMap.get('extension1')), 'i'));
}
});

test('getAzureResourcesApis should return matching APIs if provided a valid credential', async () => {
const credentialManager = new MockUUIDCredentialManager();

const authApi: AzureResourcesExtensionAuthApi = createAuthApi(credentialManager, coreApiVersions);
await authApi.createAzureResourcesApiSession(extensionId, extensionVersion, crypto.randomUUID());

const resourcesApis = await authApi.getAzureResourcesApis(extensionId, nonNullValue(credentialManager.uuidMap.get(extensionId)), ['0.0.1', '^2.0.0']);
assert.match(resourcesApis[0]?.apiVersion ?? '', /^0.0.1$/);
assert.match(resourcesApis[1]?.apiVersion ?? '', /^2./);
});

test('getAzureResourcesApis should throw if provided an invalid credential', async () => {
const credentialManager = new MockUUIDCredentialManager();
const coreApiVersions: string[] = ['0.0.1', '2.0.0', '3.0.0'];
const authApi: AzureResourcesExtensionAuthApi = createAuthApi(credentialManager, coreApiVersions);
assertThrowsAsync(async () => await authApi.getAzureResourcesApis(extensionId, crypto.randomUUID(), ['^2.0.0']));
});

test('getAzureResourcesApis should not spill sensitive extension credentials in errors', async () => {
const credentialManager = new MockUUIDCredentialManager();
const authApi: AzureResourcesExtensionAuthApi = createAuthApi(credentialManager, coreApiVersions);

credentialManager.createCredential('extension1');
credentialManager.createCredential('extension2');
credentialManager.createCredential('extension3');

try {
await authApi.getAzureResourcesApis(extensionId, crypto.randomUUID(), ['^2.0.0']);
assert.fail('Should throw if requesting Azure Resources APIs without a valid credential.');
} catch (err) {
const perr = parseError(err);
for (const credential of credentialManager.uuidMap.values()) {
assert.doesNotMatch(perr.message, new RegExp(credential, 'i'));
}
}
});
});

/**
* Use to quickly bootstrap a testable auth API with core API factories matching the provided versions.
*/
function createAuthApi(credentialManager: AzExtCredentialManager, coreApiVersions: string[]): AzureResourcesExtensionAuthApi {
const coreApiProvider = createMockApiProvider(coreApiVersions);
const authApiProvider = createAzureResourcesAuthApiFactory(credentialManager, coreApiProvider);
return authApiProvider.createApi({ extensionId: 'ms-azuretools.vscode-azureresourcegroups-tests' });
}