Skip to content

Added token revocation functionality to Managed Identity's App Service and Service Fabric sources #7679

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
"dependentChangeType": "patch"
}
1 change: 1 addition & 0 deletions lib/msal-node/apiReview/msal-node.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ export class ManagedIdentityApplication {

// @public (undocumented)
export type ManagedIdentityConfiguration = {
clientCapabilities?: Array<string>;
managedIdentityIdParams?: ManagedIdentityIdParams;
system?: NodeSystemOptions;
};
Expand Down
80 changes: 70 additions & 10 deletions lib/msal-node/src/client/ManagedIdentityApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ import {
DEFAULT_AUTHORITY_FOR_MANAGED_IDENTITY,
ManagedIdentitySourceNames,
} from "../utils/Constants.js";
import { ManagedIdentityId } from "../config/ManagedIdentityId.js";

const SOURCES_THAT_SUPPORT_TOKEN_REVOCATION: Array<ManagedIdentitySourceNames> =

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great that you have a dedicated list of sources that handle revocation. If more services adopt it, it should be trivial to add them to SOURCES_THAT_SUPPORT_TOKEN_REVOCATION.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It won't be that easy, would it? Each MSIv1 provider is different. We will need to know their API version (which may or may not change, and if they do, other aspects of the protocol will likely also change).

(I am not necessarily against the current implementation, as long as it is functionally correct. I am just calling out the need to still be careful when more services adopt token revocation.)

[
ManagedIdentitySourceNames.APP_SERVICE,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you remove app service, you can merge the PR. we can add app service later

ManagedIdentitySourceNames.SERVICE_FABRIC,
];

/**
* Class to initialize a managed identity and identify the service
Expand Down Expand Up @@ -141,14 +148,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
Expand All @@ -164,16 +169,47 @@ 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 (

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GOOD - if claims are set and you have a cached token, you skip the cache for that request, if the source supports revocation, you also send the old token’s SHA-256 to the server. This ensures the old token is invalidated on the RP

cachedAuthenticationResult &&
SOURCES_THAT_SUPPORT_TOKEN_REVOCATION.includes(sourceName)
) {
const revokedTokenSha256Hash: string =
await this.cryptoProvider.hashString(
cachedAuthenticationResult.accessToken
);
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) {
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;
await this.managedIdentityClient.sendManagedIdentityTokenRequest(
await this.acquireTokenFromManagedIdentity(
managedIdentityRequest,
this.config.managedIdentityId,
this.fakeAuthority,
Expand All @@ -183,15 +219,39 @@ 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
);
}
}

/**
* 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(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this method needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I extracted the functionality for the network request since it's being used multiple times.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allowed me to write better docs for it

managedIdentityRequest: ManagedIdentityRequest,
managedIdentityId: ManagedIdentityId,
fakeAuthority: Authority,
refreshAccessToken?: boolean
): Promise<AuthenticationResult> {
// 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
Expand Down
14 changes: 7 additions & 7 deletions lib/msal-node/src/client/ManagedIdentitySources/AppService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,19 @@ 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";
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";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please do not merge this

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, remove the app service changes from this PR, and then we can merge it for Service Fabric. We will create a new PR later after app service deploys their token revocation support.


/**
* Original source of code: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/src/AppServiceManagedIdentitySource.cs
Expand Down Expand Up @@ -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 (
Expand Down
16 changes: 8 additions & 8 deletions lib/msal-node/src/client/ManagedIdentitySources/AzureArc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<string, string> = networkRequest.headers;
headers[HeaderNames.CONTENT_TYPE] = Constants.URL_FORM_CONTENT_TYPE;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 5 additions & 6 deletions lib/msal-node/src/client/ManagedIdentitySources/Imds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 (
Expand Down
Loading