diff --git a/change/@azure-msal-browser-60c15904-56e7-4805-bf26-8460c6897182.json b/change/@azure-msal-browser-60c15904-56e7-4805-bf26-8460c6897182.json new file mode 100644 index 0000000000..0b87d87569 --- /dev/null +++ b/change/@azure-msal-browser-60c15904-56e7-4805-bf26-8460c6897182.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Track MSAL node SKU for broker flows #7213", + "packageName": "@azure/msal-browser", + "email": "kshabelko@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-msal-common-b5fa97f3-1dc4-4676-bfec-0ab93163bd3e.json b/change/@azure-msal-common-b5fa97f3-1dc4-4676-bfec-0ab93163bd3e.json new file mode 100644 index 0000000000..dccfb3cdc4 --- /dev/null +++ b/change/@azure-msal-common-b5fa97f3-1dc4-4676-bfec-0ab93163bd3e.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Track MSAL node SKU for broker flows #7213", + "packageName": "@azure/msal-common", + "email": "kshabelko@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-msal-node-1ce00b57-221a-49f7-88eb-9c8b62b01389.json b/change/@azure-msal-node-1ce00b57-221a-49f7-88eb-9c8b62b01389.json new file mode 100644 index 0000000000..be33bc2717 --- /dev/null +++ b/change/@azure-msal-node-1ce00b57-221a-49f7-88eb-9c8b62b01389.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Track MSAL node SKU for broker flows #7213", + "packageName": "@azure/msal-node", + "email": "kshabelko@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-msal-node-extensions-305e7120-5371-4eb0-9293-cc5f040391f1.json b/change/@azure-msal-node-extensions-305e7120-5371-4eb0-9293-cc5f040391f1.json new file mode 100644 index 0000000000..c0632200bd --- /dev/null +++ b/change/@azure-msal-node-extensions-305e7120-5371-4eb0-9293-cc5f040391f1.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Track MSAL node SKU for broker flows #7213", + "packageName": "@azure/msal-node-extensions", + "email": "kshabelko@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/extensions/msal-node-extensions/src/broker/NativeBrokerPlugin.ts b/extensions/msal-node-extensions/src/broker/NativeBrokerPlugin.ts index 69a22c64f6..d12f3192df 100644 --- a/extensions/msal-node-extensions/src/broker/NativeBrokerPlugin.ts +++ b/extensions/msal-node-extensions/src/broker/NativeBrokerPlugin.ts @@ -4,6 +4,7 @@ */ import { + AADServerParamKeys, AccountInfo, AuthenticationResult, AuthenticationScheme, @@ -21,6 +22,7 @@ import { NativeSignOutRequest, PromptValue, ServerError, + ServerTelemetryManager, } from "@azure/msal-common"; import { msalNodeRuntime, @@ -487,6 +489,23 @@ export class NativeBrokerPlugin implements INativeBrokerPlugin { } ); } + + const skus = + request.extraParameters && + request.extraParameters[AADServerParamKeys.X_CLIENT_EXTRA_SKU] + ?.length + ? request.extraParameters[ + AADServerParamKeys.X_CLIENT_EXTRA_SKU + ] + : ""; + authParams.SetAdditionalParameter( + AADServerParamKeys.X_CLIENT_EXTRA_SKU, + ServerTelemetryManager.makeExtraSkuString({ + skus, + extensionName: "msal.node.ext", + extensionVersion: version, + }) + ); } catch (e) { const wrappedError = this.wrapError(e); if (wrappedError) { diff --git a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts index c6ae34a7b3..1d363d9e68 100644 --- a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts @@ -81,34 +81,6 @@ const BrokerServerParamKeys = { BROKER_REDIRECT_URI: "brk_redirect_uri", }; -/** - * Provides MSAL and browser extension SKUs - * @param messageHandler {NativeMessageHandler} - */ -function getSKUs(messageHandler: NativeMessageHandler): string { - const groupSeparator = ","; - const valueSeparator = "|"; - const skus = Array.from({ length: 4 }, () => valueSeparator); - // Report MSAL SKU - skus[0] = [BrowserConstants.MSAL_SKU, version].join(valueSeparator); - - // Report extension SKU - const extensionName = - messageHandler.getExtensionId() === - NativeConstants.PREFERRED_EXTENSION_ID - ? "chrome" - : messageHandler.getExtensionId()?.length - ? "unknown" - : undefined; - - if (extensionName) { - skus[2] = [extensionName, messageHandler.getExtensionVersion()].join( - valueSeparator - ); - } - return skus.join(groupSeparator); -} - export class NativeInteractionClient extends BaseInteractionClient { protected apiId: ApiId; protected accountId: string; @@ -161,7 +133,20 @@ export class NativeInteractionClient extends BaseInteractionClient { this.serverTelemetryManager = this.initializeServerTelemetryManager( this.apiId ); - this.skus = getSKUs(this.nativeMessageHandler); + + const extensionName = + this.nativeMessageHandler.getExtensionId() === + NativeConstants.PREFERRED_EXTENSION_ID + ? "chrome" + : this.nativeMessageHandler.getExtensionId()?.length + ? "unknown" + : undefined; + this.skus = ServerTelemetryManager.makeExtraSkuString({ + libraryName: BrowserConstants.MSAL_SKU, + libraryVersion: version, + extensionName: extensionName, + extensionVersion: this.nativeMessageHandler.getExtensionVersion(), + }); } /** diff --git a/lib/msal-common/apiReview/msal-common.api.md b/lib/msal-common/apiReview/msal-common.api.md index d27e4de2da..11b6d309bb 100644 --- a/lib/msal-common/apiReview/msal-common.api.md +++ b/lib/msal-common/apiReview/msal-common.api.md @@ -3664,6 +3664,10 @@ export class ServerTelemetryManager { getNativeBrokerErrorCode(): string | undefined; getRegionDiscoveryFields(): string; incrementCacheHits(): number; + // Warning: (ae-forgotten-export) The symbol "SkuParams" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static makeExtraSkuString(params: SkuParams): string; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen static maxErrorsToSend(serverTelemetryEntity: ServerTelemetryEntity): number; setCacheOutcome(cacheOutcome: CacheOutcome): void; diff --git a/lib/msal-common/src/telemetry/server/ServerTelemetryManager.ts b/lib/msal-common/src/telemetry/server/ServerTelemetryManager.ts index 8ad791ed04..ae480c9948 100644 --- a/lib/msal-common/src/telemetry/server/ServerTelemetryManager.ts +++ b/lib/msal-common/src/telemetry/server/ServerTelemetryManager.ts @@ -17,6 +17,69 @@ import { ServerTelemetryRequest } from "./ServerTelemetryRequest"; import { ServerTelemetryEntity } from "../../cache/entities/ServerTelemetryEntity"; import { RegionDiscoveryMetadata } from "../../authority/RegionDiscoveryMetadata"; +const skuGroupSeparator = ","; +const skuValueSeparator = "|"; + +type SkuParams = { + libraryName?: string; + libraryVersion?: string; + extensionName?: string; + extensionVersion?: string; + skus?: string; +}; + +function makeExtraSkuString(params: SkuParams): string { + const { + skus, + libraryName, + libraryVersion, + extensionName, + extensionVersion, + } = params; + const skuMap: Map = new Map([ + [0, [libraryName, libraryVersion]], + [2, [extensionName, extensionVersion]], + ]); + let skuArr: string[] = []; + + if (skus?.length) { + skuArr = skus.split(skuGroupSeparator); + + // Ignore invalid input sku param + if (skuArr.length < 4) { + return skus; + } + } else { + skuArr = Array.from({ length: 4 }, () => skuValueSeparator); + } + + skuMap.forEach((value, key) => { + if (value.length === 2 && value[0]?.length && value[1]?.length) { + setSku({ + skuArr, + index: key, + skuName: value[0], + skuVersion: value[1], + }); + } + }); + + return skuArr.join(skuGroupSeparator); +} + +function setSku(params: { + skuArr: string[]; + index: number; + skuName: string; + skuVersion: string; +}): void { + const { skuArr, index, skuName, skuVersion } = params; + if (index >= skuArr.length) { + return; + } + skuArr[index] = [skuName, skuVersion].join(skuValueSeparator); +} + /** @internal */ export class ServerTelemetryManager { private cacheManager: CacheManager; @@ -304,4 +367,8 @@ export class ServerTelemetryManager { lastRequests ); } + + static makeExtraSkuString(params: SkuParams): string { + return makeExtraSkuString(params); + } } diff --git a/lib/msal-common/test/telemetry/ServerTelemetryManager.spec.ts b/lib/msal-common/test/telemetry/ServerTelemetryManager.spec.ts index d0d6b5694b..ff4385369c 100644 --- a/lib/msal-common/test/telemetry/ServerTelemetryManager.spec.ts +++ b/lib/msal-common/test/telemetry/ServerTelemetryManager.spec.ts @@ -402,4 +402,56 @@ describe("ServerTelemetryManager.ts", () => { ) as ServerTelemetryEntity; expect(cacheValue.cacheHits).toBe(2); }); + + describe("makeExtraSkuString", () => { + it("Creates empty string from scratch", () => { + const skus = ServerTelemetryManager.makeExtraSkuString({}); + expect(skus).toEqual("|,|,|,|"); + }); + + it("Does not modify input string", () => { + const skus = ServerTelemetryManager.makeExtraSkuString({ + skus: "test_sku|1.0.0,|,|,|", + }); + expect(skus).toEqual("test_sku|1.0.0,|,|,|"); + }); + + it("Returns invalid input", () => { + const skus = ServerTelemetryManager.makeExtraSkuString({ + skus: "test_sku|1.0.0,|,|", + libraryName: "test_lib_name", + libraryVersion: "1.2.3", + }); + expect(skus).toEqual("test_sku|1.0.0,|,|"); + }); + + it("Adds library and extension info", () => { + const skus = ServerTelemetryManager.makeExtraSkuString({ + skus: "test_sku|1.0.0,|,test_ext_sku|2.0.0,|", + libraryName: "test_lib_name", + libraryVersion: "1.2.3", + extensionName: "test_ext_name", + extensionVersion: "5.6.7", + }); + expect(skus).toEqual("test_lib_name|1.2.3,|,test_ext_name|5.6.7,|"); + }); + + it("Updates input string with library info", () => { + const skus = ServerTelemetryManager.makeExtraSkuString({ + skus: "test_sku|1.0.0,|,test_ext_sku|2.0.0,|", + libraryName: "test_lib_name", + libraryVersion: "1.2.3", + }); + expect(skus).toEqual("test_lib_name|1.2.3,|,test_ext_sku|2.0.0,|"); + }); + + it("Updates input string with extension info", () => { + const skus = ServerTelemetryManager.makeExtraSkuString({ + skus: "test_sku|1.0.0,|,test_ext_sku|2.0.0,|", + extensionName: "test_ext_name", + extensionVersion: "5.6.7", + }); + expect(skus).toEqual("test_sku|1.0.0,|,test_ext_name|5.6.7,|"); + }); + }); }); diff --git a/lib/msal-node/apiReview/msal-node.api.md b/lib/msal-node/apiReview/msal-node.api.md index 3ad5d455df..af7a0d0801 100644 --- a/lib/msal-node/apiReview/msal-node.api.md +++ b/lib/msal-node/apiReview/msal-node.api.md @@ -758,7 +758,7 @@ export const version = "2.11.1"; // src/client/OnBehalfOfClient.ts:249:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/client/OnBehalfOfClient.ts:250:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/client/OnBehalfOfClient.ts:310:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/client/PublicClientApplication.ts:298:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/client/PublicClientApplication.ts:310:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/client/UsernamePasswordClient.ts:74:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/client/UsernamePasswordClient.ts:75:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/client/UsernamePasswordClient.ts:114:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen diff --git a/lib/msal-node/src/client/PublicClientApplication.ts b/lib/msal-node/src/client/PublicClientApplication.ts index f17078173d..6817c6ca28 100644 --- a/lib/msal-node/src/client/PublicClientApplication.ts +++ b/lib/msal-node/src/client/PublicClientApplication.ts @@ -22,6 +22,8 @@ import { AccountInfo, INativeBrokerPlugin, ServerAuthorizationCodeResponse, + AADServerParamKeys, + ServerTelemetryManager, } from "@azure/msal-common"; import { Configuration } from "../config/Configuration.js"; import { ClientApplication } from "./ClientApplication.js"; @@ -36,6 +38,7 @@ import { SilentFlowRequest } from "../request/SilentFlowRequest.js"; import { SignOutRequest } from "../request/SignOutRequest.js"; import { ILoopbackClient } from "../network/ILoopbackClient.js"; import { DeviceCodeClient } from "./DeviceCodeClient.js"; +import { version } from "../packageMetadata"; /** * This class is to be used to acquire tokens for public client applications (desktop, mobile). Public client applications @@ -47,6 +50,7 @@ export class PublicClientApplication implements IPublicClientApplication { private nativeBrokerPlugin?: INativeBrokerPlugin; + private readonly skus: string; /** * Important attributes in the Configuration object for auth are: * - clientID: the application ID of your application. You can obtain one by registering your application with our Application registration portal. @@ -78,6 +82,10 @@ export class PublicClientApplication ); } } + this.skus = ServerTelemetryManager.makeExtraSkuString({ + libraryName: Constants.MSAL_SKU, + libraryVersion: version, + }); } /** @@ -156,6 +164,7 @@ export class PublicClientApplication extraParameters: { ...remainingProperties.extraQueryParameters, ...remainingProperties.tokenQueryParameters, + [AADServerParamKeys.X_CLIENT_EXTRA_SKU]: this.skus, }, accountId: remainingProperties.account?.nativeAccountId, }; @@ -247,7 +256,10 @@ export class PublicClientApplication redirectUri: `${Constants.HTTP_PROTOCOL}${Constants.LOCALHOST}`, authority: request.authority || this.config.auth.authority, correlationId: correlationId, - extraParameters: request.tokenQueryParameters, + extraParameters: { + ...request.tokenQueryParameters, + [AADServerParamKeys.X_CLIENT_EXTRA_SKU]: this.skus, + }, accountId: request.account.nativeAccountId, forceRefresh: request.forceRefresh || false, }; diff --git a/lib/msal-node/test/client/PublicClientApplication.spec.ts b/lib/msal-node/test/client/PublicClientApplication.spec.ts index b2c7e426b8..a923e1e029 100644 --- a/lib/msal-node/test/client/PublicClientApplication.spec.ts +++ b/lib/msal-node/test/client/PublicClientApplication.spec.ts @@ -23,6 +23,7 @@ import { CacheHelpers, AuthorityFactory, ProtocolMode, + AADServerParamKeys, } from "@azure/msal-common"; import { Configuration, @@ -53,6 +54,7 @@ import { ClientAuthErrorCodes } from "@azure/msal-common"; import { TEST_CONFIG } from "../test_kit/StringConstants"; import { HttpClient } from "../../src/network/HttpClient"; import { MockStorageClass } from "./ClientTestUtils"; +import { Constants } from "../../src/utils/Constants"; const msalCommon: MSALCommonModule = jest.requireActual("@azure/msal-common"); @@ -256,6 +258,33 @@ describe("PublicClientApplication", () => { ); }); + test("acquireTokenSilent sends extra telemetry to NativeBrokerPlugin", async () => { + const authApp = new PublicClientApplication({ + ...appConfig, + broker: { + nativeBrokerPlugin: new MockNativeBrokerPlugin(), + }, + }); + + const request: SilentFlowRequest = { + account: mockNativeAccountInfo, + scopes: TEST_CONSTANTS.DEFAULT_GRAPH_SCOPE, + }; + const brokerSpy: jest.SpyInstance = + jest.spyOn( + MockNativeBrokerPlugin.prototype, + "acquireTokenSilent" + ); + await authApp.acquireTokenSilent(request); + const nativeRequest = brokerSpy.mock.calls[0][0]; + expect(nativeRequest).toHaveProperty("extraParameters"); + // @ts-ignore + expect(nativeRequest.extraParameters).toHaveProperty( + AADServerParamKeys.X_CLIENT_EXTRA_SKU, + `${Constants.MSAL_SKU}|${version},|,|,|` + ); + }); + test("acquireTokenSilent - calls into NativeBrokerPlugin and throws", (done) => { const authApp = new PublicClientApplication({ ...appConfig,