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 {