diff --git a/change/@azure-msal-node-cb198692-a92c-4591-96a7-77bb2383a356.json b/change/@azure-msal-node-cb198692-a92c-4591-96a7-77bb2383a356.json new file mode 100644 index 0000000000..5956a7154f --- /dev/null +++ b/change/@azure-msal-node-cb198692-a92c-4591-96a7-77bb2383a356.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Added token revocation functionality to Managed Identity's App Service source #7772", + "packageName": "@azure/msal-node", + "email": "rginsburg@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/lib/msal-node/src/client/ManagedIdentityApplication.ts b/lib/msal-node/src/client/ManagedIdentityApplication.ts index c98ec75385..5e9a95355d 100644 --- a/lib/msal-node/src/client/ManagedIdentityApplication.ts +++ b/lib/msal-node/src/client/ManagedIdentityApplication.ts @@ -40,7 +40,10 @@ import { ManagedIdentityId } from "../config/ManagedIdentityId.js"; import { HashUtils } from "../crypto/HashUtils.js"; const SOURCES_THAT_SUPPORT_TOKEN_REVOCATION: Array = - [ManagedIdentitySourceNames.SERVICE_FABRIC]; + [ + ManagedIdentitySourceNames.APP_SERVICE, + ManagedIdentitySourceNames.SERVICE_FABRIC, + ]; /** * Class to initialize a managed identity and identify the service diff --git a/lib/msal-node/src/client/ManagedIdentitySources/AppService.ts b/lib/msal-node/src/client/ManagedIdentitySources/AppService.ts index 431dd65c00..d59eb5ac05 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/AppService.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/AppService.ts @@ -19,7 +19,7 @@ import { ManagedIdentityId } from "../../config/ManagedIdentityId.js"; import { NodeStorage } from "../../cache/NodeStorage.js"; // MSI Constants. Docs for MSI are available here https://docs.microsoft.com/azure/app-service/overview-managed-identity -const APP_SERVICE_MSI_API_VERSION: string = "2019-08-01"; +const APP_SERVICE_MSI_API_VERSION: string = "2025-03-30"; /** * Original source of code: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/src/AppServiceManagedIdentitySource.cs diff --git a/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts index ed9679f00a..08b13678c6 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts @@ -5,11 +5,14 @@ import { ManagedIdentityApplication } from "../../../src/client/ManagedIdentityApplication.js"; import { + CAE_CONSTANTS, + DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT_ACCESS_TOKEN_SHA256_HASH_IN_HEX, DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, MANAGED_IDENTITY_APP_SERVICE_NETWORK_REQUEST_400_ERROR, MANAGED_IDENTITY_RESOURCE, MANAGED_IDENTITY_RESOURCE_ID, + TEST_CONFIG, } from "../../test_kit/StringConstants.js"; import { @@ -28,6 +31,7 @@ import { import { ManagedIdentityClient } from "../../../src/client/ManagedIdentityClient.js"; import { ManagedIdentityEnvironmentVariableNames, + ManagedIdentityQueryParameters, ManagedIdentitySourceNames, } from "../../../src/utils/Constants.js"; import { ManagedIdentityUserAssignedIdQueryParameterNames } from "../../../src/client/ManagedIdentitySources/BaseManagedIdentitySource.js"; @@ -176,6 +180,86 @@ describe("Acquires a token successfully via an App Service Managed Identity", () }); }); + describe("Miscellaneous", () => { + it.each([ + [ + CAE_CONSTANTS.CLIENT_CAPABILITIES, + CAE_CONSTANTS.CLIENT_CAPABILITIES.toString(), + ], + [undefined, null], + ])( + "ignores a cached token when claims are provided (regardless of if client capabilities are provided or not) and the Managed Identity does support token revocation, and ensures the token revocation query parameter token_sha256_to_refresh is included in the network request to the Managed Identity", + async (providedCapabilities, capabilitiesOnNetworkRequest) => { + const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn( + networkClient, + "sendGetRequestAsync" + ); + + const managedIdentityApplication: ManagedIdentityApplication = + new ManagedIdentityApplication({ + ...systemAssignedConfig, + clientCapabilities: providedCapabilities, + }); + expect( + managedIdentityApplication.getManagedIdentitySource() + ).toBe(ManagedIdentitySourceNames.APP_SERVICE); + + let 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 + ); + + expect(sendGetRequestAsyncSpy.mock.calls.length).toEqual(1); + const firstNetworkRequestUrlParams: URLSearchParams = + new URLSearchParams( + sendGetRequestAsyncSpy.mock.lastCall[0] + ); + expect( + firstNetworkRequestUrlParams.get( + ManagedIdentityQueryParameters.XMS_CC + ) + ).toEqual(capabilitiesOnNetworkRequest); + + 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 + ); + expect(sendGetRequestAsyncSpy.mock.calls.length).toEqual(1); + + networkManagedIdentityResult = + await managedIdentityApplication.acquireToken({ + claims: TEST_CONFIG.CLAIMS, + resource: MANAGED_IDENTITY_RESOURCE, + }); + expect(networkManagedIdentityResult.fromCache).toBe(false); + expect(networkManagedIdentityResult.accessToken).toEqual( + DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken + ); + + expect(sendGetRequestAsyncSpy.mock.calls.length).toEqual(2); + const secondNetworkRequestUrlParams: URLSearchParams = + new URLSearchParams( + sendGetRequestAsyncSpy.mock.lastCall[0] + ); + expect( + secondNetworkRequestUrlParams.get( + ManagedIdentityQueryParameters.SHA256_TOKEN_TO_REFRESH + ) + ).toEqual( + DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT_ACCESS_TOKEN_SHA256_HASH_IN_HEX + ); + } + ); + }); + describe("Errors", () => { test("ensures that the error format is correct", async () => { const managedIdentityNetworkErrorClient400 =