diff --git a/LibsAndSamples.sln b/LibsAndSamples.sln
index b2d234b638..9d6551c7bc 100644
--- a/LibsAndSamples.sln
+++ b/LibsAndSamples.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
-VisualStudioVersion = 18.0.11217.181 d18.0
+VisualStudioVersion = 18.0.11217.181
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9B0B5396-4D95-4C15-82ED-DC22B5A3123F}"
ProjectSection(SolutionItems) = preProject
@@ -192,10 +192,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MacMauiAppWithBroker", "tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MacConsoleAppWithBroker", "tests\devapps\MacConsoleAppWithBroker\MacConsoleAppWithBroker.csproj", "{DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Client.MtlsPop", "src\client\Microsoft.Identity.Client.MtlsPop\Microsoft.Identity.Client.MtlsPop.csproj", "{3E1C29E5-6E67-D9B2-28DF-649A609937A2}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinUI3PackagedSampleApp", "tests\devapps\WinUI3PackagedSampleApp\WinUI3PackagedSampleApp.csproj", "{CE282240-0806-EB91-87E4-D791DC86DEE8}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Client.KeyAttestation", "src\client\Microsoft.Identity.Client.KeyAttestation\Microsoft.Identity.Client.KeyAttestation.csproj", "{425EAEBE-595F-0037-6FDC-2D08D5184705}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug + MobileApps|Any CPU = Debug + MobileApps|Any CPU
@@ -1948,48 +1948,6 @@ Global
{DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|x64.Build.0 = Release|Any CPU
{DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|x86.ActiveCfg = Release|Any CPU
{DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|x86.Build.0 = Release|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|Any CPU.ActiveCfg = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|Any CPU.Build.0 = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|ARM.ActiveCfg = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|ARM.Build.0 = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|ARM64.ActiveCfg = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|ARM64.Build.0 = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|iPhone.ActiveCfg = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|iPhone.Build.0 = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|iPhoneSimulator.ActiveCfg = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|iPhoneSimulator.Build.0 = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|x64.ActiveCfg = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|x64.Build.0 = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|x86.ActiveCfg = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|x86.Build.0 = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|ARM.ActiveCfg = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|ARM.Build.0 = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|ARM64.ActiveCfg = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|ARM64.Build.0 = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|iPhone.ActiveCfg = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|iPhone.Build.0 = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|x64.ActiveCfg = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|x64.Build.0 = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|x86.ActiveCfg = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|x86.Build.0 = Debug|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|Any CPU.Build.0 = Release|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|ARM.ActiveCfg = Release|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|ARM.Build.0 = Release|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|ARM64.ActiveCfg = Release|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|ARM64.Build.0 = Release|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|iPhone.ActiveCfg = Release|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|iPhone.Build.0 = Release|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|x64.ActiveCfg = Release|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|x64.Build.0 = Release|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|x86.ActiveCfg = Release|Any CPU
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|x86.Build.0 = Release|Any CPU
{CE282240-0806-EB91-87E4-D791DC86DEE8}.Debug + MobileApps|Any CPU.ActiveCfg = Debug|x64
{CE282240-0806-EB91-87E4-D791DC86DEE8}.Debug + MobileApps|Any CPU.Build.0 = Debug|x64
{CE282240-0806-EB91-87E4-D791DC86DEE8}.Debug + MobileApps|ARM.ActiveCfg = Debug|x64
@@ -2032,6 +1990,48 @@ Global
{CE282240-0806-EB91-87E4-D791DC86DEE8}.Release|x64.Build.0 = Release|x64
{CE282240-0806-EB91-87E4-D791DC86DEE8}.Release|x86.ActiveCfg = Release|x86
{CE282240-0806-EB91-87E4-D791DC86DEE8}.Release|x86.Build.0 = Release|x86
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|Any CPU.ActiveCfg = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|Any CPU.Build.0 = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|ARM.ActiveCfg = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|ARM.Build.0 = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|ARM64.ActiveCfg = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|ARM64.Build.0 = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|iPhone.ActiveCfg = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|iPhone.Build.0 = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|x64.ActiveCfg = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|x64.Build.0 = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|x86.ActiveCfg = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|x86.Build.0 = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|ARM.Build.0 = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|x64.Build.0 = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|x86.Build.0 = Debug|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|Any CPU.Build.0 = Release|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|ARM.ActiveCfg = Release|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|ARM.Build.0 = Release|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|ARM64.Build.0 = Release|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|iPhone.Build.0 = Release|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|x64.ActiveCfg = Release|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|x64.Build.0 = Release|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|x86.ActiveCfg = Release|Any CPU
+ {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2089,8 +2089,8 @@ Global
{97995B86-AA0F-3AF9-DA40-85A6263E4391} = {9B0B5396-4D95-4C15-82ED-DC22B5A3123F}
{AEF6BB00-931F-4638-955D-24D735625C34} = {34BE693E-3496-45A4-B1D2-D3A0E068EEDB}
{DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0} = {34BE693E-3496-45A4-B1D2-D3A0E068EEDB}
- {3E1C29E5-6E67-D9B2-28DF-649A609937A2} = {1A37FD75-94E9-4D6F-953A-0DABBD7B49E9}
{CE282240-0806-EB91-87E4-D791DC86DEE8} = {34BE693E-3496-45A4-B1D2-D3A0E068EEDB}
+ {425EAEBE-595F-0037-6FDC-2D08D5184705} = {1A37FD75-94E9-4D6F-953A-0DABBD7B49E9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {020399A9-DC27-4B82-9CAA-EF488665AC27}
diff --git a/prototype/MsiV2DemoApp/MsiV2DemoApp.csproj b/prototype/MsiV2DemoApp/MsiV2DemoApp.csproj
index e8ae98ddef..dda6a17198 100644
--- a/prototype/MsiV2DemoApp/MsiV2DemoApp.csproj
+++ b/prototype/MsiV2DemoApp/MsiV2DemoApp.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/prototype/MsiV2DemoApp/Program.cs b/prototype/MsiV2DemoApp/Program.cs
index 02cd5adce7..d60cf1f225 100644
--- a/prototype/MsiV2DemoApp/Program.cs
+++ b/prototype/MsiV2DemoApp/Program.cs
@@ -40,7 +40,7 @@
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.AppConfig;
-using Microsoft.Identity.Client.MtlsPop;
+using Microsoft.Identity.Client.KeyAttestation;
using Microsoft.IdentityModel.Abstractions;
using System.Net.Http;
using System.Net.Http.Headers;
@@ -256,7 +256,7 @@ private static IManagedIdentityApplication BuildMiApp(ManagedIdentityId miId, II
bool showFullToken)
{
var builder = app.AcquireTokenForManagedIdentity(scope);
- if (useMtls) builder = builder.WithMtlsProofOfPossession();
+ if (useMtls) builder = builder.WithMtlsProofOfPossession().WithAttestationSupport();
if (forceRefresh) builder = builder.WithForceRefresh(true);
var result = await Ui.WithSpinnerAsync(
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationClient.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationClient.cs
similarity index 96%
rename from src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationClient.cs
rename to src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationClient.cs
index c0c8faf588..9d75c6fce8 100644
--- a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationClient.cs
+++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationClient.cs
@@ -1,11 +1,11 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
-namespace Microsoft.Identity.Client.MtlsPop.Attestation
+namespace Microsoft.Identity.Client.KeyAttestation.Attestation
{
///
/// Managed façade for AttestationClientLib.dll. Holds initialization state,
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationClientLib.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationClientLib.cs
similarity index 91%
rename from src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationClientLib.cs
rename to src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationClientLib.cs
index df84387024..07f0daa659 100644
--- a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationClientLib.cs
+++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationClientLib.cs
@@ -1,12 +1,11 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using Microsoft.Win32.SafeHandles;
using System;
-using System.IO;
using System.Runtime.InteropServices;
-namespace Microsoft.Identity.Client.MtlsPop.Attestation
+namespace Microsoft.Identity.Client.KeyAttestation.Attestation
{
internal static class AttestationClientLib
{
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationErrors.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationErrors.cs
similarity index 83%
rename from src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationErrors.cs
rename to src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationErrors.cs
index 0c47ceed76..2dee80fbd3 100644
--- a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationErrors.cs
+++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationErrors.cs
@@ -1,11 +1,7 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace Microsoft.Identity.Client.MtlsPop.Attestation
+namespace Microsoft.Identity.Client.KeyAttestation.Attestation
{
internal static class AttestationErrors
{
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationLogger.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationLogger.cs
similarity index 90%
rename from src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationLogger.cs
rename to src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationLogger.cs
index a59f601bdf..c4a20a3e8b 100644
--- a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationLogger.cs
+++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationLogger.cs
@@ -1,11 +1,11 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
-namespace Microsoft.Identity.Client.MtlsPop.Attestation
+namespace Microsoft.Identity.Client.KeyAttestation.Attestation
{
internal static class AttestationLogger
{
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationResult.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationResult.cs
similarity index 91%
rename from src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationResult.cs
rename to src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationResult.cs
index 79b3f647f5..930ebde5e6 100644
--- a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationResult.cs
+++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationResult.cs
@@ -1,7 +1,7 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-namespace Microsoft.Identity.Client.MtlsPop.Attestation
+namespace Microsoft.Identity.Client.KeyAttestation.Attestation
{
///
/// AttestationResult is the result of an attestation operation.
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationResultErrorCode.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationResultErrorCode.cs
similarity index 95%
rename from src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationResultErrorCode.cs
rename to src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationResultErrorCode.cs
index 4f02375292..d23ce55103 100644
--- a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationResultErrorCode.cs
+++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationResultErrorCode.cs
@@ -1,11 +1,11 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Text;
-namespace Microsoft.Identity.Client.MtlsPop.Attestation
+namespace Microsoft.Identity.Client.KeyAttestation.Attestation
{
///
/// Error codes returned by AttestationClientLib.dll.
@@ -35,7 +35,7 @@ internal enum AttestationResultErrorCode
/// The attestation enclave rejected the supplied evidence (policy or signature failure).
ERRORATTESTATIONFAILED = -6,
- /// libcurl reported “couldn’t send” (DNS resolution, TLS handshake, or socket error).
+ /// libcurl reported "couldn't send" (DNS resolution, TLS handshake, or socket error).
ERRORSENDINGCURLREQUESTFAILED = -7,
/// One or more input parameters passed to the native API were invalid or null.
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationStatus.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationStatus.cs
similarity index 87%
rename from src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationStatus.cs
rename to src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationStatus.cs
index ff20df8aa9..c91ed09822 100644
--- a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationStatus.cs
+++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationStatus.cs
@@ -1,11 +1,11 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Text;
-namespace Microsoft.Identity.Client.MtlsPop.Attestation
+namespace Microsoft.Identity.Client.KeyAttestation.Attestation
{
///
/// High-level outcome categories returned by .
diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/KeyGuardAttestationProvider.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/KeyGuardAttestationProvider.cs
new file mode 100644
index 0000000000..b4c55f9e38
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/KeyGuardAttestationProvider.cs
@@ -0,0 +1,78 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Identity.Client.ManagedIdentity;
+
+namespace Microsoft.Identity.Client.KeyAttestation.Attestation
+{
+ ///
+ /// Implementation of IAttestationProvider for KeyGuard attestation.
+ /// This provider is automatically registered when the KeyAttestation package is loaded.
+ ///
+ internal class KeyGuardAttestationProvider : IAttestationProvider
+ {
+ public async Task AttestKeyGuardAsync(
+ string attestationEndpoint,
+ SafeHandle keyHandle,
+ string clientId,
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ // Call the existing PopKeyAttestor implementation
+ var result = await PopKeyAttestor.AttestKeyGuardAsync(
+ attestationEndpoint,
+ keyHandle,
+ clientId,
+ cancellationToken).ConfigureAwait(false);
+
+ // Map the result to the MSAL interface types
+ return new ManagedIdentity.AttestationResult
+ {
+ Status = result.Status == AttestationStatus.Success
+ ? ManagedIdentity.AttestationStatus.Success
+ : ManagedIdentity.AttestationStatus.Failed,
+ Jwt = result.Jwt,
+ ErrorMessage = result.ErrorMessage,
+ NativeErrorCode = result.NativeErrorCode
+ };
+ }
+ catch (Exception ex)
+ {
+ return new ManagedIdentity.AttestationResult
+ {
+ Status = ManagedIdentity.AttestationStatus.Failed,
+ ErrorMessage = ex.Message,
+ NativeErrorCode = -1
+ };
+ }
+ }
+ }
+
+ ///
+ /// Static initializer that registers the KeyGuard attestation provider
+ /// when the KeyAttestation assembly is loaded.
+ ///
+ internal static class AttestationProviderInitializer
+ {
+ static AttestationProviderInitializer()
+ {
+ // Register the provider when this type is first accessed
+ AttestationProviderRegistry.RegisterProvider(new KeyGuardAttestationProvider());
+ }
+
+ ///
+ /// Method to force static constructor execution.
+ /// Called from module initializer.
+ ///
+ internal static void Initialize()
+ {
+ // This method body is intentionally empty.
+ // Its purpose is to trigger the static constructor above.
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/NativeDiagnostics.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/NativeDiagnostics.cs
similarity index 86%
rename from src/client/Microsoft.Identity.Client.MtlsPop/Attestation/NativeDiagnostics.cs
rename to src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/NativeDiagnostics.cs
index 9482039c8e..456aca8bfa 100644
--- a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/NativeDiagnostics.cs
+++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/NativeDiagnostics.cs
@@ -1,11 +1,11 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.ComponentModel;
using System.IO;
-namespace Microsoft.Identity.Client.MtlsPop.Attestation
+namespace Microsoft.Identity.Client.KeyAttestation.Attestation
{
internal static class NativeDiagnostics
{
@@ -38,7 +38,7 @@ internal static string ProbeNativeDll()
return $"Unable to load {NativeDll}: {ex.Message}";
}
- // success – unload and return null (meaning “no error”)
+ // success – unload and return null (meaning "no error")
WindowsDllLoader.Free(h);
return null;
}
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/WindowsDllLoader.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/WindowsDllLoader.cs
similarity index 92%
rename from src/client/Microsoft.Identity.Client.MtlsPop/Attestation/WindowsDllLoader.cs
rename to src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/WindowsDllLoader.cs
index aaee9eadb2..54b52586a1 100644
--- a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/WindowsDllLoader.cs
+++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/WindowsDllLoader.cs
@@ -1,11 +1,11 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.ComponentModel;
-using System.Runtime.InteropServices;
+using System.Runtime.InteropServices;
-namespace Microsoft.Identity.Client.MtlsPop.Attestation
+namespace Microsoft.Identity.Client.KeyAttestation.Attestation
{
///
/// Windows‑only helper that loads a native DLL from an absolute path.
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/IsExternalInit.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/IsExternalInit.cs
similarity index 71%
rename from src/client/Microsoft.Identity.Client.MtlsPop/IsExternalInit.cs
rename to src/client/Microsoft.Identity.Client.KeyAttestation/IsExternalInit.cs
index dfb6a17acc..6eeb15edba 100644
--- a/src/client/Microsoft.Identity.Client.MtlsPop/IsExternalInit.cs
+++ b/src/client/Microsoft.Identity.Client.KeyAttestation/IsExternalInit.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#if NETSTANDARD
diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs
new file mode 100644
index 0000000000..513605ddd0
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using Microsoft.Identity.Client.KeyAttestation.Attestation;
+using Microsoft.Identity.Client.ManagedIdentity;
+
+namespace Microsoft.Identity.Client.KeyAttestation
+{
+ ///
+ /// Extension methods for enabling KeyGuard attestation support in managed identity mTLS PoP flows.
+ ///
+ public static class ManagedIdentityAttestationExtensions
+ {
+ ///
+ /// Enables KeyGuard attestation support for managed identity mTLS Proof-of-Possession flows.
+ /// This method should be called after .
+ ///
+ /// The AcquireTokenForManagedIdentityParameterBuilder instance.
+ /// The builder to chain .With methods.
+ public static AcquireTokenForManagedIdentityParameterBuilder WithAttestationSupport(
+ this AcquireTokenForManagedIdentityParameterBuilder builder)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ // Ensure the provider is registered (triggers static constructor on older frameworks)
+ Attestation.AttestationProviderInitializer.Initialize();
+
+ // Set the flag to enable attestation
+ builder.CommonParameters.IsAttestationRequested = true;
+ return builder;
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Microsoft.Identity.Client.MtlsPop.csproj b/src/client/Microsoft.Identity.Client.KeyAttestation/Microsoft.Identity.Client.KeyAttestation.csproj
similarity index 85%
rename from src/client/Microsoft.Identity.Client.MtlsPop/Microsoft.Identity.Client.MtlsPop.csproj
rename to src/client/Microsoft.Identity.Client.KeyAttestation/Microsoft.Identity.Client.KeyAttestation.csproj
index 281eaacf00..c29569dfb7 100644
--- a/src/client/Microsoft.Identity.Client.MtlsPop/Microsoft.Identity.Client.MtlsPop.csproj
+++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Microsoft.Identity.Client.KeyAttestation.csproj
@@ -1,4 +1,4 @@
-
+
@@ -19,18 +19,17 @@
$(MicrosoftIdentityClientVersion)-preview
- MSAL.NET extension for managed identity proof-of-possession flows
+ MSAL.NET extension for KeyGuard attestation support
- This package contains binaries needed to use managed identity proof-of-possession (MTLS PoP) flows in applications using MSAL.NET.
+ This package contains binaries needed to enable KeyGuard attestation in managed identity proof-of-possession (mTLS PoP) flows using MSAL.NET.
- Microsoft Authentication Library Managed Identity MSAL Proof-of-Possession
+ Microsoft Authentication Library Managed Identity MSAL KeyGuard Attestation Proof-of-Possession
Microsoft Authentication Library
-
+
@@ -47,4 +46,4 @@
-
+
\ No newline at end of file
diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/ModuleInitializer.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/ModuleInitializer.cs
new file mode 100644
index 0000000000..55167a9339
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.KeyAttestation/ModuleInitializer.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+#if NET5_0_OR_GREATER
+using System.Runtime.CompilerServices;
+#endif
+using Microsoft.Identity.Client.KeyAttestation.Attestation;
+
+namespace Microsoft.Identity.Client.KeyAttestation
+{
+#if NET5_0_OR_GREATER
+ ///
+ /// Module initializer that runs when the KeyAttestation assembly is loaded.
+ /// Automatically registers the KeyGuard attestation provider with MSAL.
+ ///
+ internal static class ModuleInitializer
+ {
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2255:The 'ModuleInitializer' attribute should not be used in libraries", Justification = "Required for auto-registration of attestation provider")]
+ [ModuleInitializer]
+ internal static void Initialize()
+ {
+ // Force the static constructor of AttestationProviderInitializer to run,
+ // which registers the KeyGuard attestation provider
+ AttestationProviderInitializer.Initialize();
+ }
+ }
+#else
+ // For .NET Standard 2.0 and .NET Framework, we rely on the static constructor
+ // being triggered when the extension method is first called.
+ // This ensures the provider is registered before it's needed.
+#endif
+}
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/PopKeyAttestor.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/PopKeyAttestor.cs
similarity index 93%
rename from src/client/Microsoft.Identity.Client.MtlsPop/PopKeyAttestor.cs
rename to src/client/Microsoft.Identity.Client.KeyAttestation/PopKeyAttestor.cs
index f855041bce..ac80bdb7ec 100644
--- a/src/client/Microsoft.Identity.Client.MtlsPop/PopKeyAttestor.cs
+++ b/src/client/Microsoft.Identity.Client.KeyAttestation/PopKeyAttestor.cs
@@ -1,14 +1,14 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.Identity.Client.MtlsPop.Attestation;
+using Microsoft.Identity.Client.KeyAttestation.Attestation;
using Microsoft.Win32.SafeHandles;
-namespace Microsoft.Identity.Client.MtlsPop
+namespace Microsoft.Identity.Client.KeyAttestation
{
///
/// Static facade for attesting a KeyGuard/CNG key and getting a JWT back.
diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/net8.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/net8.0/PublicAPI.Shipped.txt
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/net8.0/PublicAPI.Unshipped.txt
new file mode 100644
index 0000000000..1a5cf5f13c
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/net8.0/PublicAPI.Unshipped.txt
@@ -0,0 +1,2 @@
+Microsoft.Identity.Client.KeyAttestation.ManagedIdentityAttestationExtensions
+static Microsoft.Identity.Client.KeyAttestation.ManagedIdentityAttestationExtensions.WithAttestationSupport(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
new file mode 100644
index 0000000000..1a5cf5f13c
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
@@ -0,0 +1,2 @@
+Microsoft.Identity.Client.KeyAttestation.ManagedIdentityAttestationExtensions
+static Microsoft.Identity.Client.KeyAttestation.ManagedIdentityAttestationExtensions.WithAttestationSupport(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/ManagedIdentityPopExtensions.cs b/src/client/Microsoft.Identity.Client.MtlsPop/ManagedIdentityPopExtensions.cs
deleted file mode 100644
index 742df7b00f..0000000000
--- a/src/client/Microsoft.Identity.Client.MtlsPop/ManagedIdentityPopExtensions.cs
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-
-using System.Runtime.InteropServices;
-using Microsoft.Identity.Client.MtlsPop.Attestation;
-using Microsoft.Identity.Client.PlatformsCommon.Shared;
-
-namespace Microsoft.Identity.Client.MtlsPop
-{
- ///
- /// Registers the mTLS PoP attestation runtime (interop) by installing a provider
- /// function into MSAL's internal config.
- ///
- public static class ManagedIdentityPopExtensions
- {
- ///
- /// App-level registration: tells MSAL how to obtain a KeyGuard/CNG handle
- /// and perform attestation to get the JWT needed for mTLS PoP.
- ///
- public static AcquireTokenForManagedIdentityParameterBuilder WithMtlsProofOfPossession(
- this AcquireTokenForManagedIdentityParameterBuilder builder)
- {
- void MtlsNotSupportedForManagedIdentity(string message)
- {
- throw new MsalClientException(
- MsalError.MtlsNotSupportedForManagedIdentity,
- message);
- }
-
- if (!DesktopOsHelper.IsWindows())
- {
- MtlsNotSupportedForManagedIdentity(MsalErrorMessage.MtlsNotSupportedForNonWindowsMessage);
- }
-
-#if NET462
- MtlsNotSupportedForManagedIdentity(MsalErrorMessage.MtlsNotSupportedForManagedIdentityMessage);
-#endif
-
- builder.CommonParameters.IsMtlsPopRequested = true;
- AddRuntimeSupport(builder);
- return builder;
- }
-
- ///
- /// Adds the runtime support by registering the attestation function.
- ///
- ///
- ///
- private static void AddRuntimeSupport(
- AcquireTokenForManagedIdentityParameterBuilder builder)
- {
- // Register the "runtime" function that PoP operation will invoke.
- builder.CommonParameters.AttestationTokenProvider =
- async (req, ct) =>
- {
- // 1) Get the caller-provided KeyGuard/CNG handle
- SafeHandle keyHandle = req.KeyHandle;
-
- // 2) Call the native interop via PopKeyAttestor
- AttestationResult attestationResult = await PopKeyAttestor.AttestKeyGuardAsync(
- req.AttestationEndpoint.AbsoluteUri, // expects string
- keyHandle,
- req.ClientId ?? string.Empty,
- ct).ConfigureAwait(false);
-
- // 3) Map to MSAL's internal response
- if (attestationResult != null &&
- attestationResult.Status == AttestationStatus.Success &&
- !string.IsNullOrWhiteSpace(attestationResult.Jwt))
- {
- return new ManagedIdentity.AttestationTokenResponse { AttestationToken = attestationResult.Jwt };
- }
-
- throw new MsalClientException(
- "attestation_failure",
- $"Key Attestation failed " +
- $"(status={attestationResult?.Status}, " +
- $"code={attestationResult?.NativeErrorCode}). {attestationResult?.ErrorMessage}");
- };
- }
- }
-}
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/net8.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/net8.0/PublicAPI.Shipped.txt
deleted file mode 100644
index a068838189..0000000000
--- a/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/net8.0/PublicAPI.Shipped.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Microsoft.Identity.Client.MtlsPop.ManagedIdentityPopExtensions
-static Microsoft.Identity.Client.MtlsPop.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
-
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/net8.0/PublicAPI.Unshipped.txt
deleted file mode 100644
index 5f282702bb..0000000000
--- a/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/net8.0/PublicAPI.Unshipped.txt
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/netstandard2.0/PublicAPI.Shipped.txt
deleted file mode 100644
index a068838189..0000000000
--- a/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/netstandard2.0/PublicAPI.Shipped.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Microsoft.Identity.Client.MtlsPop.ManagedIdentityPopExtensions
-static Microsoft.Identity.Client.MtlsPop.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
-
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
deleted file mode 100644
index 5f282702bb..0000000000
--- a/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForManagedIdentityParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForManagedIdentityParameterBuilder.cs
index dd9600b1aa..98d6457fed 100644
--- a/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForManagedIdentityParameterBuilder.cs
+++ b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForManagedIdentityParameterBuilder.cs
@@ -96,28 +96,12 @@ internal override ApiEvent.ApiIds CalculateApiEventId()
return ApiEvent.ApiIds.AcquireTokenForUserAssignedManagedIdentity;
}
- ///
- /// TEST HOOK ONLY: Allows unit tests to inject a fake attestation-token provider
- /// so we don't hit the real attestation service. Not part of the public API.
- ///
- internal AcquireTokenForManagedIdentityParameterBuilder WithAttestationProviderForTests(
- Func> provider)
- {
- if (provider is null)
- {
- throw new ArgumentNullException(nameof(provider));
- }
-
- CommonParameters.AttestationTokenProvider = provider;
- return this;
- }
-
private static void ApplyMtlsPopAndAttestation(
AcquireTokenCommonParameters acquireTokenCommonParameters,
AcquireTokenForManagedIdentityParameters acquireTokenForManagedIdentityParameters)
{
acquireTokenForManagedIdentityParameters.IsMtlsPopRequested = acquireTokenCommonParameters.IsMtlsPopRequested;
- acquireTokenForManagedIdentityParameters.AttestationTokenProvider ??= acquireTokenCommonParameters.AttestationTokenProvider;
+ acquireTokenForManagedIdentityParameters.IsAttestationRequested = acquireTokenCommonParameters.IsAttestationRequested;
}
}
}
diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs
index cbb52f4a88..da3c5533d0 100644
--- a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs
+++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs
@@ -40,7 +40,7 @@ internal class AcquireTokenCommonParameters
public string FmiPathSuffix { get; internal set; }
public string ClientAssertionFmiPath { get; internal set; }
public bool IsMtlsPopRequested { get; set; }
- internal Func> AttestationTokenProvider { get; set; }
+ public bool IsAttestationRequested { get; set; }
internal async Task InitMtlsPopParametersAsync(IServiceBundle serviceBundle, CancellationToken ct)
{
diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs
index 32646eaa62..2207cbc9d0 100644
--- a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs
+++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs
@@ -25,7 +25,7 @@ internal class AcquireTokenForManagedIdentityParameters : IAcquireTokenParameter
public bool IsMtlsPopRequested { get; set; }
- internal Func> AttestationTokenProvider { get; set; }
+ public bool IsAttestationRequested { get; set; }
internal X509Certificate2 MtlsCertificate { get; set; }
diff --git a/src/client/Microsoft.Identity.Client/Internal/RequestContext.cs b/src/client/Microsoft.Identity.Client/Internal/RequestContext.cs
index 2fb786a6a6..02b88de971 100644
--- a/src/client/Microsoft.Identity.Client/Internal/RequestContext.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/RequestContext.cs
@@ -31,7 +31,7 @@ internal class RequestContext
public X509Certificate2 MtlsCertificate { get; }
- internal Func> AttestationTokenProvider { get; set; }
+ public bool IsAttestationRequested { get; set; }
public RequestContext(IServiceBundle serviceBundle, Guid correlationId, X509Certificate2 mtlsCertificate, CancellationToken cancellationToken = default)
{
diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs
index 1833cbb466..07e02e9e68 100644
--- a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs
@@ -216,9 +216,9 @@ private async Task SendTokenRequestForManagedIdentityAsync
_managedIdentityParameters.IsMtlsPopRequested = AuthenticationRequestParameters.IsMtlsPopRequested;
- // Ensure the attestation provider reaches RequestContext for IMDSv2
- AuthenticationRequestParameters.RequestContext.AttestationTokenProvider ??=
- _managedIdentityParameters.AttestationTokenProvider;
+ // Pass the attestation flag to the request context
+ AuthenticationRequestParameters.RequestContext.IsAttestationRequested =
+ _managedIdentityParameters.IsAttestationRequested;
ManagedIdentityResponse managedIdentityResponse =
await _managedIdentityClient
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AttestationProviderRegistry.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AttestationProviderRegistry.cs
new file mode 100644
index 0000000000..602c49b123
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AttestationProviderRegistry.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Identity.Client.ManagedIdentity
+{
+ ///
+ /// Static registry for attestation providers.
+ /// The KeyAttestation package registers itself here via InternalsVisibleTo.
+ ///
+ internal static class AttestationProviderRegistry
+ {
+ private static IAttestationProvider s_provider;
+
+ ///
+ /// Gets the current attestation provider, if one has been registered.
+ ///
+ internal static IAttestationProvider Provider => s_provider;
+
+ ///
+ /// Registers an attestation provider. Called by the KeyAttestation package.
+ ///
+ /// The attestation provider to register.
+ internal static void RegisterProvider(IAttestationProvider provider)
+ {
+ s_provider = provider;
+ }
+
+ ///
+ /// Clears the registered provider. Used for testing.
+ ///
+ internal static void ClearProvider()
+ {
+ s_provider = null;
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/IAttestationProvider.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/IAttestationProvider.cs
new file mode 100644
index 0000000000..2a5f10b6cd
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/IAttestationProvider.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Identity.Client.ManagedIdentity
+{
+ ///
+ /// Internal interface for attestation providers.
+ /// The KeyAttestation package implements this to provide attestation functionality.
+ ///
+ internal interface IAttestationProvider
+ {
+ ///
+ /// Attests a KeyGuard key and returns an attestation JWT.
+ ///
+ /// The attestation endpoint URL.
+ /// The KeyGuard key handle (must be SafeNCryptKeyHandle).
+ /// The client ID to include in the attestation.
+ /// Cancellation token.
+ /// An attestation result containing the JWT or error information.
+ Task AttestKeyGuardAsync(
+ string attestationEndpoint,
+ SafeHandle keyHandle,
+ string clientId,
+ CancellationToken cancellationToken);
+ }
+
+ ///
+ /// Result of an attestation operation.
+ ///
+ internal class AttestationResult
+ {
+ public AttestationStatus Status { get; set; }
+ public string Jwt { get; set; }
+ public string ErrorMessage { get; set; }
+ public int NativeErrorCode { get; set; }
+ }
+
+ ///
+ /// Status codes for attestation operations.
+ ///
+ internal enum AttestationStatus
+ {
+ Success = 0,
+ Failed = 1
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityPopExtensions.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityPopExtensions.cs
new file mode 100644
index 0000000000..61e220fb4b
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityPopExtensions.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Microsoft.Identity.Client.PlatformsCommon.Shared;
+
+namespace Microsoft.Identity.Client
+{
+ ///
+ /// Extension methods for enabling mTLS Proof-of-Possession in managed identity flows.
+ ///
+ public static class ManagedIdentityPopExtensions
+ {
+ ///
+ /// Enables mTLS Proof-of-Possession for managed identity token acquisition.
+ /// When attestation is required (KeyGuard scenarios), use the Msal.KeyAttestation package
+ /// and call .WithAttestationSupport() after this method.
+ ///
+ /// The AcquireTokenForManagedIdentityParameterBuilder instance.
+ /// The builder to chain .With methods.
+ public static AcquireTokenForManagedIdentityParameterBuilder WithMtlsProofOfPossession(
+ this AcquireTokenForManagedIdentityParameterBuilder builder)
+ {
+ if (!DesktopOsHelper.IsWindows())
+ {
+ throw new MsalClientException(
+ MsalError.MtlsNotSupportedForManagedIdentity,
+ MsalErrorMessage.MtlsNotSupportedForNonWindowsMessage);
+ }
+
+#if NET462
+ throw new MsalClientException(
+ MsalError.MtlsNotSupportedForManagedIdentity,
+ MsalErrorMessage.MtlsNotSupportedForManagedIdentityMessage);
+#else
+ builder.CommonParameters.IsMtlsPopRequested = true;
+ return builder;
+#endif
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/CertificateRequestBody.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/CertificateRequestBody.cs
index 64b27ccc45..33a88671b6 100644
--- a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/CertificateRequestBody.cs
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/CertificateRequestBody.cs
@@ -3,6 +3,7 @@
#if SUPPORTS_SYSTEM_TEXT_JSON
using JsonProperty = System.Text.Json.Serialization.JsonPropertyNameAttribute;
+ using JsonIgnore = System.Text.Json.Serialization.JsonIgnoreAttribute;
#else
using Microsoft.Identity.Json;
#endif
@@ -14,7 +15,12 @@ internal class CertificateRequestBody
[JsonProperty("csr")]
public string Csr { get; set; }
+#if SUPPORTS_SYSTEM_TEXT_JSON
[JsonProperty("attestation_token")]
+ [JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)]
+#else
+ [JsonProperty("attestation_token", NullValueHandling = NullValueHandling.Ignore)]
+#endif
public string AttestationToken { get; set; }
public static bool IsNullOrEmpty(CertificateRequestBody certificateRequestBody)
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs
index 114b6bbc01..ef1c355607 100644
--- a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs
@@ -26,6 +26,7 @@ internal class ImdsV2ManagedIdentitySource : AbstractManagedIdentity
internal static readonly ICertificateCache s_mtlsCertificateCache = new InMemoryCertificateCache();
private readonly IMtlsCertificateCache _mtlsCache;
+ private bool _isAttestationRequested;
// used in unit tests
public const string ApiVersionQueryParam = "cred-api-version";
@@ -165,6 +166,15 @@ internal ImdsV2ManagedIdentitySource(
_mtlsCache = mtlsCache ?? throw new ArgumentNullException(nameof(mtlsCache));
}
+ public override async Task AuthenticateAsync(
+ ApiConfig.Parameters.AcquireTokenForManagedIdentityParameters parameters,
+ CancellationToken cancellationToken)
+ {
+ // Capture the attestation flag before calling base
+ _isAttestationRequested = parameters.IsAttestationRequested;
+ return await base.AuthenticateAsync(parameters, cancellationToken).ConfigureAwait(false);
+ }
+
private async Task ExecuteCertificateRequestAsync(
string clientId,
string attestationEndpoint,
@@ -181,14 +191,8 @@ private async Task ExecuteCertificateRequestAsync(
{ OAuth2Header.XMsCorrelationId, _requestContext.CorrelationId.ToString() }
};
- if (managedIdentityKeyInfo.Type != ManagedIdentityKeyType.KeyGuard)
- {
- throw new MsalClientException(
- "mtls_pop_requires_keyguard",
- "[ImdsV2] mTLS Proof-of-Possession requires a KeyGuard-backed key. Enable KeyGuard or use a KeyGuard-supported environment.");
- }
-
- // Ask helper for JWT only for KeyGuard keys
+ // Attempt attestation only for KeyGuard keys when provider is available
+ // For non-KeyGuard keys (Hardware, InMemory), proceed with non-attested flow
string attestationJwt = string.Empty;
var attestationUri = new Uri(attestationEndpoint);
@@ -200,6 +204,10 @@ private async Task ExecuteCertificateRequestAsync(
managedIdentityKeyInfo,
_requestContext.UserCancellationToken).ConfigureAwait(false);
}
+ else
+ {
+ _requestContext.Logger.Info($"[ImdsV2] Using {managedIdentityKeyInfo.Type} key. Proceeding with non-attested mTLS PoP flow.");
+ }
var certificateRequestBody = new CertificateRequestBody()
{
@@ -261,6 +269,22 @@ protected override async Task CreateRequestAsync(string
{
CsrMetadata csrMetadata = await GetCsrMetadataAsync(_requestContext).ConfigureAwait(false);
+ // Validate that mTLS PoP requires KeyGuard - fail fast before network calls
+ if (_isMtlsPopRequested)
+ {
+ IManagedIdentityKeyProvider keyProvider = _requestContext.ServiceBundle.PlatformProxy.ManagedIdentityKeyProvider;
+ ManagedIdentityKeyInfo keyInfo = await keyProvider
+ .GetOrCreateKeyAsync(_requestContext.Logger, _requestContext.UserCancellationToken)
+ .ConfigureAwait(false);
+
+ if (keyInfo.Type != ManagedIdentityKeyType.KeyGuard)
+ {
+ throw new MsalClientException(
+ "mtls_pop_requires_keyguard",
+ $"[ImdsV2] mTLS Proof-of-Possession requires KeyGuard keys. Current key type: {keyInfo.Type}");
+ }
+ }
+
string certCacheKey = _requestContext.ServiceBundle.Config.ClientId;
MtlsBindingInfo mtlsBinding = await GetOrCreateMtlsBindingAsync(
@@ -357,39 +381,51 @@ private async Task GetAttestationJwtAsync(
ManagedIdentityKeyInfo keyInfo,
CancellationToken cancellationToken)
{
- // Provider is a local dependency; missing provider is a client error
- var provider = _requestContext.AttestationTokenProvider;
+ // Check if attestation was requested via WithAttestationSupport()
+ if (!_isAttestationRequested)
+ {
+ _requestContext.Logger.Info("[ImdsV2] Attestation not requested. Proceeding with non-attested flow.");
+ return null; // Null attestation token indicates non-attested flow
+ }
+
+ // Check if an attestation provider has been registered
+ var attestationProvider = AttestationProviderRegistry.Provider;
+ if (attestationProvider == null)
+ {
+ throw new MsalClientException(
+ "attestation_not_configured",
+ "[ImdsV2] Attestation was requested but no attestation provider is registered. " +
+ "Ensure you reference the Microsoft.Identity.Client.KeyAttestation package.");
+ }
// KeyGuard requires RSACng on Windows
- if (keyInfo.Type == ManagedIdentityKeyType.KeyGuard &&
- keyInfo.Key is not System.Security.Cryptography.RSACng rsaCng)
+ if (keyInfo.Key is not System.Security.Cryptography.RSACng rsaCng)
{
throw new MsalClientException(
"keyguard_requires_cng",
"[ImdsV2] KeyGuard attestation currently supports only RSA CNG keys on Windows.");
}
- // Attestation token input
- var input = new AttestationTokenInput
- {
- ClientId = clientId,
- AttestationEndpoint = attestationEndpoint,
- KeyHandle = (keyInfo.Key as System.Security.Cryptography.RSACng)?.Key.Handle
- };
-
- // response from provider
- var response = await provider(input, cancellationToken).ConfigureAwait(false);
-
- // Validate response
- if (response == null || string.IsNullOrWhiteSpace(response.AttestationToken))
+ // Call attestation via the registered provider
+ AttestationResult attestationResult = await attestationProvider.AttestKeyGuardAsync(
+ attestationEndpoint.AbsoluteUri,
+ rsaCng.Key.Handle,
+ clientId,
+ cancellationToken).ConfigureAwait(false);
+
+ // Validate and return the attestation JWT
+ if (attestationResult != null &&
+ attestationResult.Status == AttestationStatus.Success &&
+ !string.IsNullOrWhiteSpace(attestationResult.Jwt))
{
- throw new MsalClientException(
- "attestation_failed",
- "[ImdsV2] Attestation provider failed to return an attestation token.");
+ return attestationResult.Jwt;
}
- // Return the JWT
- return response.AttestationToken;
+ throw new MsalClientException(
+ "attestation_failed",
+ $"[ImdsV2] Key Attestation failed " +
+ $"(status={attestationResult?.Status}, " +
+ $"code={attestationResult?.NativeErrorCode}). {attestationResult?.ErrorMessage}");
}
private Task GetOrCreateMtlsBindingAsync(
diff --git a/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs b/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs
index d6c67f8270..36547b51ab 100644
--- a/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs
+++ b/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs
@@ -7,7 +7,7 @@
[assembly: InternalsVisibleTo("Microsoft.Identity.Client.Desktop" + KeyTokens.MSAL)]
[assembly: InternalsVisibleTo("Microsoft.Identity.Client.Desktop.WinUI3" + KeyTokens.MSAL)]
[assembly: InternalsVisibleTo("Microsoft.Identity.Client.Broker" + KeyTokens.MSAL)]
-[assembly: InternalsVisibleTo("Microsoft.Identity.Client.MtlsPop" + KeyTokens.MSAL)]
+[assembly: InternalsVisibleTo("Microsoft.Identity.Client.KeyAttestation" + KeyTokens.MSAL)]
[assembly: InternalsVisibleTo("Microsoft.Identity.Test.Unit" + KeyTokens.MSAL)]
[assembly: InternalsVisibleTo("Microsoft.Identity.Test.Common" + KeyTokens.MSAL)]
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
index ed17fdb6a4..12e767a98e 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
@@ -6,3 +6,5 @@ Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task
+Microsoft.Identity.Client.ManagedIdentityPopExtensions
+static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
index ed17fdb6a4..12e767a98e 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
@@ -6,3 +6,5 @@ Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task
+Microsoft.Identity.Client.ManagedIdentityPopExtensions
+static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt
index ed17fdb6a4..12e767a98e 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt
@@ -6,3 +6,5 @@ Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task
+Microsoft.Identity.Client.ManagedIdentityPopExtensions
+static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt
index ed17fdb6a4..12e767a98e 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt
@@ -6,3 +6,5 @@ Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task
+Microsoft.Identity.Client.ManagedIdentityPopExtensions
+static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
index ed17fdb6a4..12e767a98e 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
@@ -6,3 +6,5 @@ Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task
+Microsoft.Identity.Client.ManagedIdentityPopExtensions
+static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
index ed17fdb6a4..12e767a98e 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
@@ -6,3 +6,5 @@ Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task
+Microsoft.Identity.Client.ManagedIdentityPopExtensions
+static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
diff --git a/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs b/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs
index 4542e9c210..598f8d698d 100644
--- a/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs
+++ b/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs
@@ -28,13 +28,11 @@ If any prerequisite is missing (e.g., VBS off, endpoint unset, native DLL absent
the test exits early with Assert.Inconclusive instead of failing the overall build.
*/
-using Microsoft.Identity.Client.MtlsPop.Attestation;
-using Microsoft.Identity.Test.Common.Core.Helpers;
+using Microsoft.Identity.Client.KeyAttestation;
+using Microsoft.Identity.Client.KeyAttestation.Attestation;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
-using System.Runtime.InteropServices;
using System.Security.Cryptography;
-using Microsoft.Identity.Client.MtlsPop;
using System.Threading.Tasks;
using System.Threading;
@@ -134,7 +132,7 @@ public void Attest_KeyGuardKey_OnAzureArc_Succeeds()
Assert.Inconclusive("Key was created but not KeyGuard-protected. Is KeyGuard/VBS enabled on this machine?");
}
- // Use the new public AttestationClient from the MtlsPop package. :contentReference[oaicite:2]{index=2}
+ // Use the new public AttestationClient from the KeyAttestation package. :contentReference[oaicite:2]{index=2}
using var client = new AttestationClient();
var result = client.Attest(endpoint, key.Handle, clientId);
diff --git a/tests/Microsoft.Identity.Test.E2e/ManagedIdentityAzureArcTests.cs b/tests/Microsoft.Identity.Test.E2e/ManagedIdentityAzureArcTests.cs
index bf541b8396..2c99cf63e2 100644
--- a/tests/Microsoft.Identity.Test.E2e/ManagedIdentityAzureArcTests.cs
+++ b/tests/Microsoft.Identity.Test.E2e/ManagedIdentityAzureArcTests.cs
@@ -5,7 +5,6 @@
using Microsoft.Identity.Client.AppConfig;
using Microsoft.Identity.Test.Common.Core.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
-using System;
using System.Threading.Tasks;
namespace Microsoft.Identity.Test.E2E
diff --git a/tests/Microsoft.Identity.Test.E2e/ManagedIdentityImdsTests.cs b/tests/Microsoft.Identity.Test.E2e/ManagedIdentityImdsTests.cs
index d0b149c232..1947b1d083 100644
--- a/tests/Microsoft.Identity.Test.E2e/ManagedIdentityImdsTests.cs
+++ b/tests/Microsoft.Identity.Test.E2e/ManagedIdentityImdsTests.cs
@@ -3,7 +3,6 @@
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.AppConfig;
-using Microsoft.Identity.Client.MtlsPop;
using Microsoft.Identity.Test.Common.Core.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
diff --git a/tests/Microsoft.Identity.Test.E2e/Microsoft.Identity.Test.E2E.MSI.csproj b/tests/Microsoft.Identity.Test.E2e/Microsoft.Identity.Test.E2E.MSI.csproj
index ae7c12399c..377a72a0f7 100644
--- a/tests/Microsoft.Identity.Test.E2e/Microsoft.Identity.Test.E2E.MSI.csproj
+++ b/tests/Microsoft.Identity.Test.E2e/Microsoft.Identity.Test.E2E.MSI.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs
index 7e61d7d908..b36321771b 100644
--- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs
+++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs
@@ -1,21 +1,20 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
-using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.AppConfig;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.Internal.Logger;
+using Microsoft.Identity.Client.KeyAttestation;
using Microsoft.Identity.Client.ManagedIdentity;
using Microsoft.Identity.Client.ManagedIdentity.KeyProviders;
using Microsoft.Identity.Client.ManagedIdentity.V2;
-using Microsoft.Identity.Client.MtlsPop;
using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
using Microsoft.Identity.Client.PlatformsCommon.Shared;
using Microsoft.Identity.Test.Common.Core.Helpers;
@@ -41,14 +40,6 @@ public class ImdsV2Tests : TestBase
enablePiiLogging: false
);
- // Fake attestation provider used by mTLS PoP tests so we never hit the real service
- private static readonly Func>
- s_fakeAttestationProvider =
- (input, ct) => Task.FromResult(new AttestationTokenResponse
- {
- AttestationToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.fake.attestation.sig"
- });
-
public const string Bearer = "Bearer";
public const string MTLSPoP = "mtls_pop";
@@ -61,6 +52,16 @@ public void ImdsV2Tests_Init()
// A broad sweep is simplest and safe for our fake endpoints/certs
ImdsV2TestStoreCleaner.RemoveAllTestArtifacts();
}
+
+ // Register fake attestation provider for tests
+ AttestationProviderRegistry.RegisterProvider(TestAttestationProviders.CreateFakeProvider());
+ }
+
+ [TestCleanup]
+ public void ImdsV2Tests_Cleanup()
+ {
+ // Clear provider after each test
+ AttestationProviderRegistry.ClearProvider();
}
private void AddMocksToGetEntraToken(
@@ -180,7 +181,7 @@ public async Task mTLSPopTokenHappyPath(
var result = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.IsNotNull(result);
@@ -191,7 +192,7 @@ public async Task mTLSPopTokenHappyPath(
result = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.IsNotNull(result);
@@ -223,7 +224,7 @@ public async Task mTLSPopTokenIsPerIdentity(
var result = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.IsNotNull(result);
@@ -257,7 +258,7 @@ public async Task mTLSPopTokenIsPerIdentity(
var result2 = await managedIdentityApp2.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.IsNotNull(result2);
@@ -268,7 +269,7 @@ public async Task mTLSPopTokenIsPerIdentity(
result2 = await managedIdentityApp2.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.IsNotNull(result2);
@@ -302,7 +303,7 @@ public async Task mTLSPopTokenIsReAcquiredWhenCertificateIsExpired(
var result = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.IsNotNull(result);
@@ -317,7 +318,7 @@ public async Task mTLSPopTokenIsReAcquiredWhenCertificateIsExpired(
result = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.IsNotNull(result);
@@ -384,7 +385,7 @@ public async Task ApplicationsCannotSwitchBetweenImdsVersionsForPreview(
var result = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
//.WithMtlsProofOfPossession() - excluding this will cause fallback to ImdsV1
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.IsNotNull(result);
@@ -642,7 +643,7 @@ public void AttachPrivateKeyToCert_NullPrivateKey_ThrowsArgumentNullException()
#region Attestation Tests
[TestMethod]
- public async Task MtlsPop_AttestationProviderMissing_ThrowsClientException()
+ public async Task MtlsPop_NoAttestationProvider_UsesNonAttestedFlow()
{
using (new EnvVariableContext())
using (var httpManager = new MockHttpManager())
@@ -651,17 +652,19 @@ public async Task MtlsPop_AttestationProviderMissing_ThrowsClientException()
var mi = await CreateManagedIdentityAsync(httpManager, managedIdentityKeyType: ManagedIdentityKeyType.KeyGuard).ConfigureAwait(false);
- // CreateManagedIdentityAsync does a probe; Add one more CSR response for the actual acquire.
- httpManager.AddMockHandler(MockHelpers.MockCsrResponse());
+ // Add mocks for successful non-attested flow (CSR + issuecredential + token)
+ // Note: No attestation token in the certificate request
+ AddMocksToGetEntraToken(httpManager);
- var ex = await Assert.ThrowsExceptionAsync(async () =>
- await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
- .WithMtlsProofOfPossession()
- // Intentionally DO NOT call .WithAttestationProviderForTests(...)
- .ExecuteAsync().ConfigureAwait(false)
- ).ConfigureAwait(false);
+ var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
+ .WithMtlsProofOfPossession()
+ // Intentionally DO NOT call .WithAttestationProviderForTests(...)
+ .ExecuteAsync().ConfigureAwait(false);
- Assert.AreEqual("attestation_failure", ex.ErrorCode);
+ Assert.IsNotNull(result);
+ Assert.AreEqual(MTLSPoP, result.TokenType, "Should get mTLS PoP token without attestation provider");
+ Assert.IsNotNull(result.BindingCertificate, "Should have binding certificate even without attestation");
+ Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource);
}
}
@@ -678,13 +681,13 @@ public async Task MtlsPop_AttestationProviderReturnsNull_ThrowsClientException()
// CreateManagedIdentityAsync does a probe; Add one more CSR response for the actual acquire.
httpManager.AddMockHandler(MockHelpers.MockCsrResponse());
- var nullProvider = new Func>(
- (input, ct) => Task.FromResult(null));
+ // Register null provider for this test
+ AttestationProviderRegistry.RegisterProvider(TestAttestationProviders.CreateNullProvider());
var ex = await Assert.ThrowsExceptionAsync(async () =>
await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(nullProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false)
).ConfigureAwait(false);
@@ -705,13 +708,13 @@ public async Task MtlsPop_AttestationProviderReturnsEmptyToken_ThrowsClientExcep
// CreateManagedIdentityAsync does a probe; Add one more CSR response for the actual acquire.
httpManager.AddMockHandler(MockHelpers.MockCsrResponse());
- var emptyProvider = new Func>(
- (input, ct) => Task.FromResult(new AttestationTokenResponse { AttestationToken = " " }));
+ // Register empty provider for this test
+ AttestationProviderRegistry.RegisterProvider(TestAttestationProviders.CreateEmptyProvider());
var ex = await Assert.ThrowsExceptionAsync(async () =>
await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(emptyProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false)
).ConfigureAwait(false);
@@ -762,23 +765,19 @@ public async Task mTLSPop_ForceRefresh_UsesCachedCert_NoIssueCredential_PostsCan
// First acquire: full flow (CSR + issuecredential + token)
AddMocksToGetEntraToken(httpManager);
- int attestationCalls = 0;
- Func> countingProvider =
- (input, ct) =>
- {
- Interlocked.Increment(ref attestationCalls);
- return Task.FromResult(new AttestationTokenResponse { AttestationToken = "header.payload.sig" });
- };
+ // Register counting provider for this test
+ var countingProvider = TestAttestationProviders.CreateCountingProvider();
+ AttestationProviderRegistry.RegisterProvider(countingProvider);
var result1 = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(countingProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.AreEqual(ImdsV2Tests.MTLSPoP, result1.TokenType);
Assert.IsNotNull(result1.BindingCertificate);
Assert.AreEqual(TokenSource.IdentityProvider, result1.AuthenticationResultMetadata.TokenSource);
- Assert.AreEqual(1, attestationCalls, "Attestation must be called exactly once on first mint.");
+ Assert.AreEqual(1, countingProvider.CallCount, "Attestation must be called exactly once on first mint.");
// Second acquire: FORCE REFRESH to bypass token cache.
// Expect: 1x getplatformmetadata + token request. NO /issuecredential. Attestation NOT called again.
@@ -792,13 +791,13 @@ public async Task mTLSPop_ForceRefresh_UsesCachedCert_NoIssueCredential_PostsCan
var result2 = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithForceRefresh(true) // if your API is parameterless, use .WithForceRefresh()
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(countingProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.AreEqual(ImdsV2Tests.MTLSPoP, result2.TokenType);
Assert.IsNotNull(result2.BindingCertificate);
Assert.AreEqual(TokenSource.IdentityProvider, result2.AuthenticationResultMetadata.TokenSource);
- Assert.AreEqual(1, attestationCalls, "Attestation must NOT be invoked on refresh when cert is cached.");
+ Assert.AreEqual(1, countingProvider.CallCount, "Attestation must NOT be invoked on refresh when cert is cached.");
}
}
@@ -822,11 +821,11 @@ public async Task mTLSPop_CachedCertIsPerIdentity_OnRefresh_Identity1UsesCache_I
var result1 = await mi1.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.AreEqual(TokenSource.IdentityProvider, result1.AuthenticationResultMetadata.TokenSource);
- // Identity 1 – force refresh (should use cached cert → NO /issuecredential)
+ // Identity 1 – force refresh (should use cached cert ? NO /issuecredential)
MockHelpers.AddMocksToGetEntraTokenUsingCachedCert(
httpManager,
_identityLoggerAdapter,
@@ -840,19 +839,19 @@ public async Task mTLSPop_CachedCertIsPerIdentity_OnRefresh_Identity1UsesCache_I
var result1Refresh = await mi1.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithForceRefresh(true)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync()
.ConfigureAwait(false);
Assert.AreEqual(TokenSource.IdentityProvider, result1Refresh.AuthenticationResultMetadata.TokenSource);
- // Identity 2 – new identity (should MINT again → requires /issuecredential)
+ // Identity 2 – new identity (should MINT again ? requires /issuecredential)
var mi2 = await CreateManagedIdentityAsync(httpManager, userAssignedIdentityId, userAssignedId2, addProbeMock: false, addSourceCheck: false, managedIdentityKeyType: ManagedIdentityKeyType.KeyGuard).ConfigureAwait(false);
AddMocksToGetEntraToken(httpManager, userAssignedIdentityId, userAssignedId2);
var result2 = await mi2.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.AreEqual(TokenSource.IdentityProvider, result2.AuthenticationResultMetadata.TokenSource);
}
@@ -894,7 +893,6 @@ public async Task mTLSPopTokenHappyPath_LongLivedCert_IdentityMapping(
var first = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
.ExecuteAsync().ConfigureAwait(false);
Assert.IsNotNull(first);
@@ -907,7 +905,6 @@ public async Task mTLSPopTokenHappyPath_LongLivedCert_IdentityMapping(
var second = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
.ExecuteAsync().ConfigureAwait(false);
Assert.IsNotNull(second);
@@ -958,7 +955,7 @@ public async Task mTLSPop_LongLivedCerts_SamiVsUami_DistinctAndCached(
managedIdentityKeyType: ManagedIdentityKeyType.KeyGuard).ConfigureAwait(false);
// --- First acquire (MINT): return the identity-specific cert we want ---
- // SAMI → use rawCertSami ; UAMI (any alias) → use rawCertUami
+ // SAMI ? use rawCertSami ; UAMI (any alias) ? use rawCertUami
string selectedCert = (userAssignedIdentityId == UserAssignedIdentityId.None) ? rawCertSami : rawCertUami;
AddMocksToGetEntraToken(
@@ -969,7 +966,7 @@ public async Task mTLSPop_LongLivedCerts_SamiVsUami_DistinctAndCached(
var first = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.IsNotNull(first);
@@ -984,7 +981,7 @@ public async Task mTLSPop_LongLivedCerts_SamiVsUami_DistinctAndCached(
// --- Second acquire: cached; cert should be the SAME (cached binding cert) ---
var second = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.IsNotNull(second);
@@ -1031,7 +1028,7 @@ public async Task mTLSPop_LongLivedCerts_SamiAndUami_ThumbprintsDiffer_AndEachCa
var s1 = await sami.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.IsNotNull(s1.BindingCertificate);
@@ -1042,7 +1039,7 @@ public async Task mTLSPop_LongLivedCerts_SamiAndUami_ThumbprintsDiffer_AndEachCa
// cached
var s2 = await sami.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.AreEqual(TokenSource.Cache, s2.AuthenticationResultMetadata.TokenSource);
@@ -1066,7 +1063,7 @@ public async Task mTLSPop_LongLivedCerts_SamiAndUami_ThumbprintsDiffer_AndEachCa
var u1 = await uami.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.IsNotNull(u1.BindingCertificate);
@@ -1076,7 +1073,7 @@ public async Task mTLSPop_LongLivedCerts_SamiAndUami_ThumbprintsDiffer_AndEachCa
var u2 = await uami.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.AreEqual(TokenSource.Cache, u2.AuthenticationResultMetadata.TokenSource);
@@ -1089,8 +1086,8 @@ public async Task mTLSPop_LongLivedCerts_SamiAndUami_ThumbprintsDiffer_AndEachCa
///
/// Subject mapping test that mirrors prod: CN=canonical client_id, DC=tenant id.
- /// - SAMI → CN = Constants.ManagedIdentityDefaultClientId
- /// - UAMI (client_id|object_id|resource_id) → CN = TestConstants.ClientId (canonical)
+ /// - SAMI ? CN = Constants.ManagedIdentityDefaultClientId
+ /// - UAMI (client_id|object_id|resource_id) ? CN = TestConstants.ClientId (canonical)
/// Both assert DC = TestConstants.TenantId and cert cache reuse.
///
[DataTestMethod]
@@ -1123,7 +1120,6 @@ public async Task mTLSPop_SubjectCnDc_MatchesMetadata_AndCaches(
var first = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
.ExecuteAsync().ConfigureAwait(false);
Assert.AreEqual(MTLSPoP, first.TokenType, $"[{label}]");
@@ -1131,7 +1127,6 @@ public async Task mTLSPop_SubjectCnDc_MatchesMetadata_AndCaches(
var second = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
.ExecuteAsync().ConfigureAwait(false);
Assert.AreEqual(TokenSource.Cache, second.AuthenticationResultMetadata.TokenSource, $"[{label}] cache");
@@ -1152,7 +1147,7 @@ public async Task mTLSPoP_Uami_ClientIdThenObjectId_MintsThenCaches_SubjectCNIsC
string expectedDc = TestConstants.TenantId;
string rawCert = CreateRawCertForCsrKeyWithCnDc(expectedCn, expectedDc, DateTimeOffset.UtcNow.AddYears(20));
- // (1) client_id → MINT (CSR + issuecredential + token)
+ // (1) client_id ? MINT (CSR + issuecredential + token)
var miClientId = await CreateManagedIdentityAsync(
httpManager,
userAssignedIdentityId: UserAssignedIdentityId.ClientId,
@@ -1167,14 +1162,14 @@ public async Task mTLSPoP_Uami_ClientIdThenObjectId_MintsThenCaches_SubjectCNIsC
var c1 = await miClientId.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.AreEqual(MTLSPoP, c1.TokenType);
Assert.AreEqual(TokenSource.IdentityProvider, c1.AuthenticationResultMetadata.TokenSource);
AssertCertSubjectCnDc(c1.BindingCertificate, expectedCn, expectedDc, "[client_id]");
- // (2) object_id → MINT (new alias → its own cache key)
+ // (2) object_id ? MINT (new alias ? its own cache key)
var miObjectId = await CreateManagedIdentityAsync(
httpManager,
userAssignedIdentityId: UserAssignedIdentityId.ObjectId,
@@ -1191,7 +1186,7 @@ public async Task mTLSPoP_Uami_ClientIdThenObjectId_MintsThenCaches_SubjectCNIsC
var o1 = await miObjectId.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.AreEqual(MTLSPoP, o1.TokenType);
@@ -1199,7 +1194,7 @@ public async Task mTLSPoP_Uami_ClientIdThenObjectId_MintsThenCaches_SubjectCNIsC
AssertCertSubjectCnDc(o1.BindingCertificate, expectedCn, expectedDc, "[object_id first]");
var objectIdThumb = o1.BindingCertificate.Thumbprint;
- // (3) object_id again → CACHED
+ // (3) object_id again ? CACHED
var o2 = await miObjectId.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
.ExecuteAsync().ConfigureAwait(false);
@@ -1227,7 +1222,7 @@ public async Task mTLSPoP_Uami_ClientIdThenAlias_MintsThenCaches_SubjectCNIsClie
string expectedDc = TestConstants.TenantId;
string rawCert = CreateRawCertForCsrKeyWithCnDc(expectedCn, expectedDc, DateTimeOffset.UtcNow.AddYears(20));
- // (1) client_id → MINT (CSR + issuecredential + token)
+ // (1) client_id ? MINT (CSR + issuecredential + token)
var miClientId = await CreateManagedIdentityAsync(
httpManager,
userAssignedIdentityId: UserAssignedIdentityId.ClientId,
@@ -1242,14 +1237,14 @@ public async Task mTLSPoP_Uami_ClientIdThenAlias_MintsThenCaches_SubjectCNIsClie
var c1 = await miClientId.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.AreEqual(MTLSPoP, c1.TokenType, "[client_id]");
Assert.AreEqual(TokenSource.IdentityProvider, c1.AuthenticationResultMetadata.TokenSource, "[client_id] should mint");
AssertCertSubjectCnDc(c1.BindingCertificate, expectedCn, expectedDc, "[client_id]");
- // (2) alias (object_id/resource_id) → MINT (new alias → new cache key)
+ // (2) alias (object_id/resource_id) ? MINT (new alias ? new cache key)
var miAlias = await CreateManagedIdentityAsync(
httpManager,
userAssignedIdentityId: aliasKind,
@@ -1266,7 +1261,7 @@ public async Task mTLSPoP_Uami_ClientIdThenAlias_MintsThenCaches_SubjectCNIsClie
var a1 = await miAlias.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.AreEqual(MTLSPoP, a1.TokenType, $"[{label} first]");
@@ -1274,10 +1269,10 @@ public async Task mTLSPoP_Uami_ClientIdThenAlias_MintsThenCaches_SubjectCNIsClie
AssertCertSubjectCnDc(a1.BindingCertificate, expectedCn, expectedDc, $"[{label} first]");
var aliasThumb = a1.BindingCertificate.Thumbprint;
- // (3) alias again → CACHED (no /issuecredential; no extra mocks needed)
+ // (3) alias again ? CACHED (no /issuecredential; no extra mocks needed)
var a2 = await miAlias.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.AreEqual(TokenSource.Cache, a2.AuthenticationResultMetadata.TokenSource, $"[{label} second] should be cached");
@@ -1321,7 +1316,7 @@ public async Task mTLSPop_ShortLivedCert_LessThan24h_NotCached_ReMints(
var first = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.AreEqual(TokenSource.IdentityProvider, first.AuthenticationResultMetadata.TokenSource, $"[{label}] first must mint.");
@@ -1332,7 +1327,7 @@ public async Task mTLSPop_ShortLivedCert_LessThan24h_NotCached_ReMints(
var second = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithForceRefresh(true) // <-- key change
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.AreEqual(TokenSource.IdentityProvider, second.AuthenticationResultMetadata.TokenSource, $"[{label}] second must mint (no cert cache for <24h).");
@@ -1356,7 +1351,7 @@ public async Task mTLSPop_CertAtLeast24h_IsCached_ReusedOnSecondAcquire(
ManagedIdentityClient.ResetSourceForTest();
SetEnvironmentVariables(ManagedIdentitySource.ImdsV2, TestConstants.ImdsEndpoint);
- // NotAfter >= 24h + 1min → should be cached and reused
+ // NotAfter >= 24h + 1min ? should be cached and reused
var rawLong = CreateRawCertForCsrKeyWithCnDc(
cn: (idKind == UserAssignedIdentityId.None ? Constants.ManagedIdentityDefaultClientId : TestConstants.ClientId),
dc: TestConstants.TenantId,
@@ -1365,20 +1360,20 @@ public async Task mTLSPop_CertAtLeast24h_IsCached_ReusedOnSecondAcquire(
var mi = await CreateManagedIdentityAsync(httpManager, idKind, idValue, managedIdentityKeyType: ManagedIdentityKeyType.KeyGuard)
.ConfigureAwait(false);
- // First acquire → MINT
+ // First acquire ? MINT
AddMocksToGetEntraToken(httpManager, idKind, idValue, certificateRequestCertificate: rawLong);
var first = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.AreEqual(TokenSource.IdentityProvider, first.AuthenticationResultMetadata.TokenSource, $"[{label}] first must mint long-lived cert.");
- // Second acquire → CACHED (no /issuecredential mocks needed)
+ // Second acquire ? CACHED (no /issuecredential mocks needed)
var second = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
- .WithAttestationProviderForTests(s_fakeAttestationProvider)
+ .WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);
Assert.AreEqual(TokenSource.Cache, second.AuthenticationResultMetadata.TokenSource, $"[{label}] second should be cache.");
diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/TestAttestationProviders.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/TestAttestationProviders.cs
new file mode 100644
index 0000000000..160026233e
--- /dev/null
+++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/TestAttestationProviders.cs
@@ -0,0 +1,120 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Identity.Client.ManagedIdentity;
+
+namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests
+{
+ ///
+ /// Test attestation providers for unit testing.
+ ///
+ internal static class TestAttestationProviders
+ {
+ ///
+ /// Fake attestation provider that returns a mock JWT.
+ ///
+ public static IAttestationProvider CreateFakeProvider()
+ {
+ return new FakeAttestationProvider();
+ }
+
+ ///
+ /// Attestation provider that returns null (for error testing).
+ ///
+ public static IAttestationProvider CreateNullProvider()
+ {
+ return new NullAttestationProvider();
+ }
+
+ ///
+ /// Attestation provider that returns empty/whitespace token (for error testing).
+ ///
+ public static IAttestationProvider CreateEmptyProvider()
+ {
+ return new EmptyAttestationProvider();
+ }
+
+ ///
+ /// Attestation provider that counts calls.
+ ///
+ public static CountingAttestationProvider CreateCountingProvider()
+ {
+ return new CountingAttestationProvider();
+ }
+
+ private class FakeAttestationProvider : IAttestationProvider
+ {
+ public Task AttestKeyGuardAsync(
+ string attestationEndpoint,
+ SafeHandle keyHandle,
+ string clientId,
+ CancellationToken cancellationToken)
+ {
+ return Task.FromResult(new AttestationResult
+ {
+ Status = AttestationStatus.Success,
+ Jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.fake.attestation.sig",
+ ErrorMessage = null,
+ NativeErrorCode = 0
+ });
+ }
+ }
+
+ private class NullAttestationProvider : IAttestationProvider
+ {
+ public Task AttestKeyGuardAsync(
+ string attestationEndpoint,
+ SafeHandle keyHandle,
+ string clientId,
+ CancellationToken cancellationToken)
+ {
+ return Task.FromResult(null);
+ }
+ }
+
+ private class EmptyAttestationProvider : IAttestationProvider
+ {
+ public Task AttestKeyGuardAsync(
+ string attestationEndpoint,
+ SafeHandle keyHandle,
+ string clientId,
+ CancellationToken cancellationToken)
+ {
+ return Task.FromResult(new AttestationResult
+ {
+ Status = AttestationStatus.Success,
+ Jwt = " ",
+ ErrorMessage = null,
+ NativeErrorCode = 0
+ });
+ }
+ }
+
+ public class CountingAttestationProvider : IAttestationProvider
+ {
+ private int _callCount;
+
+ public int CallCount => _callCount;
+
+ public Task AttestKeyGuardAsync(
+ string attestationEndpoint,
+ SafeHandle keyHandle,
+ string clientId,
+ CancellationToken cancellationToken)
+ {
+ Interlocked.Increment(ref _callCount);
+ return Task.FromResult(new AttestationResult
+ {
+ Status = AttestationStatus.Success,
+ Jwt = "header.payload.sig",
+ ErrorMessage = null,
+ NativeErrorCode = 0
+ });
+ }
+ }
+ }
+}
diff --git a/tests/Microsoft.Identity.Test.Unit/Microsoft.Identity.Test.Unit.csproj b/tests/Microsoft.Identity.Test.Unit/Microsoft.Identity.Test.Unit.csproj
index a6f295c6d2..71989abaa8 100644
--- a/tests/Microsoft.Identity.Test.Unit/Microsoft.Identity.Test.Unit.csproj
+++ b/tests/Microsoft.Identity.Test.Unit/Microsoft.Identity.Test.Unit.csproj
@@ -16,7 +16,7 @@
-
+
{3433eb33-114a-4db7-bc57-14f17f55da3c}
Microsoft.Identity.Client
diff --git a/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/ManagedIdentityAppVM.csproj b/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/ManagedIdentityAppVM.csproj
index ea3a7b6aec..150d7f869c 100644
--- a/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/ManagedIdentityAppVM.csproj
+++ b/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/ManagedIdentityAppVM.csproj
@@ -8,7 +8,6 @@
-
diff --git a/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs b/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs
index 68eade0c26..48175a3e70 100644
--- a/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs
+++ b/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs
@@ -9,7 +9,6 @@
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.AppConfig;
using Microsoft.IdentityModel.Abstractions;
-using Microsoft.Identity.Client.MtlsPop;
internal class Program
{