From 920bac326c5e8f22164ac5426327c677b6da2f41 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 2 Apr 2025 15:08:00 -0400 Subject: [PATCH 01/18] Initial Implementation --- lib/msal-node/apiReview/msal-node.api.md | 1 + .../src/client/ManagedIdentityApplication.ts | 28 +++++++++++--- .../ManagedIdentitySources/AppService.ts | 18 +++++---- .../client/ManagedIdentitySources/AzureArc.ts | 20 ++++++---- .../BaseManagedIdentitySource.ts | 38 ++++++++++++++++++- .../ManagedIdentitySources/CloudShell.ts | 12 ++++-- .../src/client/ManagedIdentitySources/Imds.ts | 15 +++++--- .../ManagedIdentitySources/MachineLearning.ts | 19 ++++++---- .../ManagedIdentitySources/ServiceFabric.ts | 16 +++++--- lib/msal-node/src/config/Configuration.ts | 6 ++- .../src/request/ManagedIdentityRequest.ts | 5 ++- lib/msal-node/src/utils/Constants.ts | 30 ++++++++++++--- 12 files changed, 155 insertions(+), 53 deletions(-) diff --git a/lib/msal-node/apiReview/msal-node.api.md b/lib/msal-node/apiReview/msal-node.api.md index beba4484ed..1bbe9769f9 100644 --- a/lib/msal-node/apiReview/msal-node.api.md +++ b/lib/msal-node/apiReview/msal-node.api.md @@ -397,6 +397,7 @@ export class ManagedIdentityApplication { // @public (undocumented) export type ManagedIdentityConfiguration = { + clientCapabilities?: Array; managedIdentityIdParams?: ManagedIdentityIdParams; system?: NodeSystemOptions; }; diff --git a/lib/msal-node/src/client/ManagedIdentityApplication.ts b/lib/msal-node/src/client/ManagedIdentityApplication.ts index 6f2388335a..7586f08f44 100644 --- a/lib/msal-node/src/client/ManagedIdentityApplication.ts +++ b/lib/msal-node/src/client/ManagedIdentityApplication.ts @@ -141,12 +141,11 @@ export class ManagedIdentityApplication { ], authority: this.fakeAuthority.canonicalAuthority, correlationId: this.cryptoProvider.createNewGuid(), + claims: managedIdentityRequestParams.claims, + clientCapabilities: this.config.clientCapabilities, }; - if ( - managedIdentityRequestParams.claims || - managedIdentityRequest.forceRefresh - ) { + if (managedIdentityRequest.forceRefresh) { // make a network call to the managed identity source return this.managedIdentityClient.sendManagedIdentityTokenRequest( managedIdentityRequest, @@ -165,6 +164,25 @@ export class ManagedIdentityApplication { ); if (cachedAuthenticationResult) { + /* + * Check if claims are present in the managed identity request. + * If they are, hash the access token and add it to the request. + */ + if (managedIdentityRequest.claims) { + const accessTokenSha256Hash: string = + await this.cryptoProvider.hashString( + cachedAuthenticationResult.accessToken + ); + managedIdentityRequest.accessTokenSha256Hash = + accessTokenSha256Hash; + + return this.managedIdentityClient.sendManagedIdentityTokenRequest( + managedIdentityRequest, + this.config.managedIdentityId, + this.fakeAuthority + ); + } + // if the token is not expired but must be refreshed; get a new one in the background if (lastCacheOutcome === CacheOutcome.PROACTIVELY_REFRESHED) { this.logger.info( @@ -173,7 +191,7 @@ export class ManagedIdentityApplication { // make a network call to the managed identity source; refresh the access token in the background const refreshAccessToken = true; - await this.managedIdentityClient.sendManagedIdentityTokenRequest( + return this.managedIdentityClient.sendManagedIdentityTokenRequest( managedIdentityRequest, this.config.managedIdentityId, this.fakeAuthority, diff --git a/lib/msal-node/src/client/ManagedIdentitySources/AppService.ts b/lib/msal-node/src/client/ManagedIdentitySources/AppService.ts index 9eccbf287c..e474c9eefd 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/AppService.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/AppService.ts @@ -7,12 +7,11 @@ import { INetworkModule, Logger } from "@azure/msal-common/node"; import { BaseManagedIdentitySource } from "./BaseManagedIdentitySource.js"; import { HttpMethod, - APP_SERVICE_SECRET_HEADER_NAME, - API_VERSION_QUERY_PARAMETER_NAME, - RESOURCE_BODY_OR_QUERY_PARAMETER_NAME, ManagedIdentityEnvironmentVariableNames, ManagedIdentitySourceNames, ManagedIdentityIdType, + ManagedIdentityQueryParameters, + ManagedIdentityHeaders, } from "../../utils/Constants.js"; import { CryptoProvider } from "../../crypto/CryptoProvider.js"; import { ManagedIdentityRequestParameters } from "../../config/ManagedIdentityRequestParameters.js"; @@ -20,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 @@ -50,6 +49,10 @@ export class AppService extends BaseManagedIdentitySource { this.identityHeader = identityHeader; } + public getSourceName(): ManagedIdentitySourceNames { + return ManagedIdentitySourceNames.APP_SERVICE; + } + public static getEnvironmentVariables(): Array { const identityEndpoint: string | undefined = process.env[ @@ -114,11 +117,12 @@ export class AppService extends BaseManagedIdentitySource { this.identityEndpoint ); - request.headers[APP_SERVICE_SECRET_HEADER_NAME] = this.identityHeader; + request.headers[ManagedIdentityHeaders.APP_SERVICE_SECRET_HEADER_NAME] = + this.identityHeader; - request.queryParameters[API_VERSION_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.API_VERSION] = APP_SERVICE_MSI_API_VERSION; - request.queryParameters[RESOURCE_BODY_OR_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.RESOURCE] = resource; if ( diff --git a/lib/msal-node/src/client/ManagedIdentitySources/AzureArc.ts b/lib/msal-node/src/client/ManagedIdentitySources/AzureArc.ts index 643f714e64..e337b07dd5 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/AzureArc.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/AzureArc.ts @@ -22,15 +22,13 @@ import { createManagedIdentityError, } from "../../error/ManagedIdentityError.js"; import { - API_VERSION_QUERY_PARAMETER_NAME, - AUTHORIZATION_HEADER_NAME, AZURE_ARC_SECRET_FILE_MAX_SIZE_BYTES, HttpMethod, - METADATA_HEADER_NAME, ManagedIdentityEnvironmentVariableNames, + ManagedIdentityHeaders, ManagedIdentityIdType, + ManagedIdentityQueryParameters, ManagedIdentitySourceNames, - RESOURCE_BODY_OR_QUERY_PARAMETER_NAME, } from "../../utils/Constants.js"; import { NodeStorage } from "../../cache/NodeStorage.js"; import { @@ -88,6 +86,10 @@ export class AzureArc extends BaseManagedIdentitySource { this.identityEndpoint = identityEndpoint; } + public getSourceName(): ManagedIdentitySourceNames { + return ManagedIdentitySourceNames.AZURE_ARC; + } + public static getEnvironmentVariables(): Array { let identityEndpoint: string | undefined = process.env[ @@ -201,11 +203,11 @@ export class AzureArc extends BaseManagedIdentitySource { this.identityEndpoint.replace("localhost", "127.0.0.1") ); - request.headers[METADATA_HEADER_NAME] = "true"; + request.headers[ManagedIdentityHeaders.METADATA_HEADER_NAME] = "true"; - request.queryParameters[API_VERSION_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.API_VERSION] = ARC_API_VERSION; - request.queryParameters[RESOURCE_BODY_OR_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.RESOURCE] = resource; // bodyParameters calculated in BaseManagedIdentity.acquireTokenWithManagedIdentity @@ -303,7 +305,9 @@ export class AzureArc extends BaseManagedIdentitySource { this.logger.info( `[Managed Identity] Adding authorization header to the request.` ); - networkRequest.headers[AUTHORIZATION_HEADER_NAME] = authHeaderValue; + networkRequest.headers[ + ManagedIdentityHeaders.AUTHORIZATION_HEADER_NAME + ] = authHeaderValue; try { retryResponse = diff --git a/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts b/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts index 1dd93e5865..5a7114202a 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts @@ -24,7 +24,12 @@ import { ManagedIdentityId } from "../../config/ManagedIdentityId.js"; import { ManagedIdentityRequestParameters } from "../../config/ManagedIdentityRequestParameters.js"; import { CryptoProvider } from "../../crypto/CryptoProvider.js"; import { ManagedIdentityRequest } from "../../request/ManagedIdentityRequest.js"; -import { HttpMethod, ManagedIdentityIdType } from "../../utils/Constants.js"; +import { + HttpMethod, + ManagedIdentityIdType, + ManagedIdentityQueryParameters, + ManagedIdentitySourceNames, +} from "../../utils/Constants.js"; import { ManagedIdentityTokenResponse } from "../../response/ManagedIdentityTokenResponse.js"; import { NodeStorage } from "../../cache/NodeStorage.js"; import { @@ -73,6 +78,8 @@ export abstract class BaseManagedIdentitySource { managedIdentityId: ManagedIdentityId ): ManagedIdentityRequestParameters; + abstract getSourceName(): ManagedIdentitySourceNames; + public async getServerTokenResponseAsync( response: NetworkResponse, // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -146,6 +153,35 @@ export abstract class BaseManagedIdentitySource { managedIdentityId ); + const sourceName: ManagedIdentitySourceNames = this.getSourceName(); + if ( + (sourceName === ManagedIdentitySourceNames.APP_SERVICE || + sourceName === ManagedIdentitySourceNames.SERVICE_FABRIC) && + managedIdentityRequest.claims && + managedIdentityRequest.accessTokenSha256Hash + ) { + this.logger.info( + `[Managed Identity] The following claims are present in the request: ${managedIdentityRequest.claims}` + ); + + networkRequest.queryParameters[ + ManagedIdentityQueryParameters.SHA256_TOKEN_TO_REFRESH + ] = managedIdentityRequest.accessTokenSha256Hash; + } + + if (managedIdentityRequest.clientCapabilities?.length) { + const clientCapabilities: string = + managedIdentityRequest.clientCapabilities.toString(); + + this.logger.info( + `[Managed Identity] The following claims are present in the request: ${clientCapabilities}` + ); + + networkRequest.queryParameters[ + ManagedIdentityQueryParameters.XMS_CC + ] = clientCapabilities; + } + const headers: Record = networkRequest.headers; headers[HeaderNames.CONTENT_TYPE] = Constants.URL_FORM_CONTENT_TYPE; diff --git a/lib/msal-node/src/client/ManagedIdentitySources/CloudShell.ts b/lib/msal-node/src/client/ManagedIdentitySources/CloudShell.ts index 55d99cabfd..7440138a17 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/CloudShell.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/CloudShell.ts @@ -10,11 +10,11 @@ import { NodeStorage } from "../../cache/NodeStorage.js"; import { CryptoProvider } from "../../crypto/CryptoProvider.js"; import { HttpMethod, - METADATA_HEADER_NAME, ManagedIdentityEnvironmentVariableNames, + ManagedIdentityHeaders, ManagedIdentityIdType, + ManagedIdentityQueryParameters, ManagedIdentitySourceNames, - RESOURCE_BODY_OR_QUERY_PARAMETER_NAME, } from "../../utils/Constants.js"; import { ManagedIdentityErrorCodes, @@ -47,6 +47,10 @@ export class CloudShell extends BaseManagedIdentitySource { this.msiEndpoint = msiEndpoint; } + public getSourceName(): ManagedIdentitySourceNames { + return ManagedIdentitySourceNames.CLOUD_SHELL; + } + public static getEnvironmentVariables(): Array { const msiEndpoint: string | undefined = process.env[ManagedIdentityEnvironmentVariableNames.MSI_ENDPOINT]; @@ -109,9 +113,9 @@ export class CloudShell extends BaseManagedIdentitySource { this.msiEndpoint ); - request.headers[METADATA_HEADER_NAME] = "true"; + request.headers[ManagedIdentityHeaders.METADATA_HEADER_NAME] = "true"; - request.bodyParameters[RESOURCE_BODY_OR_QUERY_PARAMETER_NAME] = + request.bodyParameters[ManagedIdentityQueryParameters.RESOURCE] = resource; return request; diff --git a/lib/msal-node/src/client/ManagedIdentitySources/Imds.ts b/lib/msal-node/src/client/ManagedIdentitySources/Imds.ts index fd97abceed..b2affffa3e 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/Imds.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/Imds.ts @@ -9,13 +9,12 @@ import { ManagedIdentityRequestParameters } from "../../config/ManagedIdentityRe import { BaseManagedIdentitySource } from "./BaseManagedIdentitySource.js"; import { CryptoProvider } from "../../crypto/CryptoProvider.js"; import { - API_VERSION_QUERY_PARAMETER_NAME, HttpMethod, - METADATA_HEADER_NAME, ManagedIdentityEnvironmentVariableNames, + ManagedIdentityHeaders, ManagedIdentityIdType, + ManagedIdentityQueryParameters, ManagedIdentitySourceNames, - RESOURCE_BODY_OR_QUERY_PARAMETER_NAME, } from "../../utils/Constants.js"; import { NodeStorage } from "../../cache/NodeStorage.js"; import { ImdsRetryPolicy } from "../../retry/ImdsRetryPolicy.js"; @@ -49,6 +48,10 @@ export class Imds extends BaseManagedIdentitySource { this.identityEndpoint = identityEndpoint; } + public getSourceName(): ManagedIdentitySourceNames { + return ManagedIdentitySourceNames.IMDS; + } + public static tryCreate( logger: Logger, nodeStorage: NodeStorage, @@ -112,11 +115,11 @@ export class Imds extends BaseManagedIdentitySource { this.identityEndpoint ); - request.headers[METADATA_HEADER_NAME] = "true"; + request.headers[ManagedIdentityHeaders.METADATA_HEADER_NAME] = "true"; - request.queryParameters[API_VERSION_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.API_VERSION] = IMDS_API_VERSION; - request.queryParameters[RESOURCE_BODY_OR_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.RESOURCE] = resource; if ( diff --git a/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts b/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts index cd144fa5f1..1e732f99a9 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts @@ -7,13 +7,11 @@ 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, - ML_AND_SF_SECRET_HEADER_NAME, + ManagedIdentityQueryParameters, + ManagedIdentityHeaders, } from "../../utils/Constants.js"; import { CryptoProvider } from "../../crypto/CryptoProvider.js"; import { ManagedIdentityRequestParameters } from "../../config/ManagedIdentityRequestParameters.js"; @@ -47,6 +45,10 @@ export class MachineLearning extends BaseManagedIdentitySource { this.secret = secret; } + public getSourceName(): ManagedIdentitySourceNames { + return ManagedIdentitySourceNames.MACHINE_LEARNING; + } + public static getEnvironmentVariables(): Array { const msiEndpoint: string | undefined = process.env[ManagedIdentityEnvironmentVariableNames.MSI_ENDPOINT]; @@ -107,12 +109,13 @@ export class MachineLearning extends BaseManagedIdentitySource { this.msiEndpoint ); - request.headers[METADATA_HEADER_NAME] = "true"; - request.headers[ML_AND_SF_SECRET_HEADER_NAME] = this.secret; + request.headers[ManagedIdentityHeaders.METADATA_HEADER_NAME] = "true"; + request.headers[ManagedIdentityHeaders.ML_AND_SF_SECRET_HEADER_NAME] = + this.secret; - request.queryParameters[API_VERSION_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.API_VERSION] = MACHINE_LEARNING_MSI_API_VERSION; - request.queryParameters[RESOURCE_BODY_OR_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.RESOURCE] = resource; if ( diff --git a/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts b/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts index 94bc0b8f5a..d63a26343b 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts @@ -10,13 +10,12 @@ import { BaseManagedIdentitySource } from "./BaseManagedIdentitySource.js"; import { NodeStorage } from "../../cache/NodeStorage.js"; import { CryptoProvider } from "../../crypto/CryptoProvider.js"; import { - API_VERSION_QUERY_PARAMETER_NAME, HttpMethod, ManagedIdentityEnvironmentVariableNames, ManagedIdentityIdType, ManagedIdentitySourceNames, - RESOURCE_BODY_OR_QUERY_PARAMETER_NAME, - ML_AND_SF_SECRET_HEADER_NAME, + ManagedIdentityQueryParameters, + ManagedIdentityHeaders, } from "../../utils/Constants.js"; // MSI Constants. Docs for MSI are available here https://docs.microsoft.com/azure/app-service/overview-managed-identity @@ -50,6 +49,10 @@ export class ServiceFabric extends BaseManagedIdentitySource { this.identityHeader = identityHeader; } + public getSourceName(): ManagedIdentitySourceNames { + return ManagedIdentitySourceNames.SERVICE_FABRIC; + } + public static getEnvironmentVariables(): Array { const identityEndpoint: string | undefined = process.env[ @@ -131,11 +134,12 @@ export class ServiceFabric extends BaseManagedIdentitySource { this.identityEndpoint ); - request.headers[ML_AND_SF_SECRET_HEADER_NAME] = this.identityHeader; + request.headers[ManagedIdentityHeaders.ML_AND_SF_SECRET_HEADER_NAME] = + this.identityHeader; - request.queryParameters[API_VERSION_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.API_VERSION] = SERVICE_FABRIC_MSI_API_VERSION; - request.queryParameters[RESOURCE_BODY_OR_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.RESOURCE] = resource; if ( diff --git a/lib/msal-node/src/config/Configuration.ts b/lib/msal-node/src/config/Configuration.ts index b7b7fa12ce..232dbef5bc 100644 --- a/lib/msal-node/src/config/Configuration.ts +++ b/lib/msal-node/src/config/Configuration.ts @@ -129,6 +129,7 @@ export type ManagedIdentityIdParams = { /** @public */ export type ManagedIdentityConfiguration = { + clientCapabilities?: Array; managedIdentityIdParams?: ManagedIdentityIdParams; system?: NodeSystemOptions; }; @@ -240,14 +241,16 @@ export function buildAppConfiguration({ /** @internal */ export type ManagedIdentityNodeConfiguration = { + clientCapabilities?: Array; + disableInternalRetries: boolean; managedIdentityId: ManagedIdentityId; system: Required< Pick >; - disableInternalRetries: boolean; }; export function buildManagedIdentityConfiguration({ + clientCapabilities, managedIdentityIdParams, system, }: ManagedIdentityConfiguration): ManagedIdentityNodeConfiguration { @@ -271,6 +274,7 @@ export function buildManagedIdentityConfiguration({ } return { + clientCapabilities: clientCapabilities || [], managedIdentityId: managedIdentityId, system: { loggerOptions, diff --git a/lib/msal-node/src/request/ManagedIdentityRequest.ts b/lib/msal-node/src/request/ManagedIdentityRequest.ts index a99ae12b3a..343d1dd69d 100644 --- a/lib/msal-node/src/request/ManagedIdentityRequest.ts +++ b/lib/msal-node/src/request/ManagedIdentityRequest.ts @@ -12,4 +12,7 @@ import { ManagedIdentityRequestParams } from "./ManagedIdentityRequestParams.js" * - resource - resource requested to access the protected API. It should be of the form "{ResourceIdUri}" or {ResourceIdUri/.default}. For instance https://management.azure.net or, for Microsoft Graph, https://graph.microsoft.com/.default */ export type ManagedIdentityRequest = ManagedIdentityRequestParams & - CommonClientCredentialRequest; + CommonClientCredentialRequest & { + clientCapabilities?: Array; + accessTokenSha256Hash?: string; + }; diff --git a/lib/msal-node/src/utils/Constants.ts b/lib/msal-node/src/utils/Constants.ts index a094e561bc..0571b1bc03 100644 --- a/lib/msal-node/src/utils/Constants.ts +++ b/lib/msal-node/src/utils/Constants.ts @@ -8,16 +8,34 @@ import { DefaultManagedIdentityRetryPolicy } from "../retry/DefaultManagedIdenti import { ImdsRetryPolicy } from "../retry/ImdsRetryPolicy.js"; // MSI Constants. Docs for MSI are available here https://docs.microsoft.com/azure/app-service/overview-managed-identity -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 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"; export const MANAGED_IDENTITY_DEFAULT_TENANT = "managed_identity"; export const DEFAULT_AUTHORITY_FOR_MANAGED_IDENTITY = `https://login.microsoftonline.com/${MANAGED_IDENTITY_DEFAULT_TENANT}/`; +/** + * Managed Identity Headers - used in network requests + */ +export const ManagedIdentityHeaders = { + AUTHORIZATION_HEADER_NAME: "Authorization", + METADATA_HEADER_NAME: "Metadata", + APP_SERVICE_SECRET_HEADER_NAME: "X-IDENTITY-HEADER", + ML_AND_SF_SECRET_HEADER_NAME: "secret", +} as const; +export type ManagedIdentityHeaders = + (typeof ManagedIdentityHeaders)[keyof typeof ManagedIdentityHeaders]; + +/** + * Managed Identity Query Parameters - used in network requests + */ +export const ManagedIdentityQueryParameters = { + API_VERSION: "api-version", + RESOURCE: "resource", + SHA256_TOKEN_TO_REFRESH: "token_sha256_to_refresh", + XMS_CC: "xms_cc", +} as const; +export type ManagedIdentityQueryParameters = + (typeof ManagedIdentityQueryParameters)[keyof typeof ManagedIdentityQueryParameters]; + /** * Managed Identity Environment Variable Names */ From 73ffd228d87b70465d02ef0ec946037675e76c5e Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 2 Apr 2025 15:15:51 -0400 Subject: [PATCH 02/18] Change files --- ...ure-msal-node-870e01a0-ed8b-4e52-8367-c05200d113c5.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@azure-msal-node-870e01a0-ed8b-4e52-8367-c05200d113c5.json diff --git a/change/@azure-msal-node-870e01a0-ed8b-4e52-8367-c05200d113c5.json b/change/@azure-msal-node-870e01a0-ed8b-4e52-8367-c05200d113c5.json new file mode 100644 index 0000000000..f17e8ac771 --- /dev/null +++ b/change/@azure-msal-node-870e01a0-ed8b-4e52-8367-c05200d113c5.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Added token revocation functionality to Managed Identity's App Service and Service Fabric Sources #7679", + "packageName": "@azure/msal-node", + "email": "rginsburg@microsoft.com", + "dependentChangeType": "patch" +} From f4f002a5804955e1a9937f0f454755bd0ff9732e Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 2 Apr 2025 18:23:34 -0400 Subject: [PATCH 03/18] Improvements --- .../src/client/ManagedIdentityApplication.ts | 31 ++++++++++++++----- .../ManagedIdentitySources/AppService.ts | 4 --- .../client/ManagedIdentitySources/AzureArc.ts | 4 --- .../BaseManagedIdentitySource.ts | 13 ++------ .../ManagedIdentitySources/CloudShell.ts | 4 --- .../src/client/ManagedIdentitySources/Imds.ts | 4 --- .../ManagedIdentitySources/MachineLearning.ts | 4 --- .../ManagedIdentitySources/ServiceFabric.ts | 4 --- 8 files changed, 26 insertions(+), 42 deletions(-) diff --git a/lib/msal-node/src/client/ManagedIdentityApplication.ts b/lib/msal-node/src/client/ManagedIdentityApplication.ts index 7586f08f44..65fe91edab 100644 --- a/lib/msal-node/src/client/ManagedIdentityApplication.ts +++ b/lib/msal-node/src/client/ManagedIdentityApplication.ts @@ -36,6 +36,12 @@ import { ManagedIdentitySourceNames, } from "../utils/Constants.js"; +const SOURCES_THAT_SUPPORT_TOKEN_REVOCATION: Array = + [ + ManagedIdentitySourceNames.APP_SERVICE, + ManagedIdentitySourceNames.SERVICE_FABRIC, + ]; + /** * Class to initialize a managed identity and identify the service * @public @@ -166,15 +172,26 @@ export class ManagedIdentityApplication { if (cachedAuthenticationResult) { /* * Check if claims are present in the managed identity request. - * If they are, hash the access token and add it to the request. + * If so, the cached token will not be used. */ if (managedIdentityRequest.claims) { - const accessTokenSha256Hash: string = - await this.cryptoProvider.hashString( - cachedAuthenticationResult.accessToken - ); - managedIdentityRequest.accessTokenSha256Hash = - accessTokenSha256Hash; + const sourceName: ManagedIdentitySourceNames = + this.managedIdentityClient.getManagedIdentitySource(); + + /* + * Check if the Managed Identity source supports token revocation. + * If so, hash the cached access token and add it to the request. + */ + if ( + SOURCES_THAT_SUPPORT_TOKEN_REVOCATION.includes(sourceName) + ) { + const accessTokenSha256Hash: string = + await this.cryptoProvider.hashString( + cachedAuthenticationResult.accessToken + ); + managedIdentityRequest.accessTokenSha256Hash = + accessTokenSha256Hash; + } return this.managedIdentityClient.sendManagedIdentityTokenRequest( managedIdentityRequest, diff --git a/lib/msal-node/src/client/ManagedIdentitySources/AppService.ts b/lib/msal-node/src/client/ManagedIdentitySources/AppService.ts index e474c9eefd..d59eb5ac05 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/AppService.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/AppService.ts @@ -49,10 +49,6 @@ export class AppService extends BaseManagedIdentitySource { this.identityHeader = identityHeader; } - public getSourceName(): ManagedIdentitySourceNames { - return ManagedIdentitySourceNames.APP_SERVICE; - } - public static getEnvironmentVariables(): Array { const identityEndpoint: string | undefined = process.env[ diff --git a/lib/msal-node/src/client/ManagedIdentitySources/AzureArc.ts b/lib/msal-node/src/client/ManagedIdentitySources/AzureArc.ts index e337b07dd5..ece2c5379b 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/AzureArc.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/AzureArc.ts @@ -86,10 +86,6 @@ export class AzureArc extends BaseManagedIdentitySource { this.identityEndpoint = identityEndpoint; } - public getSourceName(): ManagedIdentitySourceNames { - return ManagedIdentitySourceNames.AZURE_ARC; - } - public static getEnvironmentVariables(): Array { let identityEndpoint: string | undefined = process.env[ diff --git a/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts b/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts index 5a7114202a..67147a8512 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts @@ -28,7 +28,6 @@ import { HttpMethod, ManagedIdentityIdType, ManagedIdentityQueryParameters, - ManagedIdentitySourceNames, } from "../../utils/Constants.js"; import { ManagedIdentityTokenResponse } from "../../response/ManagedIdentityTokenResponse.js"; import { NodeStorage } from "../../cache/NodeStorage.js"; @@ -78,8 +77,6 @@ export abstract class BaseManagedIdentitySource { managedIdentityId: ManagedIdentityId ): ManagedIdentityRequestParameters; - abstract getSourceName(): ManagedIdentitySourceNames; - public async getServerTokenResponseAsync( response: NetworkResponse, // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -153,13 +150,7 @@ export abstract class BaseManagedIdentitySource { managedIdentityId ); - const sourceName: ManagedIdentitySourceNames = this.getSourceName(); - if ( - (sourceName === ManagedIdentitySourceNames.APP_SERVICE || - sourceName === ManagedIdentitySourceNames.SERVICE_FABRIC) && - managedIdentityRequest.claims && - managedIdentityRequest.accessTokenSha256Hash - ) { + if (managedIdentityRequest.accessTokenSha256Hash) { this.logger.info( `[Managed Identity] The following claims are present in the request: ${managedIdentityRequest.claims}` ); @@ -174,7 +165,7 @@ export abstract class BaseManagedIdentitySource { managedIdentityRequest.clientCapabilities.toString(); this.logger.info( - `[Managed Identity] The following claims are present in the request: ${clientCapabilities}` + `[Managed Identity] The following client capabilities are present in the request: ${clientCapabilities}` ); networkRequest.queryParameters[ diff --git a/lib/msal-node/src/client/ManagedIdentitySources/CloudShell.ts b/lib/msal-node/src/client/ManagedIdentitySources/CloudShell.ts index 7440138a17..38db56978f 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/CloudShell.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/CloudShell.ts @@ -47,10 +47,6 @@ export class CloudShell extends BaseManagedIdentitySource { this.msiEndpoint = msiEndpoint; } - public getSourceName(): ManagedIdentitySourceNames { - return ManagedIdentitySourceNames.CLOUD_SHELL; - } - public static getEnvironmentVariables(): Array { const msiEndpoint: string | undefined = process.env[ManagedIdentityEnvironmentVariableNames.MSI_ENDPOINT]; diff --git a/lib/msal-node/src/client/ManagedIdentitySources/Imds.ts b/lib/msal-node/src/client/ManagedIdentitySources/Imds.ts index b2affffa3e..c24021d0d9 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/Imds.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/Imds.ts @@ -48,10 +48,6 @@ export class Imds extends BaseManagedIdentitySource { this.identityEndpoint = identityEndpoint; } - public getSourceName(): ManagedIdentitySourceNames { - return ManagedIdentitySourceNames.IMDS; - } - public static tryCreate( logger: Logger, nodeStorage: NodeStorage, diff --git a/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts b/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts index 1e732f99a9..c41f5796e0 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts @@ -45,10 +45,6 @@ export class MachineLearning extends BaseManagedIdentitySource { this.secret = secret; } - public getSourceName(): ManagedIdentitySourceNames { - return ManagedIdentitySourceNames.MACHINE_LEARNING; - } - public static getEnvironmentVariables(): Array { const msiEndpoint: string | undefined = process.env[ManagedIdentityEnvironmentVariableNames.MSI_ENDPOINT]; diff --git a/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts b/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts index d63a26343b..9b2af42cb4 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts @@ -49,10 +49,6 @@ export class ServiceFabric extends BaseManagedIdentitySource { this.identityHeader = identityHeader; } - public getSourceName(): ManagedIdentitySourceNames { - return ManagedIdentitySourceNames.SERVICE_FABRIC; - } - public static getEnvironmentVariables(): Array { const identityEndpoint: string | undefined = process.env[ From 468de01ff687c3d5e43e0b0cfe00346fe75242e5 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 2 Apr 2025 18:47:57 -0400 Subject: [PATCH 04/18] Improvements --- .../src/client/ManagedIdentityApplication.ts | 91 ++++++++++++------- 1 file changed, 58 insertions(+), 33 deletions(-) diff --git a/lib/msal-node/src/client/ManagedIdentityApplication.ts b/lib/msal-node/src/client/ManagedIdentityApplication.ts index 65fe91edab..0cb24701a1 100644 --- a/lib/msal-node/src/client/ManagedIdentityApplication.ts +++ b/lib/msal-node/src/client/ManagedIdentityApplication.ts @@ -35,6 +35,7 @@ import { DEFAULT_AUTHORITY_FOR_MANAGED_IDENTITY, ManagedIdentitySourceNames, } from "../utils/Constants.js"; +import { ManagedIdentityId } from "../config/ManagedIdentityId.js"; const SOURCES_THAT_SUPPORT_TOKEN_REVOCATION: Array = [ @@ -152,8 +153,7 @@ export class ManagedIdentityApplication { }; if (managedIdentityRequest.forceRefresh) { - // make a network call to the managed identity source - return this.managedIdentityClient.sendManagedIdentityTokenRequest( + return this.acquireTokenFromManagedIdentity( managedIdentityRequest, this.config.managedIdentityId, this.fakeAuthority @@ -169,46 +169,47 @@ export class ManagedIdentityApplication { ManagedIdentityApplication.nodeStorage as NodeStorage ); - if (cachedAuthenticationResult) { + /* + * Check if claims are present in the managed identity request. + * If so, the cached token will not be used. + */ + if (managedIdentityRequest.claims) { + const sourceName: ManagedIdentitySourceNames = + this.managedIdentityClient.getManagedIdentitySource(); + /* - * Check if claims are present in the managed identity request. - * If so, the cached token will not be used. + * Check if there is a cached token and if the Managed Identity source supports token revocation. + * If so, hash the cached access token and add it to the request. */ - if (managedIdentityRequest.claims) { - const sourceName: ManagedIdentitySourceNames = - this.managedIdentityClient.getManagedIdentitySource(); - - /* - * Check if the Managed Identity source supports token revocation. - * If so, hash the cached access token and add it to the request. - */ - if ( - SOURCES_THAT_SUPPORT_TOKEN_REVOCATION.includes(sourceName) - ) { - const accessTokenSha256Hash: string = - await this.cryptoProvider.hashString( - cachedAuthenticationResult.accessToken - ); - managedIdentityRequest.accessTokenSha256Hash = - accessTokenSha256Hash; - } - - return this.managedIdentityClient.sendManagedIdentityTokenRequest( - managedIdentityRequest, - this.config.managedIdentityId, - this.fakeAuthority - ); + if ( + cachedAuthenticationResult && + SOURCES_THAT_SUPPORT_TOKEN_REVOCATION.includes(sourceName) + ) { + const accessTokenSha256Hash: string = + await this.cryptoProvider.hashString( + cachedAuthenticationResult.accessToken + ); + managedIdentityRequest.accessTokenSha256Hash = + accessTokenSha256Hash; } + return this.acquireTokenFromManagedIdentity( + managedIdentityRequest, + this.config.managedIdentityId, + this.fakeAuthority + ); + } + + if (cachedAuthenticationResult) { // if the token is not expired but must be refreshed; get a new one in the background if (lastCacheOutcome === CacheOutcome.PROACTIVELY_REFRESHED) { this.logger.info( "ClientCredentialClient:getCachedAuthenticationResult - Cached access token's refreshOn property has been exceeded'. It's not expired, but must be refreshed." ); - // make a network call to the managed identity source; refresh the access token in the background + // force refresh; will run in the background const refreshAccessToken = true; - return this.managedIdentityClient.sendManagedIdentityTokenRequest( + await this.acquireTokenFromManagedIdentity( managedIdentityRequest, this.config.managedIdentityId, this.fakeAuthority, @@ -218,8 +219,7 @@ export class ManagedIdentityApplication { return cachedAuthenticationResult; } else { - // make a network call to the managed identity source - return this.managedIdentityClient.sendManagedIdentityTokenRequest( + return this.acquireTokenFromManagedIdentity( managedIdentityRequest, this.config.managedIdentityId, this.fakeAuthority @@ -227,6 +227,31 @@ export class ManagedIdentityApplication { } } + /** + * Acquires a token from a managed identity endpoint. + * + * @param managedIdentityRequest - The request object containing parameters for the managed identity token request. + * @param managedIdentityId - The identifier for the managed identity (e.g., client ID or resource ID). + * @param fakeAuthority - A placeholder authority used for the token request. + * @param refreshAccessToken - Optional flag indicating whether to force a refresh of the access token. + * @returns A promise that resolves to an {@link AuthenticationResult} containing the acquired token and related information. + * @throws {@link AuthenticationError} if the token acquisition fails. + */ + private async acquireTokenFromManagedIdentity( + managedIdentityRequest: ManagedIdentityRequest, + managedIdentityId: ManagedIdentityId, + fakeAuthority: Authority, + refreshAccessToken?: boolean + ): Promise { + // make a network call to the managed identity + return this.managedIdentityClient.sendManagedIdentityTokenRequest( + managedIdentityRequest, + managedIdentityId, + fakeAuthority, + refreshAccessToken + ); + } + /** * Determine the Managed Identity Source based on available environment variables. This API is consumed by Azure Identity SDK. * @returns ManagedIdentitySourceNames - The Managed Identity source's name From dcd382faf1f2118be89d1560d49111063bde857f Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 3 Apr 2025 18:07:22 -0400 Subject: [PATCH 05/18] Added unit tests --- .../ManagedIdentitySources/AppService.spec.ts | 57 +++++++++++++++++++ .../ManagedIdentitySources/Imds.spec.ts | 20 +++++-- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts index ed9679f00a..02cf6bae07 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts @@ -10,6 +10,7 @@ import { 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 +29,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 +178,61 @@ describe("Acquires a token successfully via an App Service Managed Identity", () }); }); + describe("Miscellaneous", () => { + let managedIdentityApplication: ManagedIdentityApplication; + beforeEach(() => { + managedIdentityApplication = new ManagedIdentityApplication( + systemAssignedConfig + ); + expect(managedIdentityApplication.getManagedIdentitySource()).toBe( + ManagedIdentitySourceNames.APP_SERVICE + ); + }); + + test("ignores a cached token when claims are provided, and the Managed Identity does support token revocation, and ensures the token revocation query parameter token_sha256_to_refresh was included in the network request to the Managed Identity", async () => { + const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn( + networkClient, + "sendGetRequestAsync" + ); + + 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 + ); + + 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 + ); + + 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 url: URLSearchParams = new URLSearchParams( + sendGetRequestAsyncSpy.mock.lastCall[0] + ); + expect( + url.has(ManagedIdentityQueryParameters.SHA256_TOKEN_TO_REFRESH) + ).toBe(true); + }); + }); + describe("Errors", () => { test("ensures that the error format is correct", async () => { const managedIdentityNetworkErrorClient400 = diff --git a/lib/msal-node/test/client/ManagedIdentitySources/Imds.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/Imds.spec.ts index 935b95feb9..814120fd5a 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/Imds.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/Imds.spec.ts @@ -34,6 +34,7 @@ import { } from "../../test_kit/ManagedIdentityTestUtils.js"; import { DEFAULT_MANAGED_IDENTITY_ID, + ManagedIdentityQueryParameters, ManagedIdentitySourceNames, } from "../../../src/utils/Constants.js"; import { @@ -673,13 +674,17 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { ); }); - test("ignores a cached token when claims are provided", async () => { + test("ignores a cached token when claims are provided and the Managed Identity does not support token revocation, and ensures the token revocation query parameter token_sha256_to_refresh was not included in the network request to the Managed Identity", async () => { + const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn( + networkClient, + "sendGetRequestAsync" + ); + let networkManagedIdentityResult: AuthenticationResult = await systemAssignedManagedIdentityApplication.acquireToken({ resource: MANAGED_IDENTITY_RESOURCE, }); expect(networkManagedIdentityResult.fromCache).toBe(false); - expect(networkManagedIdentityResult.accessToken).toEqual( DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken ); @@ -702,6 +707,14 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { expect(networkManagedIdentityResult.accessToken).toEqual( DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken ); + + expect(sendGetRequestAsyncSpy.mock.calls.length).toEqual(2); + const url: URLSearchParams = new URLSearchParams( + sendGetRequestAsyncSpy.mock.lastCall[0] + ); + expect( + url.has(ManagedIdentityQueryParameters.SHA256_TOKEN_TO_REFRESH) + ).toBe(false); }); test("ignores a cached token when forceRefresh is set to true", async () => { @@ -710,7 +723,6 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { resource: MANAGED_IDENTITY_RESOURCE, }); expect(networkManagedIdentityResult.fromCache).toBe(false); - expect(networkManagedIdentityResult.accessToken).toEqual( DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken ); @@ -730,7 +742,6 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { resource: MANAGED_IDENTITY_RESOURCE, }); expect(networkManagedIdentityResult.fromCache).toBe(false); - expect(networkManagedIdentityResult.accessToken).toEqual( DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken ); @@ -742,7 +753,6 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { resource: MANAGED_IDENTITY_RESOURCE, }); expect(networkManagedIdentityResult.fromCache).toBe(false); - expect(networkManagedIdentityResult.accessToken).toEqual( DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken ); From b09a464c5968e7df8f1c042811a22e4fcc9b7218 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 3 Apr 2025 18:46:36 -0400 Subject: [PATCH 06/18] Implemented some github feedback --- lib/msal-node/src/client/ManagedIdentityApplication.ts | 6 +++--- .../ManagedIdentitySources/BaseManagedIdentitySource.ts | 4 ++-- lib/msal-node/src/request/ManagedIdentityRequest.ts | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/msal-node/src/client/ManagedIdentityApplication.ts b/lib/msal-node/src/client/ManagedIdentityApplication.ts index 0cb24701a1..9c50324d28 100644 --- a/lib/msal-node/src/client/ManagedIdentityApplication.ts +++ b/lib/msal-node/src/client/ManagedIdentityApplication.ts @@ -185,12 +185,12 @@ export class ManagedIdentityApplication { cachedAuthenticationResult && SOURCES_THAT_SUPPORT_TOKEN_REVOCATION.includes(sourceName) ) { - const accessTokenSha256Hash: string = + const revokedTokenSha256Hash: string = await this.cryptoProvider.hashString( cachedAuthenticationResult.accessToken ); - managedIdentityRequest.accessTokenSha256Hash = - accessTokenSha256Hash; + managedIdentityRequest.revokedTokenSha256Hash = + revokedTokenSha256Hash; } return this.acquireTokenFromManagedIdentity( diff --git a/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts b/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts index 67147a8512..1164ceb375 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts @@ -150,14 +150,14 @@ export abstract class BaseManagedIdentitySource { managedIdentityId ); - if (managedIdentityRequest.accessTokenSha256Hash) { + if (managedIdentityRequest.revokedTokenSha256Hash) { this.logger.info( `[Managed Identity] The following claims are present in the request: ${managedIdentityRequest.claims}` ); networkRequest.queryParameters[ ManagedIdentityQueryParameters.SHA256_TOKEN_TO_REFRESH - ] = managedIdentityRequest.accessTokenSha256Hash; + ] = managedIdentityRequest.revokedTokenSha256Hash; } if (managedIdentityRequest.clientCapabilities?.length) { diff --git a/lib/msal-node/src/request/ManagedIdentityRequest.ts b/lib/msal-node/src/request/ManagedIdentityRequest.ts index 343d1dd69d..9ad7d18b74 100644 --- a/lib/msal-node/src/request/ManagedIdentityRequest.ts +++ b/lib/msal-node/src/request/ManagedIdentityRequest.ts @@ -8,11 +8,11 @@ import { ManagedIdentityRequestParams } from "./ManagedIdentityRequestParams.js" /** * ManagedIdentityRequest - * - forceRefresh - forces managed identity requests to skip the cache and make network calls if true - * - resource - resource requested to access the protected API. It should be of the form "{ResourceIdUri}" or {ResourceIdUri/.default}. For instance https://management.azure.net or, for Microsoft Graph, https://graph.microsoft.com/.default + * - clientCapabilities - an array of capabilities to be added to all network requests as part of the `xms_cc` claims request + * - revokedTokenSha256Hash - a SHA256 hash of the token to be revoked. The managed identity will revoke the token based on a SHA256 hash of the token, not the token itself. This is to prevent the token from being leaked in transit. */ export type ManagedIdentityRequest = ManagedIdentityRequestParams & CommonClientCredentialRequest & { clientCapabilities?: Array; - accessTokenSha256Hash?: string; + revokedTokenSha256Hash?: string; }; From 0f7b2b9038f2afa3715bd33013bc623eef6efb79 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 3 Apr 2025 19:38:37 -0400 Subject: [PATCH 07/18] Implemented github feedback --- .../ConfidentialClientApplication.spec.ts | 2 +- .../ManagedIdentitySources/AppService.spec.ts | 48 ++++++++++++------- .../ManagedIdentitySources/Imds.spec.ts | 32 ++++++++++--- 3 files changed, 58 insertions(+), 24 deletions(-) diff --git a/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts b/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts index 6461e73927..e4fc599f1c 100644 --- a/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts +++ b/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts @@ -137,7 +137,7 @@ describe("ConfidentialClientApplication", () => { const config: Configuration = await ClientTestUtils.createTestConfidentialClientConfiguration( - ["cp1", "cp2"], + CAE_CONSTANTS.CLIENT_CAPABILITIES, mockNetworkClient( {}, // not needed CONFIDENTIAL_CLIENT_AUTHENTICATION_RESULT diff --git a/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts index 02cf6bae07..f316f9b894 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts @@ -5,6 +5,7 @@ import { ManagedIdentityApplication } from "../../../src/client/ManagedIdentityApplication.js"; import { + CAE_CONSTANTS, DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, MANAGED_IDENTITY_APP_SERVICE_NETWORK_REQUEST_400_ERROR, @@ -33,13 +34,17 @@ import { ManagedIdentitySourceNames, } from "../../../src/utils/Constants.js"; import { ManagedIdentityUserAssignedIdQueryParameterNames } from "../../../src/client/ManagedIdentitySources/BaseManagedIdentitySource.js"; +import { CryptoProvider } from "../../../src/index.js"; describe("Acquires a token successfully via an App Service Managed Identity", () => { + let cryptoProvider: CryptoProvider; beforeAll(() => { process.env[ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT] = "fake_IDENTITY_ENDPOINT"; process.env[ManagedIdentityEnvironmentVariableNames.IDENTITY_HEADER] = "fake_IDENTITY_HEADER"; + + cryptoProvider = new CryptoProvider(); }); afterAll(() => { @@ -179,22 +184,18 @@ describe("Acquires a token successfully via an App Service Managed Identity", () }); describe("Miscellaneous", () => { - let managedIdentityApplication: ManagedIdentityApplication; - beforeEach(() => { - managedIdentityApplication = new ManagedIdentityApplication( - systemAssignedConfig - ); - expect(managedIdentityApplication.getManagedIdentitySource()).toBe( - ManagedIdentitySourceNames.APP_SERVICE - ); - }); - - test("ignores a cached token when claims are provided, and the Managed Identity does support token revocation, and ensures the token revocation query parameter token_sha256_to_refresh was included in the network request to the Managed Identity", async () => { + test("ignores a cached token when claims are provided and the Managed Identity does support token revocation, and ensures the token revocation query parameter token_sha256_to_refresh was included in the network request to the Managed Identity", async () => { const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn( networkClient, "sendGetRequestAsync" ); + const managedIdentityApplication: ManagedIdentityApplication = + new ManagedIdentityApplication({ + ...systemAssignedConfig, + clientCapabilities: CAE_CONSTANTS.CLIENT_CAPABILITIES, + }); + let networkManagedIdentityResult: AuthenticationResult = await managedIdentityApplication.acquireToken({ resource: MANAGED_IDENTITY_RESOURCE, @@ -204,6 +205,15 @@ describe("Acquires a token successfully via an App Service Managed Identity", () 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(CAE_CONSTANTS.CLIENT_CAPABILITIES.toString()); + const cachedManagedIdentityResult: AuthenticationResult = await managedIdentityApplication.acquireToken({ resource: MANAGED_IDENTITY_RESOURCE, @@ -212,6 +222,7 @@ describe("Acquires a token successfully via an App Service Managed Identity", () expect(cachedManagedIdentityResult.accessToken).toEqual( DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken ); + expect(sendGetRequestAsyncSpy.mock.calls.length).toEqual(1); networkManagedIdentityResult = await managedIdentityApplication.acquireToken({ @@ -224,12 +235,17 @@ describe("Acquires a token successfully via an App Service Managed Identity", () ); expect(sendGetRequestAsyncSpy.mock.calls.length).toEqual(2); - const url: URLSearchParams = new URLSearchParams( - sendGetRequestAsyncSpy.mock.lastCall[0] - ); + const secondNetworkRequestUrlParams: URLSearchParams = + new URLSearchParams(sendGetRequestAsyncSpy.mock.lastCall[0]); expect( - url.has(ManagedIdentityQueryParameters.SHA256_TOKEN_TO_REFRESH) - ).toBe(true); + secondNetworkRequestUrlParams.get( + ManagedIdentityQueryParameters.SHA256_TOKEN_TO_REFRESH + ) + ).toEqual( + await cryptoProvider.hashString( + DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken + ) + ); }); }); diff --git a/lib/msal-node/test/client/ManagedIdentitySources/Imds.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/Imds.spec.ts index 814120fd5a..ec78cfccc1 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/Imds.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/Imds.spec.ts @@ -6,6 +6,7 @@ import { ManagedIdentityApplication } from "../../../src/client/ManagedIdentityApplication.js"; import { ManagedIdentityConfiguration } from "../../../src/config/Configuration.js"; import { + CAE_CONSTANTS, DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS, @@ -680,8 +681,14 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { "sendGetRequestAsync" ); + const managedIdentityApplication: ManagedIdentityApplication = + new ManagedIdentityApplication({ + ...systemAssignedConfig, + clientCapabilities: CAE_CONSTANTS.CLIENT_CAPABILITIES, + }); + let networkManagedIdentityResult: AuthenticationResult = - await systemAssignedManagedIdentityApplication.acquireToken({ + await managedIdentityApplication.acquireToken({ resource: MANAGED_IDENTITY_RESOURCE, }); expect(networkManagedIdentityResult.fromCache).toBe(false); @@ -689,17 +696,27 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { 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(CAE_CONSTANTS.CLIENT_CAPABILITIES.toString()); + const cachedManagedIdentityResult: AuthenticationResult = - await systemAssignedManagedIdentityApplication.acquireToken({ + 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 systemAssignedManagedIdentityApplication.acquireToken({ + await managedIdentityApplication.acquireToken({ claims: TEST_CONFIG.CLAIMS, resource: MANAGED_IDENTITY_RESOURCE, }); @@ -709,11 +726,12 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { ); expect(sendGetRequestAsyncSpy.mock.calls.length).toEqual(2); - const url: URLSearchParams = new URLSearchParams( - sendGetRequestAsyncSpy.mock.lastCall[0] - ); + const secondNetworkRequestUrlParams: URLSearchParams = + new URLSearchParams(sendGetRequestAsyncSpy.mock.lastCall[0]); expect( - url.has(ManagedIdentityQueryParameters.SHA256_TOKEN_TO_REFRESH) + secondNetworkRequestUrlParams.has( + ManagedIdentityQueryParameters.SHA256_TOKEN_TO_REFRESH + ) ).toBe(false); }); From b32f45e354be67b1016136bf37ab2a57f801f9b3 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 3 Apr 2025 19:48:33 -0400 Subject: [PATCH 08/18] package-lock --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e4b0e557bc..5e02d4d91c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44259,9 +44259,9 @@ } }, "node_modules/vite": { - "version": "4.5.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.11.tgz", - "integrity": "sha512-4mVdhLkZ0vpqZLGJhNm+X1n7juqXApEMGlUXcOQawA45UmpxivOYaMBkI/Js3FlBsNA8hCgEnX5X04moFitSGw==", + "version": "4.5.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.12.tgz", + "integrity": "sha512-qrMwavANtSz91nDy3zEiUHMtL09x0mniQsSMvDkNxuCBM1W5vriJ22hEmwTth6DhLSWsZnHBT0yHFAQXt6efGA==", "dev": true, "license": "MIT", "peer": true, From 604194d33895c2983bcdee4d63f52ef1c3871736 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 4 Apr 2025 15:11:40 -0400 Subject: [PATCH 09/18] Defined sha256 hash for test access token --- .../client/ManagedIdentitySources/AppService.spec.ts | 11 ++--------- lib/msal-node/test/test_kit/StringConstants.ts | 4 ++++ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts index f316f9b894..a43dda131d 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts @@ -6,6 +6,7 @@ import { ManagedIdentityApplication } from "../../../src/client/ManagedIdentityApplication.js"; import { CAE_CONSTANTS, + DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT_ACCESS_TOKEN_SHA256_HASH, DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, MANAGED_IDENTITY_APP_SERVICE_NETWORK_REQUEST_400_ERROR, @@ -34,17 +35,13 @@ import { ManagedIdentitySourceNames, } from "../../../src/utils/Constants.js"; import { ManagedIdentityUserAssignedIdQueryParameterNames } from "../../../src/client/ManagedIdentitySources/BaseManagedIdentitySource.js"; -import { CryptoProvider } from "../../../src/index.js"; describe("Acquires a token successfully via an App Service Managed Identity", () => { - let cryptoProvider: CryptoProvider; beforeAll(() => { process.env[ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT] = "fake_IDENTITY_ENDPOINT"; process.env[ManagedIdentityEnvironmentVariableNames.IDENTITY_HEADER] = "fake_IDENTITY_HEADER"; - - cryptoProvider = new CryptoProvider(); }); afterAll(() => { @@ -133,8 +130,6 @@ describe("Acquires a token successfully via an App Service Managed Identity", () ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_RESOURCE_ID_NON_IMDS ) ).toEqual(MANAGED_IDENTITY_RESOURCE_ID); - - jest.restoreAllMocks(); }); }); @@ -242,9 +237,7 @@ describe("Acquires a token successfully via an App Service Managed Identity", () ManagedIdentityQueryParameters.SHA256_TOKEN_TO_REFRESH ) ).toEqual( - await cryptoProvider.hashString( - DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken - ) + DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT_ACCESS_TOKEN_SHA256_HASH ); }); }); diff --git a/lib/msal-node/test/test_kit/StringConstants.ts b/lib/msal-node/test/test_kit/StringConstants.ts index bd003a7b47..401d858a49 100644 --- a/lib/msal-node/test/test_kit/StringConstants.ts +++ b/lib/msal-node/test/test_kit/StringConstants.ts @@ -446,6 +446,10 @@ export const getCacheKey = (resource?: string): string => { return `-${Constants.DEFAULT_AUTHORITY_HOST}-accesstoken-${resourceHelper}-managed_identity-${MANAGED_IDENTITY_RESOURCE_BASE}--`; }; +// SHA256 hash of the DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken +export const DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT_ACCESS_TOKEN_SHA256_HASH = + "2WeMMslunTWMTmHVsjAHTATwN0BUEXQNTo2RIwZjQa8"; + export const DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT: Omit< AuthenticationResult, "correlationId" From 3405bce9fc901ec9db3a18323cbb83c383b02a07 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Mon, 7 Apr 2025 11:53:04 -0400 Subject: [PATCH 10/18] Switched to hex encoding, instead of base64, for the sha256 hash --- lib/msal-common/apiReview/msal-common.api.md | 11 +++++++++++ lib/msal-common/src/exports-common.ts | 1 + lib/msal-common/src/utils/Constants.ts | 5 +++++ .../src/client/ManagedIdentityApplication.ts | 16 ++++++++++------ .../ManagedIdentitySources/AppService.spec.ts | 4 ++-- lib/msal-node/test/test_kit/StringConstants.ts | 4 ++-- 6 files changed, 31 insertions(+), 10 deletions(-) diff --git a/lib/msal-common/apiReview/msal-common.api.md b/lib/msal-common/apiReview/msal-common.api.md index ec761913ed..ffcf217879 100644 --- a/lib/msal-common/apiReview/msal-common.api.md +++ b/lib/msal-common/apiReview/msal-common.api.md @@ -2215,6 +2215,17 @@ const emptyInputScopesError = "empty_input_scopes_error"; // @public (undocumented) const emptyInputScopeSet = "empty_input_scopeset"; +// Warning: (ae-missing-release-tag) "EncodingTypes" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "EncodingTypes" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const EncodingTypes: { + readonly HEX: "hex"; +}; + +// @public (undocumented) +export type EncodingTypes = (typeof EncodingTypes)[keyof typeof EncodingTypes]; + // Warning: (ae-missing-release-tag) "endpointResolutionError" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/lib/msal-common/src/exports-common.ts b/lib/msal-common/src/exports-common.ts index 5700a50c40..6bf3a78ef1 100644 --- a/lib/msal-common/src/exports-common.ts +++ b/lib/msal-common/src/exports-common.ts @@ -181,6 +181,7 @@ export { HttpStatus, DEFAULT_TOKEN_RENEWAL_OFFSET_SEC, JsonWebTokenTypes, + EncodingTypes, } from "./utils/Constants.js"; export { StringUtils } from "./utils/StringUtils.js"; export { StringDict } from "./utils/MsalTypes.js"; diff --git a/lib/msal-common/src/utils/Constants.ts b/lib/msal-common/src/utils/Constants.ts index a358f71ba5..ba2b8dd95c 100644 --- a/lib/msal-common/src/utils/Constants.ts +++ b/lib/msal-common/src/utils/Constants.ts @@ -389,3 +389,8 @@ export const ONE_DAY_IN_MS = 86400000; // Token renewal offset default in seconds export const DEFAULT_TOKEN_RENEWAL_OFFSET_SEC = 300; + +export const EncodingTypes = { + HEX: "hex", +} as const; +export type EncodingTypes = (typeof EncodingTypes)[keyof typeof EncodingTypes]; diff --git a/lib/msal-node/src/client/ManagedIdentityApplication.ts b/lib/msal-node/src/client/ManagedIdentityApplication.ts index 9c50324d28..5e9a95355d 100644 --- a/lib/msal-node/src/client/ManagedIdentityApplication.ts +++ b/lib/msal-node/src/client/ManagedIdentityApplication.ts @@ -18,6 +18,7 @@ import { AuthenticationResult, createClientConfigurationError, ClientConfigurationErrorCodes, + EncodingTypes, } from "@azure/msal-common/node"; import { ManagedIdentityConfiguration, @@ -36,6 +37,7 @@ import { ManagedIdentitySourceNames, } from "../utils/Constants.js"; import { ManagedIdentityId } from "../config/ManagedIdentityId.js"; +import { HashUtils } from "../crypto/HashUtils.js"; const SOURCES_THAT_SUPPORT_TOKEN_REVOCATION: Array = [ @@ -63,6 +65,8 @@ export class ManagedIdentityApplication { private managedIdentityClient: ManagedIdentityClient; + private hashUtils: HashUtils; + constructor(configuration?: ManagedIdentityConfiguration) { // undefined config means the managed identity is system-assigned this.config = buildManagedIdentityConfiguration(configuration || {}); @@ -121,6 +125,8 @@ export class ManagedIdentityApplication { this.cryptoProvider, this.config.disableInternalRetries ); + + this.hashUtils = new HashUtils(); } /** @@ -185,10 +191,9 @@ export class ManagedIdentityApplication { cachedAuthenticationResult && SOURCES_THAT_SUPPORT_TOKEN_REVOCATION.includes(sourceName) ) { - const revokedTokenSha256Hash: string = - await this.cryptoProvider.hashString( - cachedAuthenticationResult.accessToken - ); + const revokedTokenSha256Hash: string = this.hashUtils + .sha256(cachedAuthenticationResult.accessToken) + .toString(EncodingTypes.HEX); managedIdentityRequest.revokedTokenSha256Hash = revokedTokenSha256Hash; } @@ -234,8 +239,7 @@ export class ManagedIdentityApplication { * @param managedIdentityId - The identifier for the managed identity (e.g., client ID or resource ID). * @param fakeAuthority - A placeholder authority used for the token request. * @param refreshAccessToken - Optional flag indicating whether to force a refresh of the access token. - * @returns A promise that resolves to an {@link AuthenticationResult} containing the acquired token and related information. - * @throws {@link AuthenticationError} if the token acquisition fails. + * @returns A promise that resolves to an AuthenticationResult containing the acquired token and related information. */ private async acquireTokenFromManagedIdentity( managedIdentityRequest: ManagedIdentityRequest, diff --git a/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts index a43dda131d..ba73ac413f 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts @@ -6,7 +6,7 @@ import { ManagedIdentityApplication } from "../../../src/client/ManagedIdentityApplication.js"; import { CAE_CONSTANTS, - DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT_ACCESS_TOKEN_SHA256_HASH, + 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, @@ -237,7 +237,7 @@ describe("Acquires a token successfully via an App Service Managed Identity", () ManagedIdentityQueryParameters.SHA256_TOKEN_TO_REFRESH ) ).toEqual( - DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT_ACCESS_TOKEN_SHA256_HASH + DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT_ACCESS_TOKEN_SHA256_HASH_IN_HEX ); }); }); diff --git a/lib/msal-node/test/test_kit/StringConstants.ts b/lib/msal-node/test/test_kit/StringConstants.ts index 401d858a49..85b205599e 100644 --- a/lib/msal-node/test/test_kit/StringConstants.ts +++ b/lib/msal-node/test/test_kit/StringConstants.ts @@ -447,8 +447,8 @@ export const getCacheKey = (resource?: string): string => { }; // SHA256 hash of the DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken -export const DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT_ACCESS_TOKEN_SHA256_HASH = - "2WeMMslunTWMTmHVsjAHTATwN0BUEXQNTo2RIwZjQa8"; +export const DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT_ACCESS_TOKEN_SHA256_HASH_IN_HEX = + "d9678c32c96e9d358c4e61d5b230074c04f037405411740d4e8d9123066341af"; export const DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT: Omit< AuthenticationResult, From f1d096f77cd0742488ad6df9e1669e37cf544988 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Mon, 7 Apr 2025 11:53:21 -0400 Subject: [PATCH 11/18] Change files --- ...e-msal-common-1ec481cb-c572-4e49-b1e0-37fb0097e025.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@azure-msal-common-1ec481cb-c572-4e49-b1e0-37fb0097e025.json diff --git a/change/@azure-msal-common-1ec481cb-c572-4e49-b1e0-37fb0097e025.json b/change/@azure-msal-common-1ec481cb-c572-4e49-b1e0-37fb0097e025.json new file mode 100644 index 0000000000..db84f30e8e --- /dev/null +++ b/change/@azure-msal-common-1ec481cb-c572-4e49-b1e0-37fb0097e025.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Added token revocation functionality to Managed Identity's App Service and Service Fabric Sources #7679", + "packageName": "@azure/msal-common", + "email": "rginsburg@microsoft.com", + "dependentChangeType": "patch" +} From b48caf95d5791c463cf092cacb36bed7953ed9e7 Mon Sep 17 00:00:00 2001 From: Robbie-Microsoft <87724641+Robbie-Microsoft@users.noreply.github.com> Date: Wed, 21 May 2025 18:37:15 -0400 Subject: [PATCH 12/18] Implemented Neha's feedback Co-authored-by: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com> --- lib/msal-node/src/request/ManagedIdentityRequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msal-node/src/request/ManagedIdentityRequest.ts b/lib/msal-node/src/request/ManagedIdentityRequest.ts index 9ad7d18b74..6d38f05040 100644 --- a/lib/msal-node/src/request/ManagedIdentityRequest.ts +++ b/lib/msal-node/src/request/ManagedIdentityRequest.ts @@ -8,7 +8,7 @@ import { ManagedIdentityRequestParams } from "./ManagedIdentityRequestParams.js" /** * ManagedIdentityRequest - * - clientCapabilities - an array of capabilities to be added to all network requests as part of the `xms_cc` claims request + * - clientCapabilities - an array of capabilities to be added to all network requests as part of the `xms_cc` claim * - revokedTokenSha256Hash - a SHA256 hash of the token to be revoked. The managed identity will revoke the token based on a SHA256 hash of the token, not the token itself. This is to prevent the token from being leaked in transit. */ export type ManagedIdentityRequest = ManagedIdentityRequestParams & From 229523558d234b679acc59aacf58d585dd844338 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 21 May 2025 18:55:20 -0400 Subject: [PATCH 13/18] Removed app service --- .../src/client/ManagedIdentityApplication.ts | 5 +- .../ManagedIdentitySources/AppService.ts | 2 +- .../ManagedIdentitySources/AppService.spec.ts | 68 ------------------- .../test/test_kit/StringConstants.ts | 4 -- 4 files changed, 2 insertions(+), 77 deletions(-) diff --git a/lib/msal-node/src/client/ManagedIdentityApplication.ts b/lib/msal-node/src/client/ManagedIdentityApplication.ts index 5e9a95355d..c98ec75385 100644 --- a/lib/msal-node/src/client/ManagedIdentityApplication.ts +++ b/lib/msal-node/src/client/ManagedIdentityApplication.ts @@ -40,10 +40,7 @@ import { ManagedIdentityId } from "../config/ManagedIdentityId.js"; import { HashUtils } from "../crypto/HashUtils.js"; const SOURCES_THAT_SUPPORT_TOKEN_REVOCATION: Array = - [ - ManagedIdentitySourceNames.APP_SERVICE, - ManagedIdentitySourceNames.SERVICE_FABRIC, - ]; + [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 d59eb5ac05..431dd65c00 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 = "2025-03-30"; +const APP_SERVICE_MSI_API_VERSION: string = "2019-08-01"; /** * 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 ba73ac413f..09cf8a031f 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts @@ -5,14 +5,11 @@ 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 { @@ -31,7 +28,6 @@ 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"; @@ -178,70 +174,6 @@ describe("Acquires a token successfully via an App Service Managed Identity", () }); }); - describe("Miscellaneous", () => { - test("ignores a cached token when claims are provided and the Managed Identity does support token revocation, and ensures the token revocation query parameter token_sha256_to_refresh was included in the network request to the Managed Identity", async () => { - const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn( - networkClient, - "sendGetRequestAsync" - ); - - const managedIdentityApplication: ManagedIdentityApplication = - new ManagedIdentityApplication({ - ...systemAssignedConfig, - clientCapabilities: CAE_CONSTANTS.CLIENT_CAPABILITIES, - }); - - 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(CAE_CONSTANTS.CLIENT_CAPABILITIES.toString()); - - 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 = diff --git a/lib/msal-node/test/test_kit/StringConstants.ts b/lib/msal-node/test/test_kit/StringConstants.ts index 9ca1a71d3c..e822294917 100644 --- a/lib/msal-node/test/test_kit/StringConstants.ts +++ b/lib/msal-node/test/test_kit/StringConstants.ts @@ -446,10 +446,6 @@ export const getCacheKey = (resource?: string): string => { return `-${Constants.DEFAULT_AUTHORITY_HOST}-accesstoken-${resourceHelper}-managed_identity-${MANAGED_IDENTITY_RESOURCE_BASE}--`; }; -// SHA256 hash of the DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken -export const DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT_ACCESS_TOKEN_SHA256_HASH_IN_HEX = - "d9678c32c96e9d358c4e61d5b230074c04f037405411740d4e8d9123066341af"; - export const DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT: Omit< AuthenticationResult, "correlationId" From c34e1e47c821da4567b2e608afc6a47e7b2db396 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 21 May 2025 19:08:10 -0400 Subject: [PATCH 14/18] undid code deletion --- .../test/client/ManagedIdentitySources/AppService.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts index 09cf8a031f..ed9679f00a 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts @@ -126,6 +126,8 @@ describe("Acquires a token successfully via an App Service Managed Identity", () ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_RESOURCE_ID_NON_IMDS ) ).toEqual(MANAGED_IDENTITY_RESOURCE_ID); + + jest.restoreAllMocks(); }); }); From f9b13fb0051601db38626cc9a060051676c1cc05 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 22 May 2025 12:34:39 -0400 Subject: [PATCH 15/18] Added back happy path test --- .../ServiceFabric.spec.ts | 71 +++++++++++++++++++ .../test/test_kit/StringConstants.ts | 4 ++ 2 files changed, 75 insertions(+) diff --git a/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts index d1e788a6b6..e97f3b5f24 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.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_RESOURCE, MANAGED_IDENTITY_RESOURCE_ID, MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR, + 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"; @@ -181,6 +185,73 @@ describe("Acquires a token successfully via an App Service Managed Identity", () }); }); + describe("Miscellaneous", () => { + test("ignores a cached token when claims are provided and the Managed Identity does support token revocation, and ensures the token revocation query parameter token_sha256_to_refresh was included in the network request to the Managed Identity", async () => { + const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn( + networkClient, + "sendGetRequestAsync" + ); + + const managedIdentityApplication: ManagedIdentityApplication = + new ManagedIdentityApplication({ + ...systemAssignedConfig, + clientCapabilities: CAE_CONSTANTS.CLIENT_CAPABILITIES, + }); + expect(managedIdentityApplication.getManagedIdentitySource()).toBe( + ManagedIdentitySourceNames.SERVICE_FABRIC + ); + + 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(CAE_CONSTANTS.CLIENT_CAPABILITIES.toString()); + + 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 = diff --git a/lib/msal-node/test/test_kit/StringConstants.ts b/lib/msal-node/test/test_kit/StringConstants.ts index e822294917..9ca1a71d3c 100644 --- a/lib/msal-node/test/test_kit/StringConstants.ts +++ b/lib/msal-node/test/test_kit/StringConstants.ts @@ -446,6 +446,10 @@ export const getCacheKey = (resource?: string): string => { return `-${Constants.DEFAULT_AUTHORITY_HOST}-accesstoken-${resourceHelper}-managed_identity-${MANAGED_IDENTITY_RESOURCE_BASE}--`; }; +// SHA256 hash of the DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken +export const DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT_ACCESS_TOKEN_SHA256_HASH_IN_HEX = + "d9678c32c96e9d358c4e61d5b230074c04f037405411740d4e8d9123066341af"; + export const DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT: Omit< AuthenticationResult, "correlationId" From e7e0af7737e2b0842f146de2e29a19ec44285fda Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 22 May 2025 13:37:15 -0400 Subject: [PATCH 16/18] added additional unit test --- .../ServiceFabric.spec.ts | 135 ++++++++++-------- 1 file changed, 74 insertions(+), 61 deletions(-) diff --git a/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts index e97f3b5f24..a66ed6d0fc 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts @@ -186,70 +186,83 @@ describe("Acquires a token successfully via an App Service Managed Identity", () }); describe("Miscellaneous", () => { - test("ignores a cached token when claims are provided and the Managed Identity does support token revocation, and ensures the token revocation query parameter token_sha256_to_refresh was included in the network request to the Managed Identity", async () => { - const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn( - networkClient, - "sendGetRequestAsync" - ); - - const managedIdentityApplication: ManagedIdentityApplication = - new ManagedIdentityApplication({ - ...systemAssignedConfig, - clientCapabilities: CAE_CONSTANTS.CLIENT_CAPABILITIES, - }); - expect(managedIdentityApplication.getManagedIdentitySource()).toBe( - ManagedIdentitySourceNames.SERVICE_FABRIC - ); - - 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(CAE_CONSTANTS.CLIENT_CAPABILITIES.toString()); + 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 were provided or not) and the Managed Identity does support token revocation, and ensures the token revocation query parameter token_sha256_to_refresh was included in the network request to the Managed Identity", + async (providedCapabilities, capabilitiesOnNetworkRequest) => { + const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn( + networkClient, + "sendGetRequestAsync" + ); - 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); + const managedIdentityApplication: ManagedIdentityApplication = + new ManagedIdentityApplication({ + ...systemAssignedConfig, + clientCapabilities: providedCapabilities, + }); + expect( + managedIdentityApplication.getManagedIdentitySource() + ).toBe(ManagedIdentitySourceNames.SERVICE_FABRIC); + + 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 + ); - 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(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 - ); - }); + 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", () => { From 4a598536418ed04796f378b028e612cf4dbea8bd Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 22 May 2025 13:40:12 -0400 Subject: [PATCH 17/18] typos --- .../test/client/ManagedIdentitySources/ServiceFabric.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts index a66ed6d0fc..dff4109cdd 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts @@ -193,7 +193,7 @@ describe("Acquires a token successfully via an App Service Managed Identity", () ], [undefined, null], ])( - "ignores a cached token when claims are provided (regardless of if client capabilities were provided or not) and the Managed Identity does support token revocation, and ensures the token revocation query parameter token_sha256_to_refresh was included in the network request to the Managed Identity", + "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, From 77c94d0e02c3ceaab7e841b4d0fafba90e6de75a Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 22 May 2025 21:26:29 -0400 Subject: [PATCH 18/18] fixed incorrect comment --- lib/msal-node/src/request/ManagedIdentityRequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msal-node/src/request/ManagedIdentityRequest.ts b/lib/msal-node/src/request/ManagedIdentityRequest.ts index 6d38f05040..bac42e7b30 100644 --- a/lib/msal-node/src/request/ManagedIdentityRequest.ts +++ b/lib/msal-node/src/request/ManagedIdentityRequest.ts @@ -9,7 +9,7 @@ import { ManagedIdentityRequestParams } from "./ManagedIdentityRequestParams.js" /** * ManagedIdentityRequest * - clientCapabilities - an array of capabilities to be added to all network requests as part of the `xms_cc` claim - * - revokedTokenSha256Hash - a SHA256 hash of the token to be revoked. The managed identity will revoke the token based on a SHA256 hash of the token, not the token itself. This is to prevent the token from being leaked in transit. + * - revokedTokenSha256Hash - a SHA256 hash of the token that was revoked. The managed identity will revoke the token based on the SHA256 hash of the token, not the token itself. This is to prevent the token from being leaked in transit. */ export type ManagedIdentityRequest = ManagedIdentityRequestParams & CommonClientCredentialRequest & {