Skip to content

Commit f36613e

Browse files
authored
Adds AKV Token Binding Managed Identity E2E tests (#5829)
* AcquireTokenAndCallAKV_OnImdsV2_MtlsPoP_WithAttestation_Succeeds * ValidateCredentialGuardCertificate * kv name
1 parent d655ba9 commit f36613e

File tree

1 file changed

+110
-9
lines changed

1 file changed

+110
-9
lines changed

tests/Microsoft.Identity.Test.E2e/ManagedIdentityImdsV2Tests.cs

Lines changed: 110 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
using Microsoft.Identity.Client;
5-
using Microsoft.Identity.Client.AppConfig;
6-
using Microsoft.Identity.Client.KeyAttestation;
7-
using Microsoft.Identity.Test.Common.Core.Helpers;
8-
using Microsoft.VisualStudio.TestTools.UnitTesting;
94
using System;
105
using System.IdentityModel.Tokens.Jwt;
116
using System.Linq;
7+
using System.Net.Http;
128
using System.Security.Cryptography;
139
using System.Security.Cryptography.X509Certificates;
14-
using System.Text;
1510
using System.Threading.Tasks;
11+
using Microsoft.Identity.Client;
12+
using Microsoft.Identity.Client.AppConfig;
13+
using Microsoft.Identity.Client.KeyAttestation;
14+
using Microsoft.Identity.Test.Common.Core.Helpers;
15+
using Microsoft.VisualStudio.TestTools.UnitTesting;
1616

1717
namespace Microsoft.Identity.Test.E2E
1818
{
@@ -24,7 +24,9 @@ namespace Microsoft.Identity.Test.E2E
2424
[DoNotParallelize]
2525
public class ManagedIdentityImdsV2Tests
2626
{
27-
private const string ArmScope = "https://graph.microsoft.com";
27+
private const string GraphResource = "https://graph.microsoft.com";
28+
private const string AkvResource = "https://vault.azure.net";
29+
private const string AkvSecretUrl = "https://tokenbinding.vault.azure.net/secrets/boundsecret/?api-version=2015-06-01";
2830

2931
// UAMI identifiers for MSALMSIV2 pool
3032
private const string UamiClientId = "6325cd32-9911-41f3-819c-416cdf9104e7";
@@ -72,7 +74,7 @@ public async Task AcquireToken_OnImdsV2_MtlsPoP_WithAttestation_Succeeds(string
7274

7375
try
7476
{
75-
var result = await mi.AcquireTokenForManagedIdentity(ArmScope)
77+
var result = await mi.AcquireTokenForManagedIdentity(GraphResource)
7678
.WithMtlsProofOfPossession()
7779
.WithAttestationSupport()
7880
.ExecuteAsync()
@@ -124,7 +126,7 @@ public async Task AcquireToken_OnImdsV2_MtlsPoP_GracefulDegradation_WhenCredenti
124126
// Temporarily clear the endpoint to simulate unavailability
125127
Environment.SetEnvironmentVariable("TOKEN_ATTESTATION_ENDPOINT", null);
126128

127-
var result = await mi.AcquireTokenForManagedIdentity(ArmScope)
129+
var result = await mi.AcquireTokenForManagedIdentity(GraphResource)
128130
.WithMtlsProofOfPossession()
129131
.WithAttestationSupport()
130132
.ExecuteAsync()
@@ -146,6 +148,65 @@ public async Task AcquireToken_OnImdsV2_MtlsPoP_GracefulDegradation_WhenCredenti
146148

147149
#endregion
148150

151+
#region AKV mTLS PoP Tests
152+
/// <summary>
153+
/// Tests mTLS PoP token acquisition and Azure Key Vault resource call with attestation.
154+
/// </summary>
155+
[RunOnAzureDevOps]
156+
[TestCategory("MI_E2E_ImdsV2_Attested")]
157+
[TestMethod]
158+
public async Task AcquireTokenAndCallAKV_OnImdsV2_MtlsPoP_WithAttestation_Succeeds()
159+
{
160+
if (!OperatingSystem.IsWindows())
161+
{
162+
Assert.Inconclusive("Credential Guard attestation is only available on Windows.");
163+
}
164+
165+
var mi = BuildMi();
166+
167+
try
168+
{
169+
var tokenResult = await mi.AcquireTokenForManagedIdentity(AkvResource)
170+
.WithMtlsProofOfPossession()
171+
.WithAttestationSupport()
172+
.ExecuteAsync()
173+
.ConfigureAwait(false);
174+
175+
Assert.IsFalse(string.IsNullOrEmpty(tokenResult.AccessToken), "AccessToken should not be empty.");
176+
Assert.AreEqual("mtls_pop", tokenResult.TokenType, "Token type should be 'mtls_pop'.");
177+
Assert.IsNotNull(tokenResult.BindingCertificate, "BindingCertificate should not be null.");
178+
179+
// Validate the certificate is backed by Credential Guard (RSACng with proper properties)
180+
ValidateCredentialGuardCertificate(tokenResult.BindingCertificate);
181+
182+
ValidateMtlsPopBinding(tokenResult.AccessToken, tokenResult.BindingCertificate);
183+
184+
await CallAkvSecretAsync(
185+
AkvSecretUrl,
186+
tokenResult.BindingCertificate,
187+
tokenResult.TokenType,
188+
tokenResult.AccessToken)
189+
.ConfigureAwait(false);
190+
191+
Assert.AreEqual(TokenSource.IdentityProvider, tokenResult.AuthenticationResultMetadata.TokenSource,
192+
"Token should come from MSI endpoint.");
193+
}
194+
catch (MsalClientException ex) when (ex.ErrorCode == "credential_guard_not_available")
195+
{
196+
Assert.Inconclusive("Credential Guard is not available.");
197+
}
198+
catch (CryptographicException ex)
199+
{
200+
Assert.Inconclusive($"Cryptographic operation failed: {ex.Message}");
201+
}
202+
catch (HttpRequestException ex)
203+
{
204+
Assert.Fail($"AKV call failed: {ex.Message}");
205+
}
206+
}
207+
208+
#endregion
209+
149210
#region Helper Methods
150211

151212
/// <summary>
@@ -272,6 +333,46 @@ private static void ValidateCredentialGuardCertificate(X509Certificate2 certific
272333
}
273334
}
274335

336+
/// <summary>
337+
/// Calls AKV secret endpoint over mTLS with client certificate and token.
338+
/// </summary>
339+
private static async Task CallAkvSecretAsync(
340+
string secretUrl,
341+
X509Certificate2 clientCertificate,
342+
string tokenType,
343+
string accessToken)
344+
{
345+
Assert.IsNotNull(clientCertificate, "Client certificate required.");
346+
Assert.IsFalse(string.IsNullOrEmpty(accessToken), "Access token required.");
347+
348+
using var httpClient = new HttpClient(new HttpClientHandler
349+
{
350+
ClientCertificateOptions = ClientCertificateOption.Manual,
351+
SslProtocols = System.Security.Authentication.SslProtocols.Tls12 |
352+
System.Security.Authentication.SslProtocols.Tls13,
353+
ClientCertificates = { clientCertificate }
354+
});
355+
356+
httpClient.DefaultRequestHeaders.Authorization =
357+
new System.Net.Http.Headers.AuthenticationHeaderValue(tokenType, accessToken);
358+
httpClient.DefaultRequestHeaders.Accept.Add(
359+
new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
360+
httpClient.DefaultRequestHeaders.Add("x-ms-tokenboundauth", "true");
361+
362+
using var response = await httpClient.GetAsync(new Uri(secretUrl)).ConfigureAwait(false);
363+
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
364+
365+
Assert.IsTrue(
366+
response.IsSuccessStatusCode,
367+
$"AKV secret GET failed: {(int)response.StatusCode} {response.StatusCode}. Body: {responseContent}");
368+
369+
using var jsonDoc = System.Text.Json.JsonDocument.Parse(responseContent);
370+
var root = jsonDoc.RootElement;
371+
372+
Assert.IsTrue(root.TryGetProperty("value", out var secretValue), "Response missing 'value' property.");
373+
Assert.IsFalse(string.IsNullOrEmpty(secretValue.GetString()), "Secret value is empty.");
374+
}
375+
275376
#endregion
276377
}
277378
}

0 commit comments

Comments
 (0)