diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs
index 9237f3ed49..96591a6aee 100644
--- a/src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs
+++ b/src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs
@@ -32,12 +32,12 @@ public static AcquireTokenForManagedIdentityParameterBuilder WithAttestationSupp
throw new ArgumentNullException(nameof(builder));
}
- builder.CommonParameters.AttestationTokenProvider = async (endpoint, keyHandle, clientId, keyId, logger, ct) =>
+ builder.CommonParameters.AttestationTokenProvider = async (endpoint, keyHandle, clientIdMetadata, keyId, logger, ct) =>
{
AttestationResult result = await PopKeyAttestor.AttestCredentialGuardAsync(
endpoint,
keyHandle,
- clientId,
+ clientIdMetadata,
keyId,
logger,
ct).ConfigureAwait(false);
diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/PopKeyAttestor.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/PopKeyAttestor.cs
index b6cd810933..5f17dacc95 100644
--- a/src/client/Microsoft.Identity.Client.KeyAttestation/PopKeyAttestor.cs
+++ b/src/client/Microsoft.Identity.Client.KeyAttestation/PopKeyAttestor.cs
@@ -31,10 +31,11 @@ internal static class PopKeyAttestor
// ephemeral keys. Callers always supply a non-empty keyId; null/empty is
// treated as a defensive fallback that bypasses the cache.
//
- // clientId – intentionally NOT included. MAA attests the CNG key itself; clientId is
- // forwarded to the service as metadata but does not change which attestation
- // token is valid for a given key. Different managed identities (SAMI vs UAMI)
- // sharing the same CNG key at the same endpoint correctly share the cached token.
+ // clientId – intentionally NOT included. MAA attests the CNG key itself; the
+ // client_id value is forwarded to the service as metadata but does not change
+ // which attestation token is valid for a given key. Different managed identities
+ // (SAMI vs UAMI) sharing the same CNG key at the same endpoint correctly share
+ // the cached token.
//
// Why a dictionary and not a single field?
// A process could theoretically use multiple MAA endpoints (e.g. in multi-region
@@ -96,7 +97,7 @@ internal static void ResetCacheForTest()
///
/// Attestation service endpoint (required).
/// Valid SafeNCryptKeyHandle (must remain valid for duration of call).
- /// Optional client identifier (may be null/empty).
+ /// Optional value forwarded as MAA client_id metadata (may be null/empty).
/// CNG key identifier scoping the cache entry. Callers should pass the
/// CNG key's when available, or a
/// stable derived identifier (e.g. SHA-256 fingerprint of the RSA public key) for ephemeral keys.
diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs
index b0fe39edbf..3e1d78e2a4 100644
--- a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs
+++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs
@@ -81,7 +81,7 @@ internal class AcquireTokenCommonParameters
/// Optional delegate for obtaining attestation JWT for Credential Guard keys.
/// Set by the KeyAttestation package via .WithAttestationSupport().
/// Returns null for non-attested flows.
- /// Signature: (endpoint, keyHandle, clientId, keyId, logger, cancellationToken) → JWT or null.
+ /// Signature: (endpoint, keyHandle, clientIdMetadata, keyId, logger, cancellationToken) → JWT or null.
///
public Func> AttestationTokenProvider { get; set; }
diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs
index 8c9a6628fd..a82f902097 100644
--- a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs
+++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs
@@ -45,7 +45,7 @@ internal class AcquireTokenForManagedIdentityParameters : IAcquireTokenParameter
///
/// Optional delegate for obtaining attestation JWT for Credential Guard keys.
/// Set by the KeyAttestation package via .WithAttestationSupport().
- /// Signature: (endpoint, keyHandle, clientId, keyId, logger, cancellationToken) → JWT or null.
+ /// Signature: (endpoint, keyHandle, clientIdMetadata, keyId, logger, cancellationToken) → JWT or null.
///
public Func> AttestationTokenProvider { get; set; }
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs
index 09f8ee6e97..59390bbd79 100644
--- a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs
@@ -213,7 +213,7 @@ internal static bool IsSchannelFailure(Exception ex)
}
private async Task ExecuteCertificateRequestAsync(
- string clientId,
+ string tenantId,
string attestationEndpoint,
string csr,
ManagedIdentityKeyInfo managedIdentityKeyInfo)
@@ -236,7 +236,7 @@ private async Task ExecuteCertificateRequestAsync(
if (managedIdentityKeyInfo.Type == ManagedIdentityKeyType.KeyGuard)
{
attestationJwt = await GetAttestationJwtAsync(
- clientId,
+ tenantId,
attestationUri,
managedIdentityKeyInfo,
_requestContext.UserCancellationToken).ConfigureAwait(false);
@@ -362,7 +362,7 @@ private async Task AcquireMtlsBindingAsync()
var privateKey = csrAndKey.privateKey;
var certificateRequestResponse = await ExecuteCertificateRequestAsync(
- csrMetadata.ClientId,
+ csrMetadata.TenantId,
csrMetadata.AttestationEndpoint,
csr,
keyInfo).ConfigureAwait(false);
@@ -431,13 +431,13 @@ public async Task AcquireMtlsBindingForDelegationAsync(
/// Obtains an attestation JWT for the Credential Guard/CSR payload using the configured
/// attestation token provider delegate.
///
- /// Client ID to be sent to the attestation provider.
+ /// Tenant ID sent as the MAA client_id telemetry metadata.
/// The attestation endpoint.
/// The key information.
/// Cancellation token.
/// JWT string suitable for the IMDSv2 attested PoP flow, or null for non-attested flow.
private async Task GetAttestationJwtAsync(
- string clientId,
+ string tenantId,
Uri attestationEndpoint,
ManagedIdentityKeyInfo keyInfo,
CancellationToken cancellationToken)
@@ -468,7 +468,7 @@ private async Task GetAttestationJwtAsync(
string attestationJwt = await _attestationTokenProvider(
attestationEndpoint.AbsoluteUri,
rsaCng.Key.Handle,
- clientId,
+ tenantId,
keyId,
_requestContext.Logger,
cancellationToken).ConfigureAwait(false);
diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs
index e566f42925..c3d2877ce2 100644
--- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs
+++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs
@@ -2261,6 +2261,44 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
}
}
+ [TestMethod]
+ public async Task AttestationProvider_ReceivesTenantIdAsClientIdMetadata()
+ {
+ // Arrange
+ string capturedClientIdMetadata = null;
+ PopKeyAttestor.s_testAttestationProvider = (endpoint, keyHandle, clientIdMetadata, keyId, ct) =>
+ {
+ capturedClientIdMetadata = clientIdMetadata;
+ const string fakeJwt = "fake.jwt.token";
+ var token = new AttestationToken(fakeJwt, DateTimeOffset.UtcNow.AddHours(1));
+ return Task.FromResult(new AttestationResult(AttestationStatus.Success, token, fakeJwt, 0, string.Empty));
+ };
+
+ using (new EnvVariableContext())
+ using (var httpManager = new MockHttpManager())
+ {
+ ManagedIdentityClient.ResetSourceForTest();
+ ImdsV2ManagedIdentitySource.ResetCertCacheForTest();
+ SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint);
+
+ var managedIdentityApp = await CreateManagedIdentityAsync(
+ httpManager,
+ managedIdentityKeyType: ManagedIdentityKeyType.KeyGuard).ConfigureAwait(false);
+
+ AddMocksToGetEntraToken(httpManager);
+
+ // Act
+ await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
+ .WithMtlsProofOfPossession()
+ .WithAttestationSupport()
+ .ExecuteAsync().ConfigureAwait(false);
+
+ // Assert
+ Assert.AreEqual(TestConstants.TenantId, capturedClientIdMetadata);
+ Assert.AreNotEqual(TestConstants.ClientId, capturedClientIdMetadata);
+ }
+ }
+
[TestMethod]
public async Task MaaTokenCache_Hit_DoesNotCallAttestationProviderAgain()
{
@@ -2330,6 +2368,53 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
}
}
+ [TestMethod]
+ public async Task MaaTokenCache_DifferentClientIdMetadata_SharesCacheEntry()
+ {
+ // Arrange
+ int providerCallCount = 0;
+ string providerClientIdMetadata = null;
+ PopKeyAttestor.s_testAttestationProvider = (endpoint, keyHandle, clientIdMetadata, keyId, ct) =>
+ {
+ Interlocked.Increment(ref providerCallCount);
+ providerClientIdMetadata = clientIdMetadata;
+ const string fakeJwt = "fake.jwt.token";
+ var token = new AttestationToken(fakeJwt, DateTimeOffset.UtcNow.AddHours(1));
+ return Task.FromResult(new AttestationResult(AttestationStatus.Success, token, fakeJwt, 0, string.Empty));
+ };
+
+ const string endpoint = "https://eastus.attestation.azure.net";
+ const string keyId = "TestKeyClientIdMetadata";
+ const string managedIdentityClientId = "11111111-1111-1111-1111-111111111111";
+ const string tenantId = "22222222-2222-2222-2222-222222222222";
+
+ using var rsa = new RSACng(2048);
+ var keyHandle = rsa.Key.Handle;
+
+ // Act
+ AttestationResult firstResult = await PopKeyAttestor.AttestCredentialGuardAsync(
+ endpoint,
+ keyHandle,
+ managedIdentityClientId,
+ keyId,
+ cancellationToken: CancellationToken.None).ConfigureAwait(false);
+
+ Assert.AreEqual(1, providerCallCount);
+ Assert.AreEqual(managedIdentityClientId, providerClientIdMetadata);
+
+ AttestationResult secondResult = await PopKeyAttestor.AttestCredentialGuardAsync(
+ endpoint,
+ keyHandle,
+ tenantId,
+ keyId,
+ cancellationToken: CancellationToken.None).ConfigureAwait(false);
+
+ // Assert
+ Assert.AreEqual(1, providerCallCount, "Changing client_id metadata must not create a second MAA cache entry.");
+ Assert.AreEqual(managedIdentityClientId, providerClientIdMetadata, "The provider must not be called on the cache hit.");
+ Assert.AreEqual(firstResult.Jwt, secondResult.Jwt);
+ }
+
[TestMethod]
public async Task MaaTokenCache_ExpiredToken_CallsAttestationProviderAgain()
{