From 1321c56961a6f04668d5026013ea58e8963eab83 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 15 Jan 2025 17:13:56 -0500 Subject: [PATCH 01/11] Added Machine Learning as a Managed Identity Source --- lib/msal-node/docs/managed-identity.md | 1 + .../src/client/ManagedIdentityClient.ts | 11 ++ .../ManagedIdentitySources/MachineLearning.ts | 130 ++++++++++++++ .../ManagedIdentitySources/ServiceFabric.ts | 7 +- lib/msal-node/src/utils/Constants.ts | 5 +- .../MachineLearning.spec.ts | 159 ++++++++++++++++++ .../test/test_kit/StringConstants.ts | 5 + 7 files changed, 314 insertions(+), 4 deletions(-) create mode 100644 lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts create mode 100644 lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts diff --git a/lib/msal-node/docs/managed-identity.md b/lib/msal-node/docs/managed-identity.md index 86c0d8e8e6..b8a578b144 100644 --- a/lib/msal-node/docs/managed-identity.md +++ b/lib/msal-node/docs/managed-identity.md @@ -11,6 +11,7 @@ A common challenge for developers is the management of secrets, credentials, cer - [Azure Arc](https://learn.microsoft.com/en-us/azure/azure-arc/overview) - [Azure Cloud Shell](https://learn.microsoft.com/en-us/azure/cloud-shell/overview) - [Azure Service Fabric](https://learn.microsoft.com/en-us/azure/service-fabric/service-fabric-overview) +- [Azure Machine Learning](https://azure.microsoft.com/en-us/products/machine-learning) For a complete list, refer to [Azure services that can use managed identities to access other services](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/managed-identities-status). diff --git a/lib/msal-node/src/client/ManagedIdentityClient.ts b/lib/msal-node/src/client/ManagedIdentityClient.ts index 555f116992..d926ab9bc3 100644 --- a/lib/msal-node/src/client/ManagedIdentityClient.ts +++ b/lib/msal-node/src/client/ManagedIdentityClient.ts @@ -24,6 +24,7 @@ import { ManagedIdentityId } from "../config/ManagedIdentityId.js"; import { NodeStorage } from "../cache/NodeStorage.js"; import { BaseManagedIdentitySource } from "./ManagedIdentitySources/BaseManagedIdentitySource.js"; import { ManagedIdentitySourceNames } from "../utils/Constants.js"; +import { MachineLearning } from "./ManagedIdentitySources/MachineLearning.js"; /* * Class to initialize a managed identity and identify the service. @@ -99,6 +100,10 @@ export class ManagedIdentityClient { AppService.getEnvironmentVariables() ) ? ManagedIdentitySourceNames.APP_SERVICE + : this.allEnvironmentVariablesAreDefined( + MachineLearning.getEnvironmentVariables() + ) + ? ManagedIdentitySourceNames.MACHINE_LEARNING : this.allEnvironmentVariablesAreDefined( CloudShell.getEnvironmentVariables() ) @@ -137,6 +142,12 @@ export class ManagedIdentityClient { networkClient, cryptoProvider ) || + MachineLearning.tryCreate( + logger, + nodeStorage, + networkClient, + cryptoProvider + ) || CloudShell.tryCreate( logger, nodeStorage, diff --git a/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts b/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts new file mode 100644 index 0000000000..c70fe1cb79 --- /dev/null +++ b/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts @@ -0,0 +1,130 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { INetworkModule, Logger } from "@azure/msal-common/node"; +import { BaseManagedIdentitySource } from "./BaseManagedIdentitySource.js"; +import { + HttpMethod, + API_VERSION_QUERY_PARAMETER_NAME, + RESOURCE_BODY_OR_QUERY_PARAMETER_NAME, + ManagedIdentityEnvironmentVariableNames, + ManagedIdentitySourceNames, + ManagedIdentityIdType, + METADATA_HEADER_NAME, + MACHINE_LEARNING_AND_SERVICE_FABRIC_SECRET_HEADER_NAME, +} from "../../utils/Constants.js"; +import { CryptoProvider } from "../../crypto/CryptoProvider.js"; +import { ManagedIdentityRequestParameters } from "../../config/ManagedIdentityRequestParameters.js"; +import { ManagedIdentityId } from "../../config/ManagedIdentityId.js"; +import { NodeStorage } from "../../cache/NodeStorage.js"; + +const MACHINE_LEARNING_MSI_API_VERSION: string = "2017-09-01"; + +// search for all App Service + +export class MachineLearning extends BaseManagedIdentitySource { + private identityEndpoint: string; + private secret: string; + + constructor( + logger: Logger, + nodeStorage: NodeStorage, + networkClient: INetworkModule, + cryptoProvider: CryptoProvider, + identityEndpoint: string, + secret: string + ) { + super(logger, nodeStorage, networkClient, cryptoProvider); + + this.identityEndpoint = identityEndpoint; + this.secret = secret; + } + + public static getEnvironmentVariables(): Array { + const identityEndpoint: string | undefined = + process.env[ + ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT + ]; + + const secret: string | undefined = + process.env[ManagedIdentityEnvironmentVariableNames.MSI_SECRET]; + + return [identityEndpoint, secret]; + } + + public static tryCreate( + logger: Logger, + nodeStorage: NodeStorage, + networkClient: INetworkModule, + cryptoProvider: CryptoProvider + ): MachineLearning | null { + const [identityEndpoint, secret] = + MachineLearning.getEnvironmentVariables(); + + // if either of the identity endpoint or MSI secret variables are undefined, this MSI provider is unavailable. + if (!identityEndpoint || !secret) { + logger.info( + `[Managed Identity] ${ManagedIdentitySourceNames.MACHINE_LEARNING} managed identity is unavailable because one or both of the '${ManagedIdentityEnvironmentVariableNames.IDENTITY_HEADER}' and '${ManagedIdentityEnvironmentVariableNames.MSI_SECRET}' environment variables are not defined.` + ); + return null; + } + + const validatedIdentityEndpoint: string = + MachineLearning.getValidatedEnvVariableUrlString( + ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT, + identityEndpoint, + ManagedIdentitySourceNames.MACHINE_LEARNING, + logger + ); + + logger.info( + `[Managed Identity] Environment variables validation passed for ${ManagedIdentitySourceNames.MACHINE_LEARNING} managed identity. Endpoint URI: ${validatedIdentityEndpoint}. Creating ${ManagedIdentitySourceNames.MACHINE_LEARNING} managed identity.` + ); + + return new MachineLearning( + logger, + nodeStorage, + networkClient, + cryptoProvider, + identityEndpoint, + secret + ); + } + + public createRequest( + resource: string, + managedIdentityId: ManagedIdentityId + ): ManagedIdentityRequestParameters { + const request: ManagedIdentityRequestParameters = + new ManagedIdentityRequestParameters( + HttpMethod.GET, + this.identityEndpoint + ); + + request.headers[METADATA_HEADER_NAME] = "true"; + request.headers[ + MACHINE_LEARNING_AND_SERVICE_FABRIC_SECRET_HEADER_NAME + ] = this.secret; + + request.queryParameters[API_VERSION_QUERY_PARAMETER_NAME] = + MACHINE_LEARNING_MSI_API_VERSION; + request.queryParameters[RESOURCE_BODY_OR_QUERY_PARAMETER_NAME] = + resource; + + if ( + managedIdentityId.idType !== ManagedIdentityIdType.SYSTEM_ASSIGNED + ) { + request.queryParameters[ + this.getManagedIdentityUserAssignedIdQueryParameterKey( + managedIdentityId.idType + ) + ] = managedIdentityId.id; + } + + // bodyParameters calculated in BaseManagedIdentity.acquireTokenWithManagedIdentity + + return request; + } +} diff --git a/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts b/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts index 501d2f24ea..96dbeae2d4 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts @@ -16,7 +16,7 @@ import { ManagedIdentityIdType, ManagedIdentitySourceNames, RESOURCE_BODY_OR_QUERY_PARAMETER_NAME, - SERVICE_FABRIC_SECRET_HEADER_NAME, + MACHINE_LEARNING_AND_SERVICE_FABRIC_SECRET_HEADER_NAME, } from "../../utils/Constants.js"; // MSI Constants. Docs for MSI are available here https://docs.microsoft.com/azure/app-service/overview-managed-identity @@ -122,8 +122,9 @@ export class ServiceFabric extends BaseManagedIdentitySource { this.identityEndpoint ); - request.headers[SERVICE_FABRIC_SECRET_HEADER_NAME] = - this.identityHeader; + request.headers[ + MACHINE_LEARNING_AND_SERVICE_FABRIC_SECRET_HEADER_NAME + ] = this.identityHeader; request.queryParameters[API_VERSION_QUERY_PARAMETER_NAME] = SERVICE_FABRIC_MSI_API_VERSION; diff --git a/lib/msal-node/src/utils/Constants.ts b/lib/msal-node/src/utils/Constants.ts index efbffa267b..3134187185 100644 --- a/lib/msal-node/src/utils/Constants.ts +++ b/lib/msal-node/src/utils/Constants.ts @@ -9,7 +9,8 @@ import { HttpStatus } from "@azure/msal-common/node"; export const AUTHORIZATION_HEADER_NAME: string = "Authorization"; export const METADATA_HEADER_NAME: string = "Metadata"; export const APP_SERVICE_SECRET_HEADER_NAME: string = "X-IDENTITY-HEADER"; -export const SERVICE_FABRIC_SECRET_HEADER_NAME: string = "secret"; +export const MACHINE_LEARNING_AND_SERVICE_FABRIC_SECRET_HEADER_NAME: string = + "secret"; export const API_VERSION_QUERY_PARAMETER_NAME: string = "api-version"; export const RESOURCE_BODY_OR_QUERY_PARAMETER_NAME: string = "resource"; export const DEFAULT_MANAGED_IDENTITY_ID = "system_assigned_managed_identity"; @@ -26,6 +27,7 @@ export const ManagedIdentityEnvironmentVariableNames = { IDENTITY_SERVER_THUMBPRINT: "IDENTITY_SERVER_THUMBPRINT", IMDS_ENDPOINT: "IMDS_ENDPOINT", MSI_ENDPOINT: "MSI_ENDPOINT", + MSI_SECRET: "MSI_SECRET", } as const; export type ManagedIdentityEnvironmentVariableNames = (typeof ManagedIdentityEnvironmentVariableNames)[keyof typeof ManagedIdentityEnvironmentVariableNames]; @@ -40,6 +42,7 @@ export const ManagedIdentitySourceNames = { CLOUD_SHELL: "CloudShell", DEFAULT_TO_IMDS: "DefaultToImds", IMDS: "Imds", + MACHINE_LEARNING: "MachineLearning", SERVICE_FABRIC: "ServiceFabric", } as const; /** diff --git a/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts new file mode 100644 index 0000000000..8bbd3470d7 --- /dev/null +++ b/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts @@ -0,0 +1,159 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ManagedIdentityApplication } from "../../../src/client/ManagedIdentityApplication.js"; +import { + DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, + DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, + MANAGED_IDENTITY_MACHINE_LEARNING_NETWORK_REQUEST_400_ERROR, + MANAGED_IDENTITY_RESOURCE, +} from "../../test_kit/StringConstants.js"; + +import { + userAssignedClientIdConfig, + managedIdentityRequestParams, + systemAssignedConfig, + networkClient, + ManagedIdentityNetworkErrorClient, +} from "../../test_kit/ManagedIdentityTestUtils.js"; +import { + AuthenticationResult, + HttpStatus, + ServerError, +} from "@azure/msal-common"; +import { ManagedIdentityClient } from "../../../src/client/ManagedIdentityClient.js"; +import { + ManagedIdentityEnvironmentVariableNames, + ManagedIdentitySourceNames, +} from "../../../src/utils/Constants.js"; + +describe("Acquires a token successfully via an Machine Learning Managed Identity", () => { + beforeAll(() => { + process.env[ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT] = + "fake_IDENTITY_ENDPOINT"; + process.env[ManagedIdentityEnvironmentVariableNames.MSI_SECRET] = + "fake_MSI_SECRET"; + }); + + afterAll(() => { + delete process.env[ + ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT + ]; + delete process.env[ManagedIdentityEnvironmentVariableNames.MSI_SECRET]; + }); + + afterEach(() => { + // reset static variables after each test + delete ManagedIdentityClient["identitySource"]; + delete ManagedIdentityApplication["nodeStorage"]; + }); + + test("acquires a User Assigned Client Id token", async () => { + const managedIdentityApplication: ManagedIdentityApplication = + new ManagedIdentityApplication(userAssignedClientIdConfig); + expect(managedIdentityApplication.getManagedIdentitySource()).toBe( + ManagedIdentitySourceNames.MACHINE_LEARNING + ); + + const networkManagedIdentityResult: AuthenticationResult = + await managedIdentityApplication.acquireToken( + managedIdentityRequestParams + ); + + expect(networkManagedIdentityResult.accessToken).toEqual( + DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken + ); + }); + + describe("System Assigned", () => { + let managedIdentityApplication: ManagedIdentityApplication; + beforeEach(() => { + managedIdentityApplication = new ManagedIdentityApplication( + systemAssignedConfig + ); + expect(managedIdentityApplication.getManagedIdentitySource()).toBe( + ManagedIdentitySourceNames.MACHINE_LEARNING + ); + }); + + test("acquires a token", async () => { + const networkManagedIdentityResult: AuthenticationResult = + await managedIdentityApplication.acquireToken( + managedIdentityRequestParams + ); + expect(networkManagedIdentityResult.fromCache).toBe(false); + + expect(networkManagedIdentityResult.accessToken).toEqual( + DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken + ); + }); + + test("returns an already acquired token from the cache", async () => { + const networkManagedIdentityResult: AuthenticationResult = + await managedIdentityApplication.acquireToken({ + resource: MANAGED_IDENTITY_RESOURCE, + }); + expect(networkManagedIdentityResult.fromCache).toBe(false); + + expect(networkManagedIdentityResult.accessToken).toEqual( + DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken + ); + + const cachedManagedIdentityResult: AuthenticationResult = + await managedIdentityApplication.acquireToken({ + resource: MANAGED_IDENTITY_RESOURCE, + }); + expect(cachedManagedIdentityResult.fromCache).toBe(true); + expect(cachedManagedIdentityResult.accessToken).toEqual( + DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken + ); + }); + }); + + describe("Errors", () => { + test("ensures that the error format is correct", async () => { + const managedIdentityNetworkErrorClient400 = + new ManagedIdentityNetworkErrorClient( + MANAGED_IDENTITY_MACHINE_LEARNING_NETWORK_REQUEST_400_ERROR, + undefined, + HttpStatus.BAD_REQUEST + ); + + jest.spyOn(networkClient, "sendGetRequestAsync") + // permanently override the networkClient's sendGetRequestAsync method to return a 400 + .mockReturnValue( + managedIdentityNetworkErrorClient400.sendGetRequestAsync() + ); + + const managedIdentityApplication: ManagedIdentityApplication = + new ManagedIdentityApplication(systemAssignedConfig); + expect(managedIdentityApplication.getManagedIdentitySource()).toBe( + ManagedIdentitySourceNames.MACHINE_LEARNING + ); + + let serverError: ServerError = new ServerError(); + try { + await managedIdentityApplication.acquireToken( + managedIdentityRequestParams + ); + } catch (e) { + serverError = e as ServerError; + } + + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_MACHINE_LEARNING_NETWORK_REQUEST_400_ERROR.message as string + ) + ).toBe(true); + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_MACHINE_LEARNING_NETWORK_REQUEST_400_ERROR.correlation_id as string + ) + ).toBe(true); + + jest.restoreAllMocks(); + }); + }); +}); diff --git a/lib/msal-node/test/test_kit/StringConstants.ts b/lib/msal-node/test/test_kit/StringConstants.ts index 7c63dfdfa9..92f8503693 100644 --- a/lib/msal-node/test/test_kit/StringConstants.ts +++ b/lib/msal-node/test/test_kit/StringConstants.ts @@ -396,6 +396,11 @@ export const MANAGED_IDENTITY_APP_SERVICE_NETWORK_REQUEST_400_ERROR: ManagedIden { ...MANAGED_IDENTITY_NETWORK_REQUEST_500_ERROR, }; +// Machine Learning 400 error response +export const MANAGED_IDENTITY_MACHINE_LEARNING_NETWORK_REQUEST_400_ERROR: ManagedIdentityTokenResponse = + { + ...MANAGED_IDENTITY_NETWORK_REQUEST_500_ERROR, + }; // Cloud Shell 400 error response export const MANAGED_IDENTITY_CLOUD_SHELL_NETWORK_REQUEST_400_ERROR: ManagedIdentityTokenResponse = { From c0535e8357de4a2d0d7db1588d90db5c0a6921eb Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 15 Jan 2025 17:14:18 -0500 Subject: [PATCH 02/11] Change files --- ...ure-msal-node-ca262c56-49c0-4e6c-b402-b296b4a10d52.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@azure-msal-node-ca262c56-49c0-4e6c-b402-b296b4a10d52.json diff --git a/change/@azure-msal-node-ca262c56-49c0-4e6c-b402-b296b4a10d52.json b/change/@azure-msal-node-ca262c56-49c0-4e6c-b402-b296b4a10d52.json new file mode 100644 index 0000000000..01774e7e05 --- /dev/null +++ b/change/@azure-msal-node-ca262c56-49c0-4e6c-b402-b296b4a10d52.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Added Machine Learning as a Managed Identity Source", + "packageName": "@azure/msal-node", + "email": "rginsburg@microsoft.com", + "dependentChangeType": "patch" +} From 9fe9abab136ebd84cda9eb91c8c62d501609fff9 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 15 Jan 2025 17:16:26 -0500 Subject: [PATCH 03/11] beachball and apiExtractor --- lib/msal-node/apiReview/msal-node.api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/msal-node/apiReview/msal-node.api.md b/lib/msal-node/apiReview/msal-node.api.md index 3a19300273..ed9af9f253 100644 --- a/lib/msal-node/apiReview/msal-node.api.md +++ b/lib/msal-node/apiReview/msal-node.api.md @@ -414,6 +414,7 @@ export const ManagedIdentitySourceNames: { readonly CLOUD_SHELL: "CloudShell"; readonly DEFAULT_TO_IMDS: "DefaultToImds"; readonly IMDS: "Imds"; + readonly MACHINE_LEARNING: "MachineLearning"; readonly SERVICE_FABRIC: "ServiceFabric"; }; From 27eaf6502a4f685b1e5154a46766bc15a4cc29a8 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 15 Jan 2025 17:19:31 -0500 Subject: [PATCH 04/11] beachball --- .../@azure-msal-node-ca262c56-49c0-4e6c-b402-b296b4a10d52.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/change/@azure-msal-node-ca262c56-49c0-4e6c-b402-b296b4a10d52.json b/change/@azure-msal-node-ca262c56-49c0-4e6c-b402-b296b4a10d52.json index 01774e7e05..5f45d1bc02 100644 --- a/change/@azure-msal-node-ca262c56-49c0-4e6c-b402-b296b4a10d52.json +++ b/change/@azure-msal-node-ca262c56-49c0-4e6c-b402-b296b4a10d52.json @@ -1,6 +1,6 @@ { "type": "minor", - "comment": "Added Machine Learning as a Managed Identity Source", + "comment": "Added Machine Learning as a Managed Identity Source #7512", "packageName": "@azure/msal-node", "email": "rginsburg@microsoft.com", "dependentChangeType": "patch" From e1ffd5eaf873d7a0d8b2a12aaaf0b089dc56d335 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 15 Jan 2025 19:01:01 -0500 Subject: [PATCH 05/11] Added additional unit tests --- .../MachineLearning.spec.ts | 67 +++++++++++++++---- 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts index 8bbd3470d7..d16986c12a 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts @@ -9,6 +9,7 @@ import { DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, MANAGED_IDENTITY_MACHINE_LEARNING_NETWORK_REQUEST_400_ERROR, MANAGED_IDENTITY_RESOURCE, + MANAGED_IDENTITY_RESOURCE_ID, } from "../../test_kit/StringConstants.js"; import { @@ -17,6 +18,7 @@ import { systemAssignedConfig, networkClient, ManagedIdentityNetworkErrorClient, + userAssignedResourceIdConfig, } from "../../test_kit/ManagedIdentityTestUtils.js"; import { AuthenticationResult, @@ -28,6 +30,7 @@ import { ManagedIdentityEnvironmentVariableNames, ManagedIdentitySourceNames, } from "../../../src/utils/Constants.js"; +import { ManagedIdentityUserAssignedIdQueryParameterNames } from "../../../src/client/ManagedIdentitySources/BaseManagedIdentitySource.js"; describe("Acquires a token successfully via an Machine Learning Managed Identity", () => { beforeAll(() => { @@ -50,21 +53,61 @@ describe("Acquires a token successfully via an Machine Learning Managed Identity delete ManagedIdentityApplication["nodeStorage"]; }); - test("acquires a User Assigned Client Id token", async () => { - const managedIdentityApplication: ManagedIdentityApplication = - new ManagedIdentityApplication(userAssignedClientIdConfig); - expect(managedIdentityApplication.getManagedIdentitySource()).toBe( - ManagedIdentitySourceNames.MACHINE_LEARNING - ); + describe("User Assigned", () => { + test("acquires a User Assigned Client Id token", async () => { + const managedIdentityApplication: ManagedIdentityApplication = + new ManagedIdentityApplication(userAssignedClientIdConfig); + expect(managedIdentityApplication.getManagedIdentitySource()).toBe( + ManagedIdentitySourceNames.MACHINE_LEARNING + ); + + const networkManagedIdentityResult: AuthenticationResult = + await managedIdentityApplication.acquireToken( + managedIdentityRequestParams + ); - const networkManagedIdentityResult: AuthenticationResult = - await managedIdentityApplication.acquireToken( - managedIdentityRequestParams + expect(networkManagedIdentityResult.accessToken).toEqual( + DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken + ); + }); + + test("acquires a User Assigned Resource Id token", async () => { + const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn( + networkClient, + "sendGetRequestAsync" + ); + + const managedIdentityApplication: ManagedIdentityApplication = + new ManagedIdentityApplication(userAssignedResourceIdConfig); + expect(managedIdentityApplication.getManagedIdentitySource()).toBe( + ManagedIdentitySourceNames.MACHINE_LEARNING ); - expect(networkManagedIdentityResult.accessToken).toEqual( - DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken - ); + const networkManagedIdentityResult: AuthenticationResult = + await managedIdentityApplication.acquireToken( + managedIdentityRequestParams + ); + + expect(networkManagedIdentityResult.accessToken).toEqual( + DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken + ); + + const url: URLSearchParams = new URLSearchParams( + sendGetRequestAsyncSpy.mock.lastCall[0] + ); + expect( + url.has( + ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_RESOURCE_ID_NON_IMDS + ) + ).toBe(true); + expect( + url.get( + ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_RESOURCE_ID_NON_IMDS + ) + ).toEqual(MANAGED_IDENTITY_RESOURCE_ID); + + jest.restoreAllMocks(); + }); }); describe("System Assigned", () => { From 474d25d12b7e2ece8e168ae44555d8448d276a24 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 15 Jan 2025 19:52:28 -0500 Subject: [PATCH 06/11] fixed typo --- .../src/client/ManagedIdentitySources/MachineLearning.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts b/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts index c70fe1cb79..71b7812a96 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts @@ -66,7 +66,7 @@ export class MachineLearning extends BaseManagedIdentitySource { // if either of the identity endpoint or MSI secret variables are undefined, this MSI provider is unavailable. if (!identityEndpoint || !secret) { logger.info( - `[Managed Identity] ${ManagedIdentitySourceNames.MACHINE_LEARNING} managed identity is unavailable because one or both of the '${ManagedIdentityEnvironmentVariableNames.IDENTITY_HEADER}' and '${ManagedIdentityEnvironmentVariableNames.MSI_SECRET}' environment variables are not defined.` + `[Managed Identity] ${ManagedIdentitySourceNames.MACHINE_LEARNING} managed identity is unavailable because one or both of the '${ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT}' and '${ManagedIdentityEnvironmentVariableNames.MSI_SECRET}' environment variables are not defined.` ); return null; } From 9b2e70742b3983096b9c561b69e6ff6b529ab03b Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 16 Jan 2025 16:31:24 -0500 Subject: [PATCH 07/11] Implemented GitHub Feedback --- .../ManagedIdentitySources/MachineLearning.ts | 39 ++++++++----------- .../ManagedIdentitySources/ServiceFabric.ts | 6 +-- lib/msal-node/src/utils/Constants.ts | 3 +- .../MachineLearning.spec.ts | 28 +++++++++++-- 4 files changed, 45 insertions(+), 31 deletions(-) diff --git a/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts b/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts index 71b7812a96..44fa47a26e 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts @@ -13,7 +13,7 @@ import { ManagedIdentitySourceNames, ManagedIdentityIdType, METADATA_HEADER_NAME, - MACHINE_LEARNING_AND_SERVICE_FABRIC_SECRET_HEADER_NAME, + ML_AND_SF_SECRET_HEADER_NAME, } from "../../utils/Constants.js"; import { CryptoProvider } from "../../crypto/CryptoProvider.js"; import { ManagedIdentityRequestParameters } from "../../config/ManagedIdentityRequestParameters.js"; @@ -25,7 +25,7 @@ const MACHINE_LEARNING_MSI_API_VERSION: string = "2017-09-01"; // search for all App Service export class MachineLearning extends BaseManagedIdentitySource { - private identityEndpoint: string; + private msiEndpoint: string; private secret: string; constructor( @@ -33,25 +33,23 @@ export class MachineLearning extends BaseManagedIdentitySource { nodeStorage: NodeStorage, networkClient: INetworkModule, cryptoProvider: CryptoProvider, - identityEndpoint: string, + msiEndpoint: string, secret: string ) { super(logger, nodeStorage, networkClient, cryptoProvider); - this.identityEndpoint = identityEndpoint; + this.msiEndpoint = msiEndpoint; this.secret = secret; } public static getEnvironmentVariables(): Array { - const identityEndpoint: string | undefined = - process.env[ - ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT - ]; + const msiEndpoint: string | undefined = + process.env[ManagedIdentityEnvironmentVariableNames.MSI_ENDPOINT]; const secret: string | undefined = process.env[ManagedIdentityEnvironmentVariableNames.MSI_SECRET]; - return [identityEndpoint, secret]; + return [msiEndpoint, secret]; } public static tryCreate( @@ -60,27 +58,26 @@ export class MachineLearning extends BaseManagedIdentitySource { networkClient: INetworkModule, cryptoProvider: CryptoProvider ): MachineLearning | null { - const [identityEndpoint, secret] = - MachineLearning.getEnvironmentVariables(); + const [msiEndpoint, secret] = MachineLearning.getEnvironmentVariables(); // if either of the identity endpoint or MSI secret variables are undefined, this MSI provider is unavailable. - if (!identityEndpoint || !secret) { + if (!msiEndpoint || !secret) { logger.info( - `[Managed Identity] ${ManagedIdentitySourceNames.MACHINE_LEARNING} managed identity is unavailable because one or both of the '${ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT}' and '${ManagedIdentityEnvironmentVariableNames.MSI_SECRET}' environment variables are not defined.` + `[Managed Identity] ${ManagedIdentitySourceNames.MACHINE_LEARNING} managed identity is unavailable because one or both of the '${ManagedIdentityEnvironmentVariableNames.MSI_ENDPOINT}' and '${ManagedIdentityEnvironmentVariableNames.MSI_SECRET}' environment variables are not defined.` ); return null; } - const validatedIdentityEndpoint: string = + const validatedMsiEndpoint: string = MachineLearning.getValidatedEnvVariableUrlString( - ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT, - identityEndpoint, + ManagedIdentityEnvironmentVariableNames.MSI_ENDPOINT, + msiEndpoint, ManagedIdentitySourceNames.MACHINE_LEARNING, logger ); logger.info( - `[Managed Identity] Environment variables validation passed for ${ManagedIdentitySourceNames.MACHINE_LEARNING} managed identity. Endpoint URI: ${validatedIdentityEndpoint}. Creating ${ManagedIdentitySourceNames.MACHINE_LEARNING} managed identity.` + `[Managed Identity] Environment variables validation passed for ${ManagedIdentitySourceNames.MACHINE_LEARNING} managed identity. Endpoint URI: ${validatedMsiEndpoint}. Creating ${ManagedIdentitySourceNames.MACHINE_LEARNING} managed identity.` ); return new MachineLearning( @@ -88,7 +85,7 @@ export class MachineLearning extends BaseManagedIdentitySource { nodeStorage, networkClient, cryptoProvider, - identityEndpoint, + msiEndpoint, secret ); } @@ -100,13 +97,11 @@ export class MachineLearning extends BaseManagedIdentitySource { const request: ManagedIdentityRequestParameters = new ManagedIdentityRequestParameters( HttpMethod.GET, - this.identityEndpoint + this.msiEndpoint ); request.headers[METADATA_HEADER_NAME] = "true"; - request.headers[ - MACHINE_LEARNING_AND_SERVICE_FABRIC_SECRET_HEADER_NAME - ] = this.secret; + request.headers[ML_AND_SF_SECRET_HEADER_NAME] = this.secret; request.queryParameters[API_VERSION_QUERY_PARAMETER_NAME] = MACHINE_LEARNING_MSI_API_VERSION; diff --git a/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts b/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts index 96dbeae2d4..8d93bacf26 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts @@ -16,7 +16,7 @@ import { ManagedIdentityIdType, ManagedIdentitySourceNames, RESOURCE_BODY_OR_QUERY_PARAMETER_NAME, - MACHINE_LEARNING_AND_SERVICE_FABRIC_SECRET_HEADER_NAME, + ML_AND_SF_SECRET_HEADER_NAME, } from "../../utils/Constants.js"; // MSI Constants. Docs for MSI are available here https://docs.microsoft.com/azure/app-service/overview-managed-identity @@ -122,9 +122,7 @@ export class ServiceFabric extends BaseManagedIdentitySource { this.identityEndpoint ); - request.headers[ - MACHINE_LEARNING_AND_SERVICE_FABRIC_SECRET_HEADER_NAME - ] = this.identityHeader; + request.headers[ML_AND_SF_SECRET_HEADER_NAME] = this.identityHeader; request.queryParameters[API_VERSION_QUERY_PARAMETER_NAME] = SERVICE_FABRIC_MSI_API_VERSION; diff --git a/lib/msal-node/src/utils/Constants.ts b/lib/msal-node/src/utils/Constants.ts index 3134187185..51ab42ad2d 100644 --- a/lib/msal-node/src/utils/Constants.ts +++ b/lib/msal-node/src/utils/Constants.ts @@ -9,8 +9,7 @@ import { HttpStatus } from "@azure/msal-common/node"; export const AUTHORIZATION_HEADER_NAME: string = "Authorization"; export const METADATA_HEADER_NAME: string = "Metadata"; export const APP_SERVICE_SECRET_HEADER_NAME: string = "X-IDENTITY-HEADER"; -export const MACHINE_LEARNING_AND_SERVICE_FABRIC_SECRET_HEADER_NAME: string = - "secret"; +export const ML_AND_SF_SECRET_HEADER_NAME: string = "secret"; export const API_VERSION_QUERY_PARAMETER_NAME: string = "api-version"; export const RESOURCE_BODY_OR_QUERY_PARAMETER_NAME: string = "resource"; export const DEFAULT_MANAGED_IDENTITY_ID = "system_assigned_managed_identity"; diff --git a/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts index d16986c12a..e92c2260c2 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts @@ -34,15 +34,15 @@ import { ManagedIdentityUserAssignedIdQueryParameterNames } from "../../../src/c describe("Acquires a token successfully via an Machine Learning Managed Identity", () => { beforeAll(() => { - process.env[ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT] = - "fake_IDENTITY_ENDPOINT"; + process.env[ManagedIdentityEnvironmentVariableNames.MSI_ENDPOINT] = + "fake_MSI_ENDPOINT"; process.env[ManagedIdentityEnvironmentVariableNames.MSI_SECRET] = "fake_MSI_SECRET"; }); afterAll(() => { delete process.env[ - ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT + ManagedIdentityEnvironmentVariableNames.MSI_ENDPOINT ]; delete process.env[ManagedIdentityEnvironmentVariableNames.MSI_SECRET]; }); @@ -108,6 +108,28 @@ describe("Acquires a token successfully via an Machine Learning Managed Identity jest.restoreAllMocks(); }); + + test("ensures that App Service is selected as the Managed Identity source when all of its and Machine Learning's environment variables are present", async () => { + process.env[ + ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT + ] = "fake_IDENTITY_ENDPOINT"; + process.env[ + ManagedIdentityEnvironmentVariableNames.IDENTITY_HEADER + ] = "fake_IDENTITY_HEADER"; + + const managedIdentityApplication: ManagedIdentityApplication = + new ManagedIdentityApplication(userAssignedClientIdConfig); + expect(managedIdentityApplication.getManagedIdentitySource()).toBe( + ManagedIdentitySourceNames.APP_SERVICE + ); + + delete process.env[ + ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT + ]; + delete process.env[ + ManagedIdentityEnvironmentVariableNames.IDENTITY_HEADER + ]; + }); }); describe("System Assigned", () => { From 400a164340f193c1b2f3869d7ded089cb0e236a4 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 17 Jan 2025 02:00:30 -0500 Subject: [PATCH 08/11] removed locale from url --- lib/msal-node/docs/managed-identity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msal-node/docs/managed-identity.md b/lib/msal-node/docs/managed-identity.md index b8a578b144..fcf48fdc5e 100644 --- a/lib/msal-node/docs/managed-identity.md +++ b/lib/msal-node/docs/managed-identity.md @@ -11,7 +11,7 @@ A common challenge for developers is the management of secrets, credentials, cer - [Azure Arc](https://learn.microsoft.com/en-us/azure/azure-arc/overview) - [Azure Cloud Shell](https://learn.microsoft.com/en-us/azure/cloud-shell/overview) - [Azure Service Fabric](https://learn.microsoft.com/en-us/azure/service-fabric/service-fabric-overview) -- [Azure Machine Learning](https://azure.microsoft.com/en-us/products/machine-learning) +- [Azure Machine Learning](https://azure.microsoft.com/products/machine-learning) For a complete list, refer to [Azure services that can use managed identities to access other services](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/managed-identities-status). From 696ab696deef0b939d9a61d79f4fc2dfb3f40497 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Tue, 21 Jan 2025 17:52:49 -0500 Subject: [PATCH 09/11] removed comment used in testing --- .../src/client/ManagedIdentitySources/MachineLearning.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts b/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts index 44fa47a26e..dddbe6b460 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts @@ -22,8 +22,6 @@ import { NodeStorage } from "../../cache/NodeStorage.js"; const MACHINE_LEARNING_MSI_API_VERSION: string = "2017-09-01"; -// search for all App Service - export class MachineLearning extends BaseManagedIdentitySource { private msiEndpoint: string; private secret: string; @@ -60,7 +58,7 @@ export class MachineLearning extends BaseManagedIdentitySource { ): MachineLearning | null { const [msiEndpoint, secret] = MachineLearning.getEnvironmentVariables(); - // if either of the identity endpoint or MSI secret variables are undefined, this MSI provider is unavailable. + // if either of the MSI endpoint or MSI secret variables are undefined, this MSI provider is unavailable. if (!msiEndpoint || !secret) { logger.info( `[Managed Identity] ${ManagedIdentitySourceNames.MACHINE_LEARNING} managed identity is unavailable because one or both of the '${ManagedIdentityEnvironmentVariableNames.MSI_ENDPOINT}' and '${ManagedIdentityEnvironmentVariableNames.MSI_SECRET}' environment variables are not defined.` From 4447212246217b3c524ef0224321b375f6b7dfdf Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Tue, 21 Jan 2025 18:07:11 -0500 Subject: [PATCH 10/11] re-worded test description --- .../test/client/ManagedIdentitySources/MachineLearning.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts index e92c2260c2..64c86a30ca 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts @@ -109,7 +109,7 @@ describe("Acquires a token successfully via an Machine Learning Managed Identity jest.restoreAllMocks(); }); - test("ensures that App Service is selected as the Managed Identity source when all of its and Machine Learning's environment variables are present", async () => { + test("ensures that App Service is selected as the Managed Identity source when all App Service and Machine Learning environment variables are present", async () => { process.env[ ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT ] = "fake_IDENTITY_ENDPOINT"; From ef8811866409144dc89014bc386b9a8ac6eb084d Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Tue, 21 Jan 2025 19:54:23 -0500 Subject: [PATCH 11/11] Fixed bug in tests --- .../test/client/ManagedIdentitySources/MachineLearning.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts index 64c86a30ca..f499edc828 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts @@ -50,6 +50,7 @@ describe("Acquires a token successfully via an Machine Learning Managed Identity afterEach(() => { // reset static variables after each test delete ManagedIdentityClient["identitySource"]; + delete ManagedIdentityClient["sourceName"]; delete ManagedIdentityApplication["nodeStorage"]; });