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" +} 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" +} diff --git a/lib/msal-common/apiReview/msal-common.api.md b/lib/msal-common/apiReview/msal-common.api.md index 550ba3a03f..98fc3af219 100644 --- a/lib/msal-common/apiReview/msal-common.api.md +++ b/lib/msal-common/apiReview/msal-common.api.md @@ -2216,6 +2216,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 2ec04920ef..c8967a9299 100644 --- a/lib/msal-common/src/utils/Constants.ts +++ b/lib/msal-common/src/utils/Constants.ts @@ -380,3 +380,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/apiReview/msal-node.api.md b/lib/msal-node/apiReview/msal-node.api.md index c431eb3e40..6edeb1d502 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..c98ec75385 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, @@ -35,6 +36,11 @@ import { DEFAULT_AUTHORITY_FOR_MANAGED_IDENTITY, ManagedIdentitySourceNames, } from "../utils/Constants.js"; +import { ManagedIdentityId } from "../config/ManagedIdentityId.js"; +import { HashUtils } from "../crypto/HashUtils.js"; + +const SOURCES_THAT_SUPPORT_TOKEN_REVOCATION: Array = + [ManagedIdentitySourceNames.SERVICE_FABRIC]; /** * Class to initialize a managed identity and identify the service @@ -56,6 +62,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 || {}); @@ -114,6 +122,8 @@ export class ManagedIdentityApplication { this.cryptoProvider, this.config.disableInternalRetries ); + + this.hashUtils = new HashUtils(); } /** @@ -141,14 +151,12 @@ export class ManagedIdentityApplication { ], authority: this.fakeAuthority.canonicalAuthority, correlationId: this.cryptoProvider.createNewGuid(), + claims: managedIdentityRequestParams.claims, + clientCapabilities: this.config.clientCapabilities, }; - if ( - managedIdentityRequestParams.claims || - managedIdentityRequest.forceRefresh - ) { - // make a network call to the managed identity source - return this.managedIdentityClient.sendManagedIdentityTokenRequest( + if (managedIdentityRequest.forceRefresh) { + return this.acquireTokenFromManagedIdentity( managedIdentityRequest, this.config.managedIdentityId, this.fakeAuthority @@ -164,6 +172,36 @@ export class ManagedIdentityApplication { ManagedIdentityApplication.nodeStorage as NodeStorage ); + /* + * 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 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 ( + cachedAuthenticationResult && + SOURCES_THAT_SUPPORT_TOKEN_REVOCATION.includes(sourceName) + ) { + const revokedTokenSha256Hash: string = this.hashUtils + .sha256(cachedAuthenticationResult.accessToken) + .toString(EncodingTypes.HEX); + managedIdentityRequest.revokedTokenSha256Hash = + revokedTokenSha256Hash; + } + + 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) { @@ -171,9 +209,9 @@ export class ManagedIdentityApplication { "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; - await this.managedIdentityClient.sendManagedIdentityTokenRequest( + await this.acquireTokenFromManagedIdentity( managedIdentityRequest, this.config.managedIdentityId, this.fakeAuthority, @@ -183,8 +221,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 @@ -192,6 +229,30 @@ 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 AuthenticationResult containing the acquired token and related information. + */ + 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 diff --git a/lib/msal-node/src/client/ManagedIdentitySources/AppService.ts b/lib/msal-node/src/client/ManagedIdentitySources/AppService.ts index 9eccbf287c..431dd65c00 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"; @@ -114,11 +113,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..ece2c5379b 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 { @@ -201,11 +199,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 +301,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..1164ceb375 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts @@ -24,7 +24,11 @@ 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, +} from "../../utils/Constants.js"; import { ManagedIdentityTokenResponse } from "../../response/ManagedIdentityTokenResponse.js"; import { NodeStorage } from "../../cache/NodeStorage.js"; import { @@ -146,6 +150,29 @@ export abstract class BaseManagedIdentitySource { managedIdentityId ); + 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.revokedTokenSha256Hash; + } + + if (managedIdentityRequest.clientCapabilities?.length) { + const clientCapabilities: string = + managedIdentityRequest.clientCapabilities.toString(); + + this.logger.info( + `[Managed Identity] The following client capabilities 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..38db56978f 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, @@ -109,9 +109,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..c24021d0d9 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"; @@ -112,11 +111,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..c41f5796e0 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"; @@ -107,12 +105,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..9b2af42cb4 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 @@ -131,11 +130,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 3c744d7e0a..ffc1d3c634 100644 --- a/lib/msal-node/src/config/Configuration.ts +++ b/lib/msal-node/src/config/Configuration.ts @@ -134,6 +134,7 @@ export type ManagedIdentityIdParams = { /** @public */ export type ManagedIdentityConfiguration = { + clientCapabilities?: Array; managedIdentityIdParams?: ManagedIdentityIdParams; system?: NodeSystemOptions; }; @@ -246,14 +247,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 { @@ -277,6 +280,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..bac42e7b30 100644 --- a/lib/msal-node/src/request/ManagedIdentityRequest.ts +++ b/lib/msal-node/src/request/ManagedIdentityRequest.ts @@ -8,8 +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` claim + * - 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; + CommonClientCredentialRequest & { + clientCapabilities?: Array; + revokedTokenSha256Hash?: 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 */ 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/Imds.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/Imds.spec.ts index 170613a5fd..53afa37006 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, @@ -34,6 +35,7 @@ import { } from "../../test_kit/ManagedIdentityTestUtils.js"; import { DEFAULT_MANAGED_IDENTITY_ID, + ManagedIdentityQueryParameters, ManagedIdentitySourceNames, } from "../../../src/utils/Constants.js"; import { @@ -664,28 +666,48 @@ 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" + ); + + 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); - 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 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, }); @@ -693,6 +715,15 @@ 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 secondNetworkRequestUrlParams: URLSearchParams = + new URLSearchParams(sendGetRequestAsyncSpy.mock.lastCall[0]); + expect( + secondNetworkRequestUrlParams.has( + ManagedIdentityQueryParameters.SHA256_TOKEN_TO_REFRESH + ) + ).toBe(false); }); test("ignores a cached token when forceRefresh is set to true", async () => { @@ -701,7 +732,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 ); @@ -721,7 +751,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 ); @@ -733,7 +762,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 ); diff --git a/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts index d1e788a6b6..dff4109cdd 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,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.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(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 = 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"