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 ;
94using System ;
105using System . IdentityModel . Tokens . Jwt ;
116using System . Linq ;
7+ using System . Net . Http ;
128using System . Security . Cryptography ;
139using System . Security . Cryptography . X509Certificates ;
14- using System . Text ;
1510using 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
1717namespace 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