From be65cbaa935899195b6c5de439ee77767cb1969b Mon Sep 17 00:00:00 2001 From: Ryan Newington Date: Fri, 21 Aug 2026 09:32:53 +1000 Subject: [PATCH 1/9] Add transparent bitmap support for credential tiles --- .../BitmapControlTests.cs | 97 +++++++++++++++++++ ...et.CredentialProvider.UnitTests.x86.csproj | 1 + .../Controls/BitmapControl.cs | 56 ++++++++--- .../Controls/CredentialProviderLogoControl.cs | 5 +- .../Controls/UserTileControl.cs | 6 +- ...ialTile3.ICredentialProviderCredential3.cs | 4 +- .../CredentialTile3.cs | 2 +- 7 files changed, 143 insertions(+), 28 deletions(-) create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/BitmapControlTests.cs diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/BitmapControlTests.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/BitmapControlTests.cs new file mode 100644 index 0000000..d543897 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/BitmapControlTests.cs @@ -0,0 +1,97 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Runtime.InteropServices; +using NUnit.Framework; + +namespace Lithnet.CredentialProvider.UnitTests +{ + public class BitmapControlTests + { + [Test] + public void TransparentBufferPreservesAlphaChannel() + { + using (Bitmap source = new Bitmap(2, 1, PixelFormat.Format32bppArgb)) + { + source.SetPixel(0, 0, Color.FromArgb(0, 10, 20, 30)); + source.SetPixel(1, 0, Color.FromArgb(128, 40, 50, 60)); + + var control = new UserTileControl("image", "Image", source); + + byte[] bytes = GetBitmapBuffer(control); + + Assert.That(bytes, Has.Length.GreaterThan(8)); + Assert.That(bytes[0], Is.EqualTo(0x89)); + Assert.That(bytes[1], Is.EqualTo(0x50)); + Assert.That(bytes[2], Is.EqualTo(0x4e)); + Assert.That(bytes[3], Is.EqualTo(0x47)); + + using (MemoryStream stream = new MemoryStream(bytes)) + using (Bitmap decoded = new Bitmap(stream)) + { + Assert.That(decoded.GetPixel(0, 0).A, Is.EqualTo(0)); + Assert.That(decoded.GetPixel(1, 0).A, Is.EqualTo(128)); + Assert.That(decoded.GetPixel(1, 0).R, Is.EqualTo(40)); + Assert.That(decoded.GetPixel(1, 0).G, Is.EqualTo(50)); + Assert.That(decoded.GetPixel(1, 0).B, Is.EqualTo(60)); + } + } + } + + [Test] + public void BitmapBufferDoesNotApplyConfiguredBackgroundColor() + { + using (Bitmap source = new Bitmap(1, 1, PixelFormat.Format32bppArgb)) + { + source.SetPixel(0, 0, Color.Transparent); + + var control = new UserTileControl("image", "Image", source) + { + BackgroundColor = Color.FromArgb(12, 34, 56) + }; + + byte[] bytes = GetBitmapBuffer(control); + + using (MemoryStream stream = new MemoryStream(bytes)) + using (Bitmap decoded = new Bitmap(stream)) + { + Assert.That(decoded.GetPixel(0, 0).A, Is.EqualTo(0)); + } + } + } + + [Test] + public void CloneCopiesBitmapAndBackgroundColor() + { + using (Bitmap source = new Bitmap(1, 1)) + { + var control = new UserTileControl("image", "Image", source) + { + BackgroundColor = Color.CornflowerBlue + }; + + var clone = (UserTileControl)control.Clone(); + + Assert.That(clone.Bitmap, Is.SameAs(source)); + Assert.That(clone.BackgroundColor, Is.EqualTo(Color.CornflowerBlue)); + } + } + + private static byte[] GetBitmapBuffer(BitmapControl control) + { + IntPtr buffer = control.GetBitmapBuffer(out uint size); + + try + { + byte[] bytes = new byte[size]; + Marshal.Copy(buffer, bytes, 0, checked((int)size)); + return bytes; + } + finally + { + Marshal.FreeCoTaskMem(buffer); + } + } + } +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x86/Lithnet.CredentialProvider.UnitTests.x86.csproj b/src/Lithnet.CredentialProvider.UnitTests.x86/Lithnet.CredentialProvider.UnitTests.x86.csproj index 31f57a7..70b55c6 100644 --- a/src/Lithnet.CredentialProvider.UnitTests.x86/Lithnet.CredentialProvider.UnitTests.x86.csproj +++ b/src/Lithnet.CredentialProvider.UnitTests.x86/Lithnet.CredentialProvider.UnitTests.x86.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Lithnet.CredentialProvider/Controls/BitmapControl.cs b/src/Lithnet.CredentialProvider/Controls/BitmapControl.cs index 15fa7ef..35c71b5 100644 --- a/src/Lithnet.CredentialProvider/Controls/BitmapControl.cs +++ b/src/Lithnet.CredentialProvider/Controls/BitmapControl.cs @@ -22,11 +22,16 @@ namespace Lithnet.CredentialProvider this.backgroundColor = Color.FromArgb(70, 70, 70); } - protected BitmapControl(BitmapControl source) : base(source) { } + protected BitmapControl(BitmapControl source) : base(source) + { + this.bitmap = source.bitmap; + this.backgroundColor = source.backgroundColor; + } /// - /// Specifies the background color that should replace any transparent elements of the image. This defaults to #707070 + /// Specifies the background color used to replace transparent pixels for and . This defaults to #707070. /// + /// This property does not apply to . public Color BackgroundColor { get { return this.backgroundColor; } @@ -53,10 +58,7 @@ namespace Lithnet.CredentialProvider { this.bitmap = value; - if (this.Events is ICredentialProviderCredentialEvents2 e) - { - e.SetFieldBitmap(this.Credential, this.Id, this.GetHBitmap()); - } + this.UpdateBitmap(); this.RaisePropertyChanged(); } @@ -76,26 +78,48 @@ namespace Lithnet.CredentialProvider internal IntPtr GetBitmapBuffer(out uint size) { size = 0; - var hbitmap = this.GetHBitmap(); - if (hbitmap == IntPtr.Zero) + if (this.bitmap == null) { return IntPtr.Zero; } - var image = Bitmap.FromHbitmap(hbitmap); - - IntPtr buffer = IntPtr.Zero; using (MemoryStream ms = new MemoryStream()) { - image.Save(ms, ImageFormat.Bmp); + this.bitmap.Save(ms, ImageFormat.Png); var bitmapBytes = ms.ToArray(); - size = (uint)bitmapBytes.Length; - buffer = Marshal.AllocCoTaskMem(bitmapBytes.Length); + size = checked((uint)bitmapBytes.Length); + IntPtr buffer = Marshal.AllocCoTaskMem(bitmapBytes.Length); Marshal.Copy(bitmapBytes, 0, buffer, bitmapBytes.Length); + return buffer; + } + } + + private void UpdateBitmap() + { + if (this.Credential is ICredentialProviderCredential3 && this.Events is ICredentialProviderCredentialEvents3 events3) + { + IntPtr buffer = this.GetBitmapBuffer(out uint size); + + try + { + events3.SetFieldBitmapBuffer(this.Credential, this.Id, size, buffer); + } + finally + { + if (buffer != IntPtr.Zero) + { + Marshal.FreeCoTaskMem(buffer); + } + } + + return; } - return buffer; + if (this.Events is ICredentialProviderCredentialEvents2 events2) + { + events2.SetFieldBitmap(this.Credential, this.Id, this.GetHBitmap()); + } } } -} \ No newline at end of file +} diff --git a/src/Lithnet.CredentialProvider/Controls/CredentialProviderLogoControl.cs b/src/Lithnet.CredentialProvider/Controls/CredentialProviderLogoControl.cs index 0f93a27..f8b11b2 100644 --- a/src/Lithnet.CredentialProvider/Controls/CredentialProviderLogoControl.cs +++ b/src/Lithnet.CredentialProvider/Controls/CredentialProviderLogoControl.cs @@ -35,10 +35,7 @@ namespace Lithnet.CredentialProvider internal override ControlBase Clone() { - var clone = new CredentialProviderLogoControl(this); - clone.Bitmap = this.Bitmap; - clone.BackgroundColor = this.BackgroundColor; - return clone; + return new CredentialProviderLogoControl(this); } } } \ No newline at end of file diff --git a/src/Lithnet.CredentialProvider/Controls/UserTileControl.cs b/src/Lithnet.CredentialProvider/Controls/UserTileControl.cs index 92eb0cf..f37deb3 100644 --- a/src/Lithnet.CredentialProvider/Controls/UserTileControl.cs +++ b/src/Lithnet.CredentialProvider/Controls/UserTileControl.cs @@ -32,11 +32,7 @@ namespace Lithnet.CredentialProvider internal override ControlBase Clone() { - var clone = new UserTileControl(this); - clone.Bitmap = this.Bitmap; - clone.BackgroundColor = this.BackgroundColor; - - return clone; + return new UserTileControl(this); } } } \ No newline at end of file diff --git a/src/Lithnet.CredentialProvider/CredentialTile3.ICredentialProviderCredential3.cs b/src/Lithnet.CredentialProvider/CredentialTile3.ICredentialProviderCredential3.cs index 088b9e5..892d37f 100644 --- a/src/Lithnet.CredentialProvider/CredentialTile3.ICredentialProviderCredential3.cs +++ b/src/Lithnet.CredentialProvider/CredentialTile3.ICredentialProviderCredential3.cs @@ -3,7 +3,7 @@ using Lithnet.CredentialProvider.Interop; namespace Lithnet.CredentialProvider { - internal abstract partial class CredentialTile3 : ICredentialProviderCredential3 + public abstract partial class CredentialTile3 : ICredentialProviderCredential3 { int ICredentialProviderCredential3.GetBitmapBufferValue(uint dwFieldID, out uint pImageBufferSize, out IntPtr ppImageBuffer) { @@ -18,7 +18,7 @@ namespace Lithnet.CredentialProvider if (this.Controls.TryGetControl(dwFieldID, FieldType.TileImage, out var instance)) { - var hbitmap = instance.GetBitmapBuffer(out pImageBufferSize); + ppImageBuffer = instance.GetBitmapBuffer(out pImageBufferSize); return HRESULT.S_OK; } diff --git a/src/Lithnet.CredentialProvider/CredentialTile3.cs b/src/Lithnet.CredentialProvider/CredentialTile3.cs index 20207b1..fa0f365 100644 --- a/src/Lithnet.CredentialProvider/CredentialTile3.cs +++ b/src/Lithnet.CredentialProvider/CredentialTile3.cs @@ -7,7 +7,7 @@ namespace Lithnet.CredentialProvider /// Represents a user credential tile that implements the functionality of and , but includes support for dynamically updating bitmap images. /// /// This interface is public, but undocumented by Microsoft. It is recommended to use tiles unless this specific functionality is needed - internal abstract partial class CredentialTile3 : CredentialTile2 + public abstract partial class CredentialTile3 : CredentialTile2 { protected CredentialTile3(CredentialProviderBase credentialProvider) : this(credentialProvider, null) { } From 43af44f87fa9e0312caa48d0665751173746b664 Mon Sep 17 00:00:00 2001 From: Ryan Newington Date: Fri, 28 Aug 2026 11:22:53 +1000 Subject: [PATCH 2/9] Expand framework support and COM ABI coverage Target .NET 8, 9, 10, Framework 4.7.2, and Framework 4.8. Rename samples to stable Core and Framework names. Add raw-vtable COM tests for x64, x86, and ARM64, and require the Azure architecture matrix before packaging. --- README.md | 6 +- azure-pipelines.yml | 52 +- ....CredentialProvider.UnitTests.arm64.csproj | 22 + .../ComInterop/AbiTestCredentialProvider.cs | 52 ++ .../ComInterop/AbiTestCredentialTile2.cs | 14 + .../ComInterop/ComInterfacePointer.cs | 79 +++ .../ComInterop/ComMarshal.cs | 17 + .../ComInterop/CredentialProviderAbi.cs | 59 +++ .../ComInterop/CredentialProviderAbiTests.cs | 450 ++++++++++++++++++ .../ComInterop/GetCredentialAtDelegate.cs | 8 + .../ComInterop/GetCredentialCountDelegate.cs | 8 + .../GetFieldDescriptorAtDelegate.cs | 8 + .../GetFieldDescriptorCountDelegate.cs | 8 + .../ComInterop/GetFieldStateDelegate.cs | 8 + .../ComInterop/GetStringValueDelegate.cs | 8 + .../ComInterop/GetUserCountDelegate.cs | 8 + .../ComInterop/GetUserSidDelegate.cs | 8 + .../ITestCredentialProviderUserArray.cs | 27 ++ ...NativeCredentialProviderFieldDescriptor.cs | 17 + .../ComInterop/ProcessArchitectureTests.cs | 27 ++ .../ComInterop/SetDeselectedDelegate.cs | 8 + .../ComInterop/SetSelectedDelegate.cs | 8 + .../ComInterop/SetUsageScenarioDelegate.cs | 8 + .../ComInterop/SetUserArrayDelegate.cs | 8 + .../TestCredentialProviderUserArray.cs | 51 ++ ...et.CredentialProvider.UnitTests.x64.csproj | 9 +- ...et.CredentialProvider.UnitTests.x86.csproj | 8 +- src/Lithnet.CredentialProvider.sln | 14 +- .../Lithnet.CredentialProvider.csproj | 5 +- .../Installing the sample.md | 8 +- ....CredentialProvider.Sample.Core.x64.csproj | 30 ++ .../Resources/TileIcon.png | Bin .../TestCredentialProviderCoreX64.cs} | 34 +- .../Installing the sample.md | 8 +- ....CredentialProvider.Sample.Core.x86.csproj | 30 ++ .../Resources/TileIcon.png | Bin .../TestCredentialProviderCoreX86.cs} | 8 +- .../ControlKeys.cs | 0 .../Installing the sample.md | 14 +- .../InternalLogger.cs | 0 ...ntialProvider.Sample.Framework.x64.csproj} | 3 +- .../Resources/TileIcon.png | Bin .../TestCredentialProviderFrameworkX64.cs} | 6 +- .../TestCredentialProviderTile.cs | 4 +- .../Installing the sample.md | 8 +- ...ntialProvider.Sample.Framework.x86.csproj} | 8 +- .../Resources/TileIcon.png | Bin .../TestCredentialProviderFrameworkX86.cs} | 8 +- ...redentialProvider.Sample.net6.0.x64.csproj | 32 -- ...redentialProvider.Sample.net6.0.x86.csproj | 32 -- ...hnet.CredentialProvider.TestApp.x64.csproj | 2 - ...hnet.CredentialProvider.TestApp.x86.csproj | 4 +- 52 files changed, 1116 insertions(+), 128 deletions(-) create mode 100644 src/Lithnet.CredentialProvider.UnitTests.arm64/Lithnet.CredentialProvider.UnitTests.arm64.csproj create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/AbiTestCredentialProvider.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/AbiTestCredentialTile2.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/ComInterfacePointer.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/ComMarshal.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialProviderAbi.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialProviderAbiTests.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetCredentialAtDelegate.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetCredentialCountDelegate.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetFieldDescriptorAtDelegate.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetFieldDescriptorCountDelegate.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetFieldStateDelegate.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetStringValueDelegate.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetUserCountDelegate.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetUserSidDelegate.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/ITestCredentialProviderUserArray.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/NativeCredentialProviderFieldDescriptor.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/ProcessArchitectureTests.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/SetDeselectedDelegate.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/SetSelectedDelegate.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/SetUsageScenarioDelegate.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/SetUserArrayDelegate.cs create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/TestCredentialProviderUserArray.cs rename src/samples/{Lithnet.CredentialProvider.Sample.net6.0.x64 => Lithnet.CredentialProvider.Sample.Core.x64}/Installing the sample.md (85%) create mode 100644 src/samples/Lithnet.CredentialProvider.Sample.Core.x64/Lithnet.CredentialProvider.Sample.Core.x64.csproj rename src/samples/{Lithnet.CredentialProvider.Sample.net6.0.x64 => Lithnet.CredentialProvider.Sample.Core.x64}/Resources/TileIcon.png (100%) rename src/samples/{Lithnet.CredentialProvider.Sample.net6.0.x64/TestCredentialProviderNet60x64.cs => Lithnet.CredentialProvider.Sample.Core.x64/TestCredentialProviderCoreX64.cs} (74%) rename src/samples/{Lithnet.CredentialProvider.Sample.net6.0.x86 => Lithnet.CredentialProvider.Sample.Core.x86}/Installing the sample.md (85%) create mode 100644 src/samples/Lithnet.CredentialProvider.Sample.Core.x86/Lithnet.CredentialProvider.Sample.Core.x86.csproj rename src/samples/{Lithnet.CredentialProvider.Sample.net6.0.x86 => Lithnet.CredentialProvider.Sample.Core.x86}/Resources/TileIcon.png (100%) rename src/samples/{Lithnet.CredentialProvider.Sample.net6.0.x86/TestCredentialProviderNet60x86.cs => Lithnet.CredentialProvider.Sample.Core.x86/TestCredentialProviderCoreX86.cs} (95%) rename src/samples/{Lithnet.CredentialProvider.Sample.net472.x64 => Lithnet.CredentialProvider.Sample.Framework.x64}/ControlKeys.cs (100%) rename src/samples/{Lithnet.CredentialProvider.Sample.net472.x64 => Lithnet.CredentialProvider.Sample.Framework.x64}/Installing the sample.md (84%) rename src/samples/{Lithnet.CredentialProvider.Sample.net472.x64 => Lithnet.CredentialProvider.Sample.Framework.x64}/InternalLogger.cs (100%) rename src/samples/{Lithnet.CredentialProvider.Sample.net472.x64/Lithnet.CredentialProvider.Sample.net472.x64.csproj => Lithnet.CredentialProvider.Sample.Framework.x64/Lithnet.CredentialProvider.Sample.Framework.x64.csproj} (83%) rename src/samples/{Lithnet.CredentialProvider.Sample.net472.x64 => Lithnet.CredentialProvider.Sample.Framework.x64}/Resources/TileIcon.png (100%) rename src/samples/{Lithnet.CredentialProvider.Sample.net472.x64/TestCredentialProviderNet472x64.cs => Lithnet.CredentialProvider.Sample.Framework.x64/TestCredentialProviderFrameworkX64.cs} (95%) rename src/samples/{Lithnet.CredentialProvider.Sample.net472.x64 => Lithnet.CredentialProvider.Sample.Framework.x64}/TestCredentialProviderTile.cs (99%) rename src/samples/{Lithnet.CredentialProvider.Sample.net472.x86 => Lithnet.CredentialProvider.Sample.Framework.x86}/Installing the sample.md (89%) rename src/samples/{Lithnet.CredentialProvider.Sample.net472.x86/Lithnet.CredentialProvider.Sample.net472.x86.csproj => Lithnet.CredentialProvider.Sample.Framework.x86/Lithnet.CredentialProvider.Sample.Framework.x86.csproj} (53%) rename src/samples/{Lithnet.CredentialProvider.Sample.net472.x86 => Lithnet.CredentialProvider.Sample.Framework.x86}/Resources/TileIcon.png (100%) rename src/samples/{Lithnet.CredentialProvider.Sample.net472.x86/TestCredentialProviderNet472x86.cs => Lithnet.CredentialProvider.Sample.Framework.x86/TestCredentialProviderFrameworkX86.cs} (95%) delete mode 100644 src/samples/Lithnet.CredentialProvider.Sample.net6.0.x64/Lithnet.CredentialProvider.Sample.net6.0.x64.csproj delete mode 100644 src/samples/Lithnet.CredentialProvider.Sample.net6.0.x86/Lithnet.CredentialProvider.Sample.net6.0.x86.csproj diff --git a/README.md b/README.md index 7cbfe37..d9e9a13 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A library for creating secure Windows Credential Providers in .NET, without the The Lithnet Credential Provider for Windows provides an easy way to create a credential provider, without having to implement the COM components. The COM components are still there, but abstracted away into a fully managed implementation. ## Getting started -* Create a new Class Library project. You can use .NET Framework 4.6.1 or higher, or you can use .NET 6.0 or higher) to create your provider. You must build as either an x64 or x86 binary. You cannot use AnyCPU. +* Create a new Class Library project. You can use .NET Framework 4.7.2 or later, or .NET 8.0, 9.0, or 10.0. You must build an x64 or x86 binary. You cannot use AnyCPU. * Install the package from nuget `Install-Package Lithnet.CredentialProvider` * Modify the `csproj` file and set `RegisterForComInterop` to `false` @@ -21,11 +21,11 @@ The Lithnet Credential Provider for Windows provides an easy way to create a cre ``` -* If you are using .NET 6 or higher, you must also set `EnableComHosting` to `true` +* If you are using .NET 8.0, 9.0, or 10.0, you must also set `EnableComHosting` to `true`. ```xml - net6.0-windows + net8.0-windows false x64 true diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d59539c..011a4a5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -26,9 +26,59 @@ name: $(build.version.major).$(build.version.minor).$(build.version.revision)$(b trigger: none stages: +- stage: test_provider + displayName: Test credential provider + dependsOn: [] + jobs: + - job: test_windows + displayName: Test Windows + strategy: + matrix: + x64: + testArchitecture: x64 + testImage: windows-latest + testProject: src/Lithnet.CredentialProvider.UnitTests.x64/Lithnet.CredentialProvider.UnitTests.x64.csproj + x86: + testArchitecture: x86 + testImage: windows-latest + testProject: src/Lithnet.CredentialProvider.UnitTests.x86/Lithnet.CredentialProvider.UnitTests.x86.csproj + arm64: + testArchitecture: ARM64 + testImage: windows-11-vs2026-arm + testProject: src/Lithnet.CredentialProvider.UnitTests.arm64/Lithnet.CredentialProvider.UnitTests.arm64.csproj + pool: + vmImage: $(testImage) + steps: + - task: UseDotNet@2 + displayName: Install .NET 8 SDK + inputs: + packageType: sdk + version: 8.0.x + + - task: UseDotNet@2 + displayName: Install .NET 9 SDK + inputs: + packageType: sdk + version: 9.0.x + + - task: UseDotNet@2 + displayName: Install .NET 10 SDK + inputs: + packageType: sdk + version: 10.0.x + + - task: DotNetCoreCLI@2 + displayName: Test $(testArchitecture) + inputs: + command: test + projects: $(testProject) + arguments: '--configuration $(buildConfiguration)' + publishTestResults: true + testRunTitle: Credential Provider $(testArchitecture) + - stage: build_provider displayName: Build credential provider - dependsOn: [] + dependsOn: test_provider jobs: - job: "build_provider_job" steps: diff --git a/src/Lithnet.CredentialProvider.UnitTests.arm64/Lithnet.CredentialProvider.UnitTests.arm64.csproj b/src/Lithnet.CredentialProvider.UnitTests.arm64/Lithnet.CredentialProvider.UnitTests.arm64.csproj new file mode 100644 index 0000000..a564069 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.arm64/Lithnet.CredentialProvider.UnitTests.arm64.csproj @@ -0,0 +1,22 @@ + + + + net8.0-windows;net9.0-windows;net10.0-windows;net472;net48 + false + ARM64 + $(DefineConstants);TEST_ARM64 + + + + + + + + + + + + + + + diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/AbiTestCredentialProvider.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/AbiTestCredentialProvider.cs new file mode 100644 index 0000000..3912406 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/AbiTestCredentialProvider.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [ComVisible(true)] + [ClassInterface(ClassInterfaceType.None)] + [Guid("2A83F3F8-A46C-4104-8FFB-BD9979279450")] + internal sealed class AbiTestCredentialProvider : CredentialProviderBase + { + public AbiTestCredentialProvider() + { + this.Field = new SmallLabelControl("message", "COM ABI test"); + } + + public SmallLabelControl Field { get; } + + public AbiTestCredentialTile2 Tile { get; private set; } + + public override bool IsUsageScenarioSupported(UsageScenario cpus, CredUIWinFlags dwFlags) + { + return cpus == UsageScenario.CredUI; + } + + public override IEnumerable GetControls(UsageScenario cpus) + { + return new ControlBase[] { this.Field }; + } + + public override bool ShouldIncludeUserTile(CredentialProviderUser user) + { + return false; + } + + public override bool ShouldIncludeGenericTile() + { + return true; + } + + public override CredentialTile CreateGenericTile() + { + this.Tile = new AbiTestCredentialTile2(this); + return this.Tile; + } + + public override CredentialTile2 CreateUserTile(CredentialProviderUser user) + { + return null; + } + } +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/AbiTestCredentialTile2.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/AbiTestCredentialTile2.cs new file mode 100644 index 0000000..be6b6d5 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/AbiTestCredentialTile2.cs @@ -0,0 +1,14 @@ +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + internal sealed class AbiTestCredentialTile2 : CredentialTile2 + { + public AbiTestCredentialTile2(CredentialProviderBase credentialProvider) : base(credentialProvider) + { + } + + protected override CredentialResponseBase GetCredentials() + { + return null; + } + } +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/ComInterfacePointer.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/ComInterfacePointer.cs new file mode 100644 index 0000000..3eae0d7 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/ComInterfacePointer.cs @@ -0,0 +1,79 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + internal sealed class ComInterfacePointer : IDisposable + { + private IntPtr value; + + private ComInterfacePointer(IntPtr value) + { + this.value = value; + } + + public IntPtr Value + { + get + { + if (this.value == IntPtr.Zero) + { + throw new ObjectDisposedException(nameof(ComInterfacePointer)); + } + + return this.value; + } + } + + public static ComInterfacePointer Create(object instance, Guid interfaceId) + { + IntPtr unknown = Marshal.GetIUnknownForObject(instance); + + try + { + int hresult = ComMarshal.QueryInterface(unknown, interfaceId, out IntPtr interfacePointer); + if (hresult != CredentialProviderAbi.S_OK) + { + if (interfacePointer != IntPtr.Zero) + { + Marshal.Release(interfacePointer); + } + + throw new COMException("The COM interface was not available", hresult); + } + + return new ComInterfacePointer(interfacePointer); + } + finally + { + Marshal.Release(unknown); + } + } + + public static ComInterfacePointer TakeOwnership(IntPtr value) + { + if (value == IntPtr.Zero) + { + throw new ArgumentException("The COM interface pointer cannot be zero", nameof(value)); + } + + return new ComInterfacePointer(value); + } + + public TDelegate GetMethod(int slot) where TDelegate : class + { + IntPtr vtable = Marshal.ReadIntPtr(this.Value); + IntPtr method = Marshal.ReadIntPtr(vtable, checked(slot * IntPtr.Size)); + return (TDelegate)(object)Marshal.GetDelegateForFunctionPointer(method, typeof(TDelegate)); + } + + public void Dispose() + { + if (this.value != IntPtr.Zero) + { + Marshal.Release(this.value); + this.value = IntPtr.Zero; + } + } + } +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/ComMarshal.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/ComMarshal.cs new file mode 100644 index 0000000..51ec0fe --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/ComMarshal.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + internal static class ComMarshal + { + public static int QueryInterface(IntPtr unknown, Guid interfaceId, out IntPtr interfacePointer) + { +#if NET9_0_OR_GREATER + return Marshal.QueryInterface(unknown, in interfaceId, out interfacePointer); +#else + return Marshal.QueryInterface(unknown, ref interfaceId, out interfacePointer); +#endif + } + } +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialProviderAbi.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialProviderAbi.cs new file mode 100644 index 0000000..61e3cf2 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialProviderAbi.cs @@ -0,0 +1,59 @@ +using System; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + internal static class CredentialProviderAbi + { + public static readonly Guid ICredentialProvider = new Guid("D27C3481-5A1C-45B2-8AAA-C20EBBE8229E"); + + public static readonly Guid ICredentialProviderCredential = new Guid("63913A93-40C1-481A-818D-4072FF8C70CC"); + + public static readonly Guid ICredentialProviderCredential2 = new Guid("FD672C54-40EA-4D6E-9B49-CFB1A7507BD7"); + + public static readonly Guid ICredentialProviderSetUserArray = new Guid("095C1484-1C0C-4388-9C6D-500E61BF84BD"); + + public static readonly Guid ICredentialProviderUserArray = new Guid("90C119AE-0F18-4520-A1F1-114366A40FE8"); + + public const int SetUsageScenarioSlot = 3; + + public const int GetFieldDescriptorCountSlot = 7; + + public const int GetFieldDescriptorAtSlot = 8; + + public const int GetCredentialCountSlot = 9; + + public const int GetCredentialAtSlot = 10; + + public const int SetUserArraySlot = 3; + + public const int UserArrayGetCountSlot = 5; + + public const int SetSelectedSlot = 5; + + public const int SetDeselectedSlot = 6; + + public const int GetFieldStateSlot = 7; + + public const int GetStringValueSlot = 8; + + public const int GetUserSidSlot = 20; + + public const int SmallTextFieldType = 2; + + public const int DisplayInSelectedTileFieldState = 1; + + public const int NoInteractiveFieldState = 0; + + public const uint NoDefaultCredential = 0xFFFFFFFF; + + public const int S_OK = 0; + + public const int S_FALSE = 1; + + public const int E_FAIL = unchecked((int)0x80004005); + + public const int E_INVALIDARG = unchecked((int)0x80070057); + + public const int E_NOTIMPL = unchecked((int)0x80004001); + } +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialProviderAbiTests.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialProviderAbiTests.cs new file mode 100644 index 0000000..d66bcaa --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/CredentialProviderAbiTests.cs @@ -0,0 +1,450 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using NUnit.Framework; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [TestFixture] + [NonParallelizable] + [Apartment(ApartmentState.STA)] + public class CredentialProviderAbiTests + { + [Test] + public void CredentialProviderInterfaceCanBeQueried() + { + var provider = new AbiTestCredentialProvider(); + IntPtr unknown = Marshal.GetIUnknownForObject(provider); + IntPtr providerInterface = IntPtr.Zero; + + try + { + int hresult = ComMarshal.QueryInterface(unknown, CredentialProviderAbi.ICredentialProvider, out providerInterface); + + Assert.That(hresult, Is.EqualTo(CredentialProviderAbi.S_OK)); + Assert.That(providerInterface, Is.Not.EqualTo(IntPtr.Zero)); + } + finally + { + if (providerInterface != IntPtr.Zero) + { + Marshal.Release(providerInterface); + } + + Marshal.Release(unknown); + GC.KeepAlive(provider); + } + } + + [Test] + public void SetUsageScenarioPreservesHResultAndArguments() + { + var provider = new AbiTestCredentialProvider(); + + using (ComInterfacePointer providerInterface = ComInterfacePointer.Create(provider, CredentialProviderAbi.ICredentialProvider)) + { + SetUsageScenarioDelegate setUsageScenario = providerInterface.GetMethod(CredentialProviderAbi.SetUsageScenarioSlot); + + int hresult = setUsageScenario(providerInterface.Value, (int)UsageScenario.CredUI, (uint)CredUIWinFlags.CREDUIWIN_SECURE_PROMPT); + + Assert.That(hresult, Is.EqualTo(CredentialProviderAbi.S_OK)); + Assert.That(provider.UsageScenario, Is.EqualTo(UsageScenario.CredUI)); + Assert.That(provider.CredUIFlags, Is.EqualTo(CredUIWinFlags.CREDUIWIN_SECURE_PROMPT)); + + hresult = setUsageScenario(providerInterface.Value, (int)UsageScenario.Logon, 0); + + Assert.That(hresult, Is.EqualTo(CredentialProviderAbi.E_NOTIMPL)); + Assert.That(provider.UsageScenario, Is.EqualTo(UsageScenario.Logon)); + Assert.That(provider.CredUIFlags, Is.EqualTo((CredUIWinFlags)0)); + } + + GC.KeepAlive(provider); + } + + [Test] + public void GetFieldDescriptorCountReturnsProviderControlCount() + { + var provider = new AbiTestCredentialProvider(); + + using (ComInterfacePointer providerInterface = ComInterfacePointer.Create(provider, CredentialProviderAbi.ICredentialProvider)) + { + GetFieldDescriptorCountDelegate getFieldDescriptorCount = providerInterface.GetMethod(CredentialProviderAbi.GetFieldDescriptorCountSlot); + + int hresult = getFieldDescriptorCount(providerInterface.Value, out uint count); + + Assert.That(hresult, Is.EqualTo(CredentialProviderAbi.S_OK)); + Assert.That(count, Is.EqualTo(1)); + } + + GC.KeepAlive(provider); + } + + [Test] + public void GetFieldDescriptorAtReturnsWindowsSdkLayout() + { + var provider = new AbiTestCredentialProvider(); + + using (ComInterfacePointer providerInterface = ComInterfacePointer.Create(provider, CredentialProviderAbi.ICredentialProvider)) + { + GetFieldDescriptorCountDelegate getFieldDescriptorCount = providerInterface.GetMethod(CredentialProviderAbi.GetFieldDescriptorCountSlot); + GetFieldDescriptorAtDelegate getFieldDescriptorAt = providerInterface.GetMethod(CredentialProviderAbi.GetFieldDescriptorAtSlot); + + Assert.That(getFieldDescriptorCount(providerInterface.Value, out uint count), Is.EqualTo(CredentialProviderAbi.S_OK)); + Assert.That(count, Is.EqualTo(1)); + + IntPtr descriptorPointer = IntPtr.Zero; + IntPtr labelPointer = IntPtr.Zero; + + try + { + int hresult = getFieldDescriptorAt(providerInterface.Value, 0, out descriptorPointer); + + Assert.That(hresult, Is.EqualTo(CredentialProviderAbi.S_OK)); + Assert.That(descriptorPointer, Is.Not.EqualTo(IntPtr.Zero)); + + NativeCredentialProviderFieldDescriptor descriptor = Marshal.PtrToStructure(descriptorPointer); + labelPointer = descriptor.Label; + + Assert.That(descriptor.FieldId, Is.EqualTo(provider.Field.Id)); + Assert.That(descriptor.FieldType, Is.EqualTo(CredentialProviderAbi.SmallTextFieldType)); + Assert.That(Marshal.PtrToStringUni(labelPointer), Is.EqualTo("COM ABI test")); + Assert.That(descriptor.FieldTypeGuid, Is.EqualTo(Guid.Empty)); + } + finally + { + if (labelPointer != IntPtr.Zero) + { + Marshal.FreeCoTaskMem(labelPointer); + } + + if (descriptorPointer != IntPtr.Zero) + { + Marshal.FreeCoTaskMem(descriptorPointer); + } + } + } + + GC.KeepAlive(provider); + } + + [Test] + public void GetFieldDescriptorAtRejectsInvalidIndex() + { + var provider = new AbiTestCredentialProvider(); + + using (ComInterfacePointer providerInterface = ComInterfacePointer.Create(provider, CredentialProviderAbi.ICredentialProvider)) + { + GetFieldDescriptorCountDelegate getFieldDescriptorCount = providerInterface.GetMethod(CredentialProviderAbi.GetFieldDescriptorCountSlot); + GetFieldDescriptorAtDelegate getFieldDescriptorAt = providerInterface.GetMethod(CredentialProviderAbi.GetFieldDescriptorAtSlot); + + Assert.That(getFieldDescriptorCount(providerInterface.Value, out uint count), Is.EqualTo(CredentialProviderAbi.S_OK)); + Assert.That(count, Is.EqualTo(1)); + + int hresult = getFieldDescriptorAt(providerInterface.Value, count, out IntPtr descriptorPointer); + + Assert.That(hresult, Is.EqualTo(CredentialProviderAbi.E_INVALIDARG)); + Assert.That(descriptorPointer, Is.EqualTo(IntPtr.Zero)); + } + + GC.KeepAlive(provider); + } + + [Test] + public void CredentialProviderSetUserArrayInterfaceCanBeQueried() + { + var provider = new AbiTestCredentialProvider(); + IntPtr unknown = Marshal.GetIUnknownForObject(provider); + IntPtr setUserArrayInterface = IntPtr.Zero; + + try + { + int hresult = ComMarshal.QueryInterface(unknown, CredentialProviderAbi.ICredentialProviderSetUserArray, out setUserArrayInterface); + + Assert.That(hresult, Is.EqualTo(CredentialProviderAbi.S_OK)); + Assert.That(setUserArrayInterface, Is.Not.EqualTo(IntPtr.Zero)); + } + finally + { + if (setUserArrayInterface != IntPtr.Zero) + { + Marshal.Release(setUserArrayInterface); + } + + Marshal.Release(unknown); + GC.KeepAlive(provider); + } + } + + [Test] + public void TestUserArrayUsesWindowsSdkGetCountSlot() + { + var users = new TestCredentialProviderUserArray(); + + using (ComInterfacePointer userArrayInterface = ComInterfacePointer.Create(users, CredentialProviderAbi.ICredentialProviderUserArray)) + { + GetUserCountDelegate getCount = userArrayInterface.GetMethod(CredentialProviderAbi.UserArrayGetCountSlot); + + int hresult = getCount(userArrayInterface.Value, out uint count); + + Assert.That(hresult, Is.EqualTo(CredentialProviderAbi.S_OK)); + Assert.That(count, Is.EqualTo(0)); + Assert.That(users.GetCountCallCount, Is.EqualTo(1)); + } + + GC.KeepAlive(users); + } + + [Test] + public void SetUserArrayInvokesWindowsSdkUserArrayGetCount() + { + var provider = new AbiTestCredentialProvider(); + var users = new TestCredentialProviderUserArray(); + + SetEmptyUserArray(provider, users); + + Assert.That(users.GetCountCallCount, Is.EqualTo(1)); + + GC.KeepAlive(users); + GC.KeepAlive(provider); + } + + [Test] + public void GetCredentialCountReturnsGenericTileWithoutDefault() + { + var provider = new AbiTestCredentialProvider(); + var users = new TestCredentialProviderUserArray(); + + SetEmptyUserArray(provider, users); + + using (ComInterfacePointer providerInterface = ComInterfacePointer.Create(provider, CredentialProviderAbi.ICredentialProvider)) + { + GetCredentialCountDelegate getCredentialCount = providerInterface.GetMethod(CredentialProviderAbi.GetCredentialCountSlot); + + int hresult = getCredentialCount(providerInterface.Value, out uint count, out uint defaultCredential, out int autoLogonWithDefault); + + Assert.That(hresult, Is.EqualTo(CredentialProviderAbi.S_OK)); + Assert.That(count, Is.EqualTo(1)); + Assert.That(defaultCredential, Is.EqualTo(CredentialProviderAbi.NoDefaultCredential)); + Assert.That(autoLogonWithDefault, Is.EqualTo(0)); + Assert.That(users.GetCountCallCount, Is.EqualTo(2)); + Assert.That(provider.Tile, Is.Not.Null); + } + + GC.KeepAlive(users); + GC.KeepAlive(provider); + } + + [Test] + public void GetCredentialAtRejectsInvalidIndex() + { + var provider = new AbiTestCredentialProvider(); + var users = new TestCredentialProviderUserArray(); + + SetEmptyUserArray(provider, users); + + using (ComInterfacePointer providerInterface = ComInterfacePointer.Create(provider, CredentialProviderAbi.ICredentialProvider)) + { + GetCredentialCountDelegate getCredentialCount = providerInterface.GetMethod(CredentialProviderAbi.GetCredentialCountSlot); + GetCredentialAtDelegate getCredentialAt = providerInterface.GetMethod(CredentialProviderAbi.GetCredentialAtSlot); + + Assert.That(getCredentialCount(providerInterface.Value, out uint count, out uint defaultCredential, out int autoLogonWithDefault), Is.EqualTo(CredentialProviderAbi.S_OK)); + + int hresult = getCredentialAt(providerInterface.Value, count, out IntPtr credential); + + Assert.That(hresult, Is.EqualTo(CredentialProviderAbi.E_FAIL)); + Assert.That(credential, Is.EqualTo(IntPtr.Zero)); + } + + GC.KeepAlive(users); + GC.KeepAlive(provider); + } + + [Test] + public void CredentialV1SelectionMethodsPreserveStateAndHResults() + { + var provider = new AbiTestCredentialProvider(); + var users = new TestCredentialProviderUserArray(); + + using (ComInterfacePointer credential = CreateCredentialInterface(provider, users)) + { + SetSelectedDelegate setSelected = credential.GetMethod(CredentialProviderAbi.SetSelectedSlot); + SetDeselectedDelegate setDeselected = credential.GetMethod(CredentialProviderAbi.SetDeselectedSlot); + + int hresult = setSelected(credential.Value, out int autoLogon); + + Assert.That(hresult, Is.EqualTo(CredentialProviderAbi.S_OK)); + Assert.That(autoLogon, Is.EqualTo(0)); + Assert.That(provider.Tile.IsSelected, Is.True); + + hresult = setDeselected(credential.Value); + + Assert.That(hresult, Is.EqualTo(CredentialProviderAbi.S_OK)); + Assert.That(provider.Tile.IsSelected, Is.False); + } + + GC.KeepAlive(users); + GC.KeepAlive(provider); + } + + [Test] + public void CredentialV1GetFieldStateUsesWindowsSdkEnumValues() + { + var provider = new AbiTestCredentialProvider(); + var users = new TestCredentialProviderUserArray(); + + using (ComInterfacePointer credential = CreateCredentialInterface(provider, users)) + { + GetFieldStateDelegate getFieldState = credential.GetMethod(CredentialProviderAbi.GetFieldStateSlot); + + int hresult = getFieldState(credential.Value, provider.Field.Id, out int fieldState, out int interactiveState); + + Assert.That(hresult, Is.EqualTo(CredentialProviderAbi.S_OK)); + Assert.That(fieldState, Is.EqualTo(CredentialProviderAbi.DisplayInSelectedTileFieldState)); + Assert.That(interactiveState, Is.EqualTo(CredentialProviderAbi.NoInteractiveFieldState)); + } + + GC.KeepAlive(users); + GC.KeepAlive(provider); + } + + [Test] + public void CredentialV1GetStringValueReturnsComTaskMemory() + { + var provider = new AbiTestCredentialProvider(); + var users = new TestCredentialProviderUserArray(); + + using (ComInterfacePointer credential = CreateCredentialInterface(provider, users)) + { + GetStringValueDelegate getStringValue = credential.GetMethod(CredentialProviderAbi.GetStringValueSlot); + IntPtr value = IntPtr.Zero; + + try + { + int hresult = getStringValue(credential.Value, provider.Field.Id, out value); + + Assert.That(hresult, Is.EqualTo(CredentialProviderAbi.S_OK)); + Assert.That(value, Is.Not.EqualTo(IntPtr.Zero)); + Assert.That(Marshal.PtrToStringUni(value), Is.EqualTo("COM ABI test")); + } + finally + { + if (value != IntPtr.Zero) + { + Marshal.FreeCoTaskMem(value); + } + } + } + + GC.KeepAlive(users); + GC.KeepAlive(provider); + } + + [Test] + public void CredentialV2GetUserSidUsesInheritedVtableOrder() + { + var provider = new AbiTestCredentialProvider(); + var users = new TestCredentialProviderUserArray(); + + using (ComInterfacePointer credential = CreateCredentialInterface(provider, users)) + { + IntPtr credential2Pointer = IntPtr.Zero; + + try + { + int hresult = ComMarshal.QueryInterface(credential.Value, CredentialProviderAbi.ICredentialProviderCredential2, out credential2Pointer); + + Assert.That(hresult, Is.EqualTo(CredentialProviderAbi.S_OK)); + Assert.That(credential2Pointer, Is.Not.EqualTo(IntPtr.Zero)); + + using (ComInterfacePointer credential2 = ComInterfacePointer.TakeOwnership(credential2Pointer)) + { + credential2Pointer = IntPtr.Zero; + GetUserSidDelegate getUserSid = credential2.GetMethod(CredentialProviderAbi.GetUserSidSlot); + IntPtr sid = IntPtr.Zero; + + try + { + hresult = getUserSid(credential2.Value, out sid); + + Assert.That(hresult, Is.EqualTo(CredentialProviderAbi.S_FALSE)); + Assert.That(sid, Is.EqualTo(IntPtr.Zero)); + } + finally + { + if (sid != IntPtr.Zero) + { + Marshal.FreeCoTaskMem(sid); + } + } + } + } + finally + { + if (credential2Pointer != IntPtr.Zero) + { + Marshal.Release(credential2Pointer); + } + } + } + + GC.KeepAlive(users); + GC.KeepAlive(provider); + } + + [Test] + public void FieldDescriptorDeclarationMatchesWindowsSdkSize() + { + int expectedSize = IntPtr.Size == 4 ? 28 : 32; + + Assert.That(Marshal.SizeOf(), Is.EqualTo(expectedSize)); + } + + private static void SetEmptyUserArray(AbiTestCredentialProvider provider, TestCredentialProviderUserArray users) + { + using (ComInterfacePointer setUserArrayInterface = ComInterfacePointer.Create(provider, CredentialProviderAbi.ICredentialProviderSetUserArray)) + using (ComInterfacePointer userArrayInterface = ComInterfacePointer.Create(users, CredentialProviderAbi.ICredentialProviderUserArray)) + { + SetUserArrayDelegate setUserArray = setUserArrayInterface.GetMethod(CredentialProviderAbi.SetUserArraySlot); + + int hresult = setUserArray(setUserArrayInterface.Value, userArrayInterface.Value); + + Assert.That(hresult, Is.EqualTo(CredentialProviderAbi.S_OK)); + } + } + + private static ComInterfacePointer CreateCredentialInterface(AbiTestCredentialProvider provider, TestCredentialProviderUserArray users) + { + SetEmptyUserArray(provider, users); + + using (ComInterfacePointer providerInterface = ComInterfacePointer.Create(provider, CredentialProviderAbi.ICredentialProvider)) + { + GetCredentialCountDelegate getCredentialCount = providerInterface.GetMethod(CredentialProviderAbi.GetCredentialCountSlot); + GetCredentialAtDelegate getCredentialAt = providerInterface.GetMethod(CredentialProviderAbi.GetCredentialAtSlot); + + Assert.That(getCredentialCount(providerInterface.Value, out uint count, out uint defaultCredential, out int autoLogonWithDefault), Is.EqualTo(CredentialProviderAbi.S_OK)); + Assert.That(count, Is.EqualTo(1)); + + IntPtr credential = IntPtr.Zero; + + try + { + int hresult = getCredentialAt(providerInterface.Value, 0, out credential); + + Assert.That(hresult, Is.EqualTo(CredentialProviderAbi.S_OK)); + Assert.That(credential, Is.Not.EqualTo(IntPtr.Zero)); + + ComInterfacePointer result = ComInterfacePointer.TakeOwnership(credential); + credential = IntPtr.Zero; + return result; + } + finally + { + if (credential != IntPtr.Zero) + { + Marshal.Release(credential); + } + } + } + } + } +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetCredentialAtDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetCredentialAtDelegate.cs new file mode 100644 index 0000000..62c0555 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetCredentialAtDelegate.cs @@ -0,0 +1,8 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate int GetCredentialAtDelegate(IntPtr instance, uint index, out IntPtr credential); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetCredentialCountDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetCredentialCountDelegate.cs new file mode 100644 index 0000000..e4a8601 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetCredentialCountDelegate.cs @@ -0,0 +1,8 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate int GetCredentialCountDelegate(IntPtr instance, out uint count, out uint defaultCredential, out int autoLogonWithDefault); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetFieldDescriptorAtDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetFieldDescriptorAtDelegate.cs new file mode 100644 index 0000000..fd9098b --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetFieldDescriptorAtDelegate.cs @@ -0,0 +1,8 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate int GetFieldDescriptorAtDelegate(IntPtr instance, uint index, out IntPtr descriptor); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetFieldDescriptorCountDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetFieldDescriptorCountDelegate.cs new file mode 100644 index 0000000..1432e9b --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetFieldDescriptorCountDelegate.cs @@ -0,0 +1,8 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate int GetFieldDescriptorCountDelegate(IntPtr instance, out uint count); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetFieldStateDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetFieldStateDelegate.cs new file mode 100644 index 0000000..a677e77 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetFieldStateDelegate.cs @@ -0,0 +1,8 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate int GetFieldStateDelegate(IntPtr instance, uint fieldId, out int fieldState, out int interactiveState); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetStringValueDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetStringValueDelegate.cs new file mode 100644 index 0000000..e4239e3 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetStringValueDelegate.cs @@ -0,0 +1,8 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate int GetStringValueDelegate(IntPtr instance, uint fieldId, out IntPtr value); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetUserCountDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetUserCountDelegate.cs new file mode 100644 index 0000000..d9480df --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetUserCountDelegate.cs @@ -0,0 +1,8 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate int GetUserCountDelegate(IntPtr instance, out uint count); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetUserSidDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetUserSidDelegate.cs new file mode 100644 index 0000000..bf67eda --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/GetUserSidDelegate.cs @@ -0,0 +1,8 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate int GetUserSidDelegate(IntPtr instance, out IntPtr sid); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/ITestCredentialProviderUserArray.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/ITestCredentialProviderUserArray.cs new file mode 100644 index 0000000..3c8c053 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/ITestCredentialProviderUserArray.cs @@ -0,0 +1,27 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [ComVisible(true)] + [Guid("90C119AE-0F18-4520-A1F1-114366A40FE8")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ITestCredentialProviderUserArray + { + [PreserveSig] + [return: MarshalAs(UnmanagedType.Error)] + int SetProviderFilter(ref Guid providerToFilterTo); + + [PreserveSig] + [return: MarshalAs(UnmanagedType.Error)] + int GetAccountOptions(out int accountOptions); + + [PreserveSig] + [return: MarshalAs(UnmanagedType.Error)] + int GetCount(out uint userCount); + + [PreserveSig] + [return: MarshalAs(UnmanagedType.Error)] + int GetAt(uint userIndex, out IntPtr user); + } +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/NativeCredentialProviderFieldDescriptor.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/NativeCredentialProviderFieldDescriptor.cs new file mode 100644 index 0000000..6909fef --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/NativeCredentialProviderFieldDescriptor.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [StructLayout(LayoutKind.Sequential)] + internal struct NativeCredentialProviderFieldDescriptor + { + public uint FieldId; + + public int FieldType; + + public IntPtr Label; + + public Guid FieldTypeGuid; + } +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/ProcessArchitectureTests.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/ProcessArchitectureTests.cs new file mode 100644 index 0000000..71a1e7f --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/ProcessArchitectureTests.cs @@ -0,0 +1,27 @@ +using System; +using NUnit.Framework; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [TestFixture] + public class ProcessArchitectureTests + { + [Test] + public void TestHostUsesRequestedArchitecture() + { +#if TEST_X86 + const string expectedArchitecture = "x86"; +#elif TEST_X64 + const string expectedArchitecture = "AMD64"; +#elif TEST_ARM64 + const string expectedArchitecture = "ARM64"; +#else +#error A test process architecture must be defined by the project. +#endif + + string actualArchitecture = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE"); + + Assert.That(actualArchitecture, Is.EqualTo(expectedArchitecture).IgnoreCase); + } + } +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/SetDeselectedDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/SetDeselectedDelegate.cs new file mode 100644 index 0000000..485f87a --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/SetDeselectedDelegate.cs @@ -0,0 +1,8 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate int SetDeselectedDelegate(IntPtr instance); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/SetSelectedDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/SetSelectedDelegate.cs new file mode 100644 index 0000000..81f9d26 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/SetSelectedDelegate.cs @@ -0,0 +1,8 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate int SetSelectedDelegate(IntPtr instance, out int autoLogon); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/SetUsageScenarioDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/SetUsageScenarioDelegate.cs new file mode 100644 index 0000000..64b1332 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/SetUsageScenarioDelegate.cs @@ -0,0 +1,8 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate int SetUsageScenarioDelegate(IntPtr instance, int usageScenario, uint flags); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/SetUserArrayDelegate.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/SetUserArrayDelegate.cs new file mode 100644 index 0000000..13e3bde --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/SetUserArrayDelegate.cs @@ -0,0 +1,8 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate int SetUserArrayDelegate(IntPtr instance, IntPtr users); +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/TestCredentialProviderUserArray.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/TestCredentialProviderUserArray.cs new file mode 100644 index 0000000..579d95a --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/ComInterop/TestCredentialProviderUserArray.cs @@ -0,0 +1,51 @@ +using System; +using System.Runtime.InteropServices; +using Lithnet.CredentialProvider.Interop; + +namespace Lithnet.CredentialProvider.UnitTests.ComInterop +{ + [ComVisible(true)] + [ClassInterface(ClassInterfaceType.None)] + // The in-process call also requires the callback to implement the parameter's managed interface type. + // ITestCredentialProviderUserArray keeps the COM contract under test independent of that adapter. + internal sealed class TestCredentialProviderUserArray : ITestCredentialProviderUserArray, ICredentialProviderUserArray + { + public int GetCountCallCount { get; private set; } + + public int SetProviderFilter(ref Guid providerToFilterTo) + { + return CredentialProviderAbi.E_NOTIMPL; + } + + public int GetAccountOptions(out int accountOptions) + { + accountOptions = 0; + return CredentialProviderAbi.S_OK; + } + + public int GetCount(out uint userCount) + { + this.GetCountCallCount++; + userCount = 0; + return CredentialProviderAbi.S_OK; + } + + public int GetAt(uint userIndex, out IntPtr user) + { + user = IntPtr.Zero; + return CredentialProviderAbi.E_INVALIDARG; + } + + int ICredentialProviderUserArray.GetAccountOptions(out AccountOptions accountOptions) + { + accountOptions = AccountOptions.None; + return CredentialProviderAbi.S_OK; + } + + int ICredentialProviderUserArray.GetAt(uint userIndex, out ICredentialProviderUser user) + { + user = null; + return CredentialProviderAbi.E_INVALIDARG; + } + } +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/Lithnet.CredentialProvider.UnitTests.x64.csproj b/src/Lithnet.CredentialProvider.UnitTests.x64/Lithnet.CredentialProvider.UnitTests.x64.csproj index 86fc565..aac65f3 100644 --- a/src/Lithnet.CredentialProvider.UnitTests.x64/Lithnet.CredentialProvider.UnitTests.x64.csproj +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/Lithnet.CredentialProvider.UnitTests.x64.csproj @@ -1,15 +1,16 @@  - net6.0-windows;net7.0-windows;net8.0-windows;net461;net48 + net8.0-windows;net9.0-windows;net10.0-windows;net472;net48 false - x64 + x64 + $(DefineConstants);TEST_X64 - - + + diff --git a/src/Lithnet.CredentialProvider.UnitTests.x86/Lithnet.CredentialProvider.UnitTests.x86.csproj b/src/Lithnet.CredentialProvider.UnitTests.x86/Lithnet.CredentialProvider.UnitTests.x86.csproj index 70b55c6..9ad8c73 100644 --- a/src/Lithnet.CredentialProvider.UnitTests.x86/Lithnet.CredentialProvider.UnitTests.x86.csproj +++ b/src/Lithnet.CredentialProvider.UnitTests.x86/Lithnet.CredentialProvider.UnitTests.x86.csproj @@ -1,20 +1,22 @@ - net6.0-windows;net7.0-windows;net8.0-windows;net461;net48 + net8.0-windows;net9.0-windows;net10.0-windows;net472;net48 false x86 + $(DefineConstants);TEST_X86 - - + + + diff --git a/src/Lithnet.CredentialProvider.sln b/src/Lithnet.CredentialProvider.sln index 4460844..0005fb0 100644 --- a/src/Lithnet.CredentialProvider.sln +++ b/src/Lithnet.CredentialProvider.sln @@ -11,13 +11,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ..\azure-pipelines.yml = ..\azure-pipelines.yml EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lithnet.CredentialProvider.Sample.net6.0.x64", "samples\Lithnet.CredentialProvider.Sample.net6.0.x64\Lithnet.CredentialProvider.Sample.net6.0.x64.csproj", "{163E16D0-9FF3-40D3-AE96-3F221C922AA3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lithnet.CredentialProvider.Sample.Core.x64", "samples\Lithnet.CredentialProvider.Sample.Core.x64\Lithnet.CredentialProvider.Sample.Core.x64.csproj", "{163E16D0-9FF3-40D3-AE96-3F221C922AA3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lithnet.CredentialProvider.Sample.net6.0.x86", "samples\Lithnet.CredentialProvider.Sample.net6.0.x86\Lithnet.CredentialProvider.Sample.net6.0.x86.csproj", "{7A8A10F9-7D79-4755-BBDD-77C80FDC58ED}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lithnet.CredentialProvider.Sample.Core.x86", "samples\Lithnet.CredentialProvider.Sample.Core.x86\Lithnet.CredentialProvider.Sample.Core.x86.csproj", "{7A8A10F9-7D79-4755-BBDD-77C80FDC58ED}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lithnet.CredentialProvider.Sample.net472.x64", "samples\Lithnet.CredentialProvider.Sample.net472.x64\Lithnet.CredentialProvider.Sample.net472.x64.csproj", "{F60AC10A-337C-46D9-A2DE-1ED48B4AB301}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lithnet.CredentialProvider.Sample.Framework.x64", "samples\Lithnet.CredentialProvider.Sample.Framework.x64\Lithnet.CredentialProvider.Sample.Framework.x64.csproj", "{F60AC10A-337C-46D9-A2DE-1ED48B4AB301}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lithnet.CredentialProvider.Sample.net472.x86", "samples\Lithnet.CredentialProvider.Sample.net472.x86\Lithnet.CredentialProvider.Sample.net472.x86.csproj", "{57F2780E-58F1-4A7B-BCB4-A218733273EA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lithnet.CredentialProvider.Sample.Framework.x86", "samples\Lithnet.CredentialProvider.Sample.Framework.x86\Lithnet.CredentialProvider.Sample.Framework.x86.csproj", "{57F2780E-58F1-4A7B-BCB4-A218733273EA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lithnet.CredentialProvider.TestApp.x64", "samples\Lithnet.CredentialProvider.TestApp.x64\Lithnet.CredentialProvider.TestApp.x64.csproj", "{5019CCEB-AD78-4688-8C11-89A85C4289CD}" EndProject @@ -27,6 +27,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lithnet.CredentialProvider. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lithnet.CredentialProvider.UnitTests.x86", "Lithnet.CredentialProvider.UnitTests.x86\Lithnet.CredentialProvider.UnitTests.x86.csproj", "{DA23151B-B7B6-4942-B7A4-48E1918EF145}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lithnet.CredentialProvider.UnitTests.arm64", "Lithnet.CredentialProvider.UnitTests.arm64\Lithnet.CredentialProvider.UnitTests.arm64.csproj", "{07D19CE5-8419-4E06-B39D-CC90EAA3FF9A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,6 +71,10 @@ Global {DA23151B-B7B6-4942-B7A4-48E1918EF145}.Debug|Any CPU.Build.0 = Debug|Any CPU {DA23151B-B7B6-4942-B7A4-48E1918EF145}.Release|Any CPU.ActiveCfg = Release|Any CPU {DA23151B-B7B6-4942-B7A4-48E1918EF145}.Release|Any CPU.Build.0 = Release|Any CPU + {07D19CE5-8419-4E06-B39D-CC90EAA3FF9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07D19CE5-8419-4E06-B39D-CC90EAA3FF9A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07D19CE5-8419-4E06-B39D-CC90EAA3FF9A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07D19CE5-8419-4E06-B39D-CC90EAA3FF9A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Lithnet.CredentialProvider/Lithnet.CredentialProvider.csproj b/src/Lithnet.CredentialProvider/Lithnet.CredentialProvider.csproj index 7dfebbb..a3a8312 100644 --- a/src/Lithnet.CredentialProvider/Lithnet.CredentialProvider.csproj +++ b/src/Lithnet.CredentialProvider/Lithnet.CredentialProvider.csproj @@ -1,6 +1,6 @@  - net6.0-windows;net7.0-windows;net8.0-windows;net461 + net8.0-windows;net9.0-windows;net10.0-windows;net472;net48 false Library true @@ -45,6 +45,9 @@ <_Parameter1>Lithnet.CredentialProvider.UnitTests.x86 + + <_Parameter1>Lithnet.CredentialProvider.UnitTests.arm64 + diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x64/Installing the sample.md b/src/samples/Lithnet.CredentialProvider.Sample.Core.x64/Installing the sample.md similarity index 85% rename from src/samples/Lithnet.CredentialProvider.Sample.net6.0.x64/Installing the sample.md rename to src/samples/Lithnet.CredentialProvider.Sample.Core.x64/Installing the sample.md index 0930d98..d686da8 100644 --- a/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x64/Installing the sample.md +++ b/src/samples/Lithnet.CredentialProvider.Sample.Core.x64/Installing the sample.md @@ -7,10 +7,10 @@ In order to install and run the sample app, you have to register the COM compone Build the EXE, and from an elevated command prompt, change to the bin folder, and run the following commands ``` -regsvr32 "Lithnet.CredentialProvider.Sample.net6.0.x64.comhost.dll" +regsvr32 "Lithnet.CredentialProvider.Sample.Core.x64.comhost.dll" REG ADD "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{4cd12d80-9259-4f38-94dc-1828080ad9ff}" /f -REG ADD "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{4cd12d80-9259-4f38-94dc-1828080ad9ff}" /ve /t REG_SZ /f /d "Lithnet.CredentialProvider.Sample.net6.0.x64" +REG ADD "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{4cd12d80-9259-4f38-94dc-1828080ad9ff}" /ve /t REG_SZ /f /d "Lithnet.CredentialProvider.Sample.Core.x64" ``` ## Disable the sample @@ -31,6 +31,6 @@ REG ADD "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credentia To remove the credential provider, run the following command. ``` -regsvr32 /u "Lithnet.CredentialProvider.Sample.net6.0.x64.comhost.dll" +regsvr32 /u "Lithnet.CredentialProvider.Sample.Core.x64.comhost.dll" REG DELETE "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{4cd12d80-9259-4f38-94dc-1828080ad9ff}" /f -``` \ No newline at end of file +``` diff --git a/src/samples/Lithnet.CredentialProvider.Sample.Core.x64/Lithnet.CredentialProvider.Sample.Core.x64.csproj b/src/samples/Lithnet.CredentialProvider.Sample.Core.x64/Lithnet.CredentialProvider.Sample.Core.x64.csproj new file mode 100644 index 0000000..0dad286 --- /dev/null +++ b/src/samples/Lithnet.CredentialProvider.Sample.Core.x64/Lithnet.CredentialProvider.Sample.Core.x64.csproj @@ -0,0 +1,30 @@ + + + + net8.0-windows + false + x64 + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x64/Resources/TileIcon.png b/src/samples/Lithnet.CredentialProvider.Sample.Core.x64/Resources/TileIcon.png similarity index 100% rename from src/samples/Lithnet.CredentialProvider.Sample.net6.0.x64/Resources/TileIcon.png rename to src/samples/Lithnet.CredentialProvider.Sample.Core.x64/Resources/TileIcon.png diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x64/TestCredentialProviderNet60x64.cs b/src/samples/Lithnet.CredentialProvider.Sample.Core.x64/TestCredentialProviderCoreX64.cs similarity index 74% rename from src/samples/Lithnet.CredentialProvider.Sample.net6.0.x64/TestCredentialProviderNet60x64.cs rename to src/samples/Lithnet.CredentialProvider.Sample.Core.x64/TestCredentialProviderCoreX64.cs index 1ef2d92..cd155ec 100644 --- a/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x64/TestCredentialProviderNet60x64.cs +++ b/src/samples/Lithnet.CredentialProvider.Sample.Core.x64/TestCredentialProviderCoreX64.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; using System.Reflection; using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; @@ -9,11 +11,11 @@ namespace Lithnet.CredentialProvider.Samples { [ComVisible(true)] [ClassInterface(ClassInterfaceType.None)] - [ProgId("Lithnet.CredentialProvider.Sample.net6.0.x64")] + [ProgId("Lithnet.CredentialProvider.Sample.Core.x64")] [Guid("4cd12d80-9259-4f38-94dc-1828080ad9ff")] - public class TestCredentialProviderNet60x64 : CredentialProviderBase + public class TestCredentialProviderCoreX64 : CredentialProviderBase { - private static readonly ICredentialProviderLogger logger = InternalLoggerFactory.Instance.CreateLogger(); + private static readonly ICredentialProviderLogger logger = InternalLoggerFactory.Instance.CreateLogger(); protected override ICredentialProviderLoggerFactory GetLoggerFactory() { @@ -36,10 +38,11 @@ namespace Lithnet.CredentialProvider.Samples { yield return new CredentialProviderLabelControl(ControlKeys.LabelCredentialProvider, "Login with showcase credential provider"); - var image = new Bitmap(Assembly.GetExecutingAssembly().GetManifestResourceStream("Lithnet.CredentialProvider.Sample.net6.0.x64.Resources.TileIcon.png")); + var providerLogo = new Bitmap(Assembly.GetExecutingAssembly().GetManifestResourceStream("Lithnet.CredentialProvider.Sample.Core.x64.Resources.TileIcon.png")); + var transparentUserTile = CreateTransparentUserTile(); - yield return new CredentialProviderLogoControl(ControlKeys.ImageCredentialProvider, "Credential provider logo", image); - yield return new CredentialProviderLogoControl(ControlKeys.ImageUserTile, "User tile image", image); + yield return new CredentialProviderLogoControl(ControlKeys.ImageCredentialProvider, "Credential provider logo", providerLogo); + yield return new UserTileControl(ControlKeys.ImageUserTile, "Transparent user tile image", transparentUserTile); yield return new LargeLabelControl(ControlKeys.LabelLargeHeading, "The is our showcase credential provider"); yield return new SmallLabelControl(ControlKeys.LabelSmallHeading, "Let's see what we can do"); @@ -95,5 +98,24 @@ namespace Lithnet.CredentialProvider.Samples { return new TestCredentialProviderTile(this, user); } + + private static Bitmap CreateTransparentUserTile() + { + Bitmap image = new Bitmap(128, 128, PixelFormat.Format32bppArgb); + + using (Graphics graphics = Graphics.FromImage(image)) + using (SolidBrush shadow = new SolidBrush(Color.FromArgb(96, 0, 0, 0))) + using (SolidBrush foreground = new SolidBrush(Color.FromArgb(255, 38, 132, 255))) + using (Pen outline = new Pen(Color.White, 5)) + { + graphics.SmoothingMode = SmoothingMode.AntiAlias; + graphics.Clear(Color.Transparent); + graphics.FillEllipse(shadow, 28, 30, 88, 88); + graphics.FillEllipse(foreground, 12, 12, 88, 88); + graphics.DrawEllipse(outline, 12, 12, 88, 88); + } + + return image; + } } } diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x86/Installing the sample.md b/src/samples/Lithnet.CredentialProvider.Sample.Core.x86/Installing the sample.md similarity index 85% rename from src/samples/Lithnet.CredentialProvider.Sample.net6.0.x86/Installing the sample.md rename to src/samples/Lithnet.CredentialProvider.Sample.Core.x86/Installing the sample.md index 5e2ab19..d2192d2 100644 --- a/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x86/Installing the sample.md +++ b/src/samples/Lithnet.CredentialProvider.Sample.Core.x86/Installing the sample.md @@ -7,10 +7,10 @@ In order to install and run the sample app, you have to register the COM compone Build the EXE, and from an elevated command prompt, change to the bin folder, and run the following commands ``` -regsvr32 "Lithnet.CredentialProvider.Sample.net6.0.x86.comhost.dll" +regsvr32 "Lithnet.CredentialProvider.Sample.Core.x86.comhost.dll" REG ADD "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{90592593-f4d3-4f62-aa83-9cf1f7b590e0}" /f -REG ADD "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{90592593-f4d3-4f62-aa83-9cf1f7b590e0}" /ve /t REG_SZ /f /d "Lithnet.CredentialProvider.Sample.net6.0.x86" +REG ADD "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{90592593-f4d3-4f62-aa83-9cf1f7b590e0}" /ve /t REG_SZ /f /d "Lithnet.CredentialProvider.Sample.Core.x86" ``` ## Disable the sample @@ -31,6 +31,6 @@ REG ADD "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credentia To remove the credential provider, run the following command. ``` -regsvr32 /u "Lithnet.CredentialProvider.Sample.net6.0.x86.comhost.dll" +regsvr32 /u "Lithnet.CredentialProvider.Sample.Core.x86.comhost.dll" REG DELETE "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{90592593-f4d3-4f62-aa83-9cf1f7b590e0}" /f -``` \ No newline at end of file +``` diff --git a/src/samples/Lithnet.CredentialProvider.Sample.Core.x86/Lithnet.CredentialProvider.Sample.Core.x86.csproj b/src/samples/Lithnet.CredentialProvider.Sample.Core.x86/Lithnet.CredentialProvider.Sample.Core.x86.csproj new file mode 100644 index 0000000..f94dc39 --- /dev/null +++ b/src/samples/Lithnet.CredentialProvider.Sample.Core.x86/Lithnet.CredentialProvider.Sample.Core.x86.csproj @@ -0,0 +1,30 @@ + + + + net8.0-windows + false + x86 + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x86/Resources/TileIcon.png b/src/samples/Lithnet.CredentialProvider.Sample.Core.x86/Resources/TileIcon.png similarity index 100% rename from src/samples/Lithnet.CredentialProvider.Sample.net6.0.x86/Resources/TileIcon.png rename to src/samples/Lithnet.CredentialProvider.Sample.Core.x86/Resources/TileIcon.png diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x86/TestCredentialProviderNet60x86.cs b/src/samples/Lithnet.CredentialProvider.Sample.Core.x86/TestCredentialProviderCoreX86.cs similarity index 95% rename from src/samples/Lithnet.CredentialProvider.Sample.net6.0.x86/TestCredentialProviderNet60x86.cs rename to src/samples/Lithnet.CredentialProvider.Sample.Core.x86/TestCredentialProviderCoreX86.cs index 8b4d847..de40289 100644 --- a/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x86/TestCredentialProviderNet60x86.cs +++ b/src/samples/Lithnet.CredentialProvider.Sample.Core.x86/TestCredentialProviderCoreX86.cs @@ -9,11 +9,11 @@ namespace Lithnet.CredentialProvider.Samples { [ComVisible(true)] [ClassInterface(ClassInterfaceType.None)] - [ProgId("Lithnet.CredentialProvider.Sample.net6.0.x86")] + [ProgId("Lithnet.CredentialProvider.Sample.Core.x86")] [Guid("90592593-f4d3-4f62-aa83-9cf1f7b590e0")] - public class TestCredentialProviderNet60x86 : CredentialProviderBase + public class TestCredentialProviderCoreX86 : CredentialProviderBase { - private static readonly ICredentialProviderLogger logger = InternalLoggerFactory.Instance.CreateLogger(); + private static readonly ICredentialProviderLogger logger = InternalLoggerFactory.Instance.CreateLogger(); protected override ICredentialProviderLoggerFactory GetLoggerFactory() { @@ -34,7 +34,7 @@ namespace Lithnet.CredentialProvider.Samples } else { - var image = new Bitmap(Assembly.GetExecutingAssembly().GetManifestResourceStream("Lithnet.CredentialProvider.Sample.net6.0.x86.Resources.TileIcon.png")); + var image = new Bitmap(Assembly.GetExecutingAssembly().GetManifestResourceStream("Lithnet.CredentialProvider.Sample.Core.x86.Resources.TileIcon.png")); yield return new CredentialProviderLabelControl(ControlKeys.LabelCredentialProvider, "Login with showcase credential provider"); yield return new CredentialProviderLogoControl(ControlKeys.ImageCredentialProvider, "Credential provider logo", image); diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/ControlKeys.cs b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/ControlKeys.cs similarity index 100% rename from src/samples/Lithnet.CredentialProvider.Sample.net472.x64/ControlKeys.cs rename to src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/ControlKeys.cs diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/Installing the sample.md b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/Installing the sample.md similarity index 84% rename from src/samples/Lithnet.CredentialProvider.Sample.net472.x64/Installing the sample.md rename to src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/Installing the sample.md index dd4a3e1..3f2162c 100644 --- a/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/Installing the sample.md +++ b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/Installing the sample.md @@ -9,8 +9,8 @@ Build the EXE, and from an elevated command prompt, change to the bin folder, an ``` SETLOCAL SET CLSID={4EB911FA-CA18-40EA-86DF-19AFF5D1DA58} -SET BinaryPath=D:\dev\git\lithnet\windows-credential-provider\src\samples\Lithnet.CredentialProvider.Sample.net472.x64\bin\Debug\net472\Lithnet.CredentialProvider.Sample.net472.x64.dll -REM %windir%\Microsoft.NET\Framework64\v4.0.30319\regasm /codebase "Lithnet.CredentialProvider.Sample.net472.x64.dll" +SET BinaryPath=D:\dev\git\lithnet\windows-credential-provider\src\samples\Lithnet.CredentialProvider.Sample.Framework.x64\bin\Debug\net472\Lithnet.CredentialProvider.Sample.Framework.x64.dll +REM %windir%\Microsoft.NET\Framework64\v4.0.30319\regasm /codebase "Lithnet.CredentialProvider.Sample.Framework.x64.dll" REG ADD "HKLM\SOFTWARE\Classes\CLSID\%CLSID%" /ve /t REG_SZ /f /d "Lithnet.CredentialProvider.Samples.TestCredentialProvider" REG ADD "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\Implemented Categories\{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}" @@ -20,11 +20,11 @@ REM REG ADD "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\InprocServer32" /v "Class" /t R REG ADD "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\InprocServer32" /v "RuntimeVersion" /t REG_SZ /f /d "v4.0.30319" REG ADD "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\InprocServer32" /v "CodeBase" /t REG_SZ /f /d "%BinaryPath%" -REG ADD "HKLM\SOFTWARE\Classes\Lithnet.CredentialProvider.Sample.net472.x64" /ve /t REG_SZ /f /d "Lithnet.CredentialProvider.Samples.TestCredentialProvider" -REG ADD "HKLM\SOFTWARE\Classes\Lithnet.CredentialProvider.Sample.net472.x64\CLSID" /ve /t REG_SZ /f /d "%CLSID%" +REG ADD "HKLM\SOFTWARE\Classes\Lithnet.CredentialProvider.Sample.Framework.x64" /ve /t REG_SZ /f /d "Lithnet.CredentialProvider.Samples.TestCredentialProvider" +REG ADD "HKLM\SOFTWARE\Classes\Lithnet.CredentialProvider.Sample.Framework.x64\CLSID" /ve /t REG_SZ /f /d "%CLSID%" -REG ADD "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\%CLSID%" /ve /t REG_SZ /f /d "Lithnet.CredentialProvider.Sample.net472.x64" +REG ADD "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\%CLSID%" /ve /t REG_SZ /f /d "Lithnet.CredentialProvider.Sample.Framework.x64" ``` ## Disable the sample @@ -45,6 +45,6 @@ REG ADD "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credentia To remove the credential provider, run the following command. ``` -%windir%\Microsoft.NET\Framework64\v4.0.30319\regasm /u "Lithnet.CredentialProvider.Sample.net472.x64.dll" +%windir%\Microsoft.NET\Framework64\v4.0.30319\regasm /u "Lithnet.CredentialProvider.Sample.Framework.x64.dll" REG DELETE "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{4eb911fa-ca18-40ea-86df-19aff5d1da58"}" /f -``` \ No newline at end of file +``` diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/InternalLogger.cs b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/InternalLogger.cs similarity index 100% rename from src/samples/Lithnet.CredentialProvider.Sample.net472.x64/InternalLogger.cs rename to src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/InternalLogger.cs diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/Lithnet.CredentialProvider.Sample.net472.x64.csproj b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/Lithnet.CredentialProvider.Sample.Framework.x64.csproj similarity index 83% rename from src/samples/Lithnet.CredentialProvider.Sample.net472.x64/Lithnet.CredentialProvider.Sample.net472.x64.csproj rename to src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/Lithnet.CredentialProvider.Sample.Framework.x64.csproj index 479b909..fb5d04b 100644 --- a/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/Lithnet.CredentialProvider.Sample.net472.x64.csproj +++ b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/Lithnet.CredentialProvider.Sample.Framework.x64.csproj @@ -15,7 +15,8 @@ - + + diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/Resources/TileIcon.png b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/Resources/TileIcon.png similarity index 100% rename from src/samples/Lithnet.CredentialProvider.Sample.net472.x64/Resources/TileIcon.png rename to src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/Resources/TileIcon.png diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/TestCredentialProviderNet472x64.cs b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/TestCredentialProviderFrameworkX64.cs similarity index 95% rename from src/samples/Lithnet.CredentialProvider.Sample.net472.x64/TestCredentialProviderNet472x64.cs rename to src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/TestCredentialProviderFrameworkX64.cs index 8346cfb..e3a4b7f 100644 --- a/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/TestCredentialProviderNet472x64.cs +++ b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/TestCredentialProviderFrameworkX64.cs @@ -10,9 +10,9 @@ namespace Lithnet.CredentialProvider.Samples { [ComVisible(true)] [ClassInterface(ClassInterfaceType.None)] - [ProgId("Lithnet.CredentialProvider.Sample.net472.x64")] + [ProgId("Lithnet.CredentialProvider.Sample.Framework.x64")] [Guid("4eb911fa-ca18-40ea-86df-19aff5d1da58")] - public class TestCredentialProviderNet472x64 : CredentialProviderBase + public class TestCredentialProviderFrameworkX64 : CredentialProviderBase { protected override ICredentialProviderLoggerFactory GetLoggerFactory() { @@ -33,7 +33,7 @@ namespace Lithnet.CredentialProvider.Samples } else { - var image = new Bitmap(Assembly.GetExecutingAssembly().GetManifestResourceStream("Lithnet.CredentialProvider.Sample.net472.x64.Resources.TileIcon.png")); + var image = new Bitmap(Assembly.GetExecutingAssembly().GetManifestResourceStream("Lithnet.CredentialProvider.Sample.Framework.x64.Resources.TileIcon.png")); yield return new CredentialProviderLabelControl(ControlKeys.LabelCredentialProvider, "Login with showcase credential provider"); yield return new CredentialProviderLogoControl(ControlKeys.ImageCredentialProvider, "Credential provider logo", image); diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/TestCredentialProviderTile.cs b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/TestCredentialProviderTile.cs similarity index 99% rename from src/samples/Lithnet.CredentialProvider.Sample.net472.x64/TestCredentialProviderTile.cs rename to src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/TestCredentialProviderTile.cs index adb26e8..204d0c1 100644 --- a/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/TestCredentialProviderTile.cs +++ b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/TestCredentialProviderTile.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging; namespace Lithnet.CredentialProvider.Samples { - public class TestCredentialProviderTile : CredentialTile2 + public class TestCredentialProviderTile : CredentialTile3 { private TextboxControl UsernameControl; private SecurePasswordTextboxControl PasswordControl; @@ -189,4 +189,4 @@ namespace Lithnet.CredentialProvider.Samples return plainTextPassword; } } -} \ No newline at end of file +} diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net472.x86/Installing the sample.md b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x86/Installing the sample.md similarity index 89% rename from src/samples/Lithnet.CredentialProvider.Sample.net472.x86/Installing the sample.md rename to src/samples/Lithnet.CredentialProvider.Sample.Framework.x86/Installing the sample.md index ae0b944..c9ea520 100644 --- a/src/samples/Lithnet.CredentialProvider.Sample.net472.x86/Installing the sample.md +++ b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x86/Installing the sample.md @@ -7,10 +7,10 @@ In order to install and run the sample app, you have to register the COM compone Build the EXE, and from an elevated command prompt, change to the bin folder, and run the following commands ``` -%windir%\Microsoft.NET\Framework\v4.0.30319\regasm /codebase "Lithnet.CredentialProvider.Sample.net472.x86.dll" +%windir%\Microsoft.NET\Framework\v4.0.30319\regasm /codebase "Lithnet.CredentialProvider.Sample.Framework.x86.dll" REG ADD "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{c9055c88-03f9-4a12-8e33-1ee75826a4a6}" /f -REG ADD "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{c9055c88-03f9-4a12-8e33-1ee75826a4a6}" /ve /t REG_SZ /f /d "Lithnet.CredentialProvider.Sample.net472.x86" +REG ADD "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{c9055c88-03f9-4a12-8e33-1ee75826a4a6}" /ve /t REG_SZ /f /d "Lithnet.CredentialProvider.Sample.Framework.x86" ``` ## Disable the sample @@ -31,6 +31,6 @@ REG ADD "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credentia To remove the credential provider, run the following command. ``` -%windir%\Microsoft.NET\Framework\v4.0.30319\regasm /u "Lithnet.CredentialProvider.Sample.net472.x86.dll" +%windir%\Microsoft.NET\Framework\v4.0.30319\regasm /u "Lithnet.CredentialProvider.Sample.Framework.x86.dll" REG DELETE "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{c9055c88-03f9-4a12-8e33-1ee75826a4a6}" /f -``` \ No newline at end of file +``` diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net472.x86/Lithnet.CredentialProvider.Sample.net472.x86.csproj b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x86/Lithnet.CredentialProvider.Sample.Framework.x86.csproj similarity index 53% rename from src/samples/Lithnet.CredentialProvider.Sample.net472.x86/Lithnet.CredentialProvider.Sample.net472.x86.csproj rename to src/samples/Lithnet.CredentialProvider.Sample.Framework.x86/Lithnet.CredentialProvider.Sample.Framework.x86.csproj index b455ab7..8c529c9 100644 --- a/src/samples/Lithnet.CredentialProvider.Sample.net472.x86/Lithnet.CredentialProvider.Sample.net472.x86.csproj +++ b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x86/Lithnet.CredentialProvider.Sample.Framework.x86.csproj @@ -11,9 +11,9 @@ - - - + + + @@ -21,7 +21,7 @@ - + diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net472.x86/Resources/TileIcon.png b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x86/Resources/TileIcon.png similarity index 100% rename from src/samples/Lithnet.CredentialProvider.Sample.net472.x86/Resources/TileIcon.png rename to src/samples/Lithnet.CredentialProvider.Sample.Framework.x86/Resources/TileIcon.png diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net472.x86/TestCredentialProviderNet472x86.cs b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x86/TestCredentialProviderFrameworkX86.cs similarity index 95% rename from src/samples/Lithnet.CredentialProvider.Sample.net472.x86/TestCredentialProviderNet472x86.cs rename to src/samples/Lithnet.CredentialProvider.Sample.Framework.x86/TestCredentialProviderFrameworkX86.cs index 9bf6ad8..0d2d088 100644 --- a/src/samples/Lithnet.CredentialProvider.Sample.net472.x86/TestCredentialProviderNet472x86.cs +++ b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x86/TestCredentialProviderFrameworkX86.cs @@ -9,11 +9,11 @@ namespace Lithnet.CredentialProvider.Samples { [ComVisible(true)] [ClassInterface(ClassInterfaceType.None)] - [ProgId("Lithnet.CredentialProvider.Sample.net472.x86")] + [ProgId("Lithnet.CredentialProvider.Sample.Framework.x86")] [Guid("c9055c88-03f9-4a12-8e33-1ee75826a4a6")] - public class TestCredentialProviderNet472x86 : CredentialProviderBase + public class TestCredentialProviderFrameworkX86 : CredentialProviderBase { - private static readonly ICredentialProviderLogger logger = InternalLoggerFactory.Instance.CreateLogger(); + private static readonly ICredentialProviderLogger logger = InternalLoggerFactory.Instance.CreateLogger(); protected override ICredentialProviderLoggerFactory GetLoggerFactory() { @@ -34,7 +34,7 @@ namespace Lithnet.CredentialProvider.Samples } else { - var image = new Bitmap(Assembly.GetExecutingAssembly().GetManifestResourceStream("Lithnet.CredentialProvider.Sample.net472.x86.Resources.TileIcon.png")); + var image = new Bitmap(Assembly.GetExecutingAssembly().GetManifestResourceStream("Lithnet.CredentialProvider.Sample.Framework.x86.Resources.TileIcon.png")); yield return new CredentialProviderLabelControl(ControlKeys.LabelCredentialProvider, "Login with showcase credential provider"); yield return new CredentialProviderLogoControl(ControlKeys.ImageCredentialProvider, "Credential provider logo", image); diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x64/Lithnet.CredentialProvider.Sample.net6.0.x64.csproj b/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x64/Lithnet.CredentialProvider.Sample.net6.0.x64.csproj deleted file mode 100644 index 707cdbd..0000000 --- a/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x64/Lithnet.CredentialProvider.Sample.net6.0.x64.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - - net6.0-windows - false - x64 - true - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x86/Lithnet.CredentialProvider.Sample.net6.0.x86.csproj b/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x86/Lithnet.CredentialProvider.Sample.net6.0.x86.csproj deleted file mode 100644 index 2b4cf5a..0000000 --- a/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x86/Lithnet.CredentialProvider.Sample.net6.0.x86.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - - net6.0-windows - false - x86 - true - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/samples/Lithnet.CredentialProvider.TestApp.x64/Lithnet.CredentialProvider.TestApp.x64.csproj b/src/samples/Lithnet.CredentialProvider.TestApp.x64/Lithnet.CredentialProvider.TestApp.x64.csproj index 6628cf4..2ce78dc 100644 --- a/src/samples/Lithnet.CredentialProvider.TestApp.x64/Lithnet.CredentialProvider.TestApp.x64.csproj +++ b/src/samples/Lithnet.CredentialProvider.TestApp.x64/Lithnet.CredentialProvider.TestApp.x64.csproj @@ -8,8 +8,6 @@ - - diff --git a/src/samples/Lithnet.CredentialProvider.TestApp.x86/Lithnet.CredentialProvider.TestApp.x86.csproj b/src/samples/Lithnet.CredentialProvider.TestApp.x86/Lithnet.CredentialProvider.TestApp.x86.csproj index 84ddc34..34ee0a4 100644 --- a/src/samples/Lithnet.CredentialProvider.TestApp.x86/Lithnet.CredentialProvider.TestApp.x86.csproj +++ b/src/samples/Lithnet.CredentialProvider.TestApp.x86/Lithnet.CredentialProvider.TestApp.x86.csproj @@ -2,14 +2,12 @@ Exe - net6.0-windows + net8.0-windows false x86 - - From ff088cee23620c4ffd3d0c587f065ac0b6b89b67 Mon Sep 17 00:00:00 2001 From: Ryan Newington Date: Fri, 28 Aug 2026 20:38:24 +1000 Subject: [PATCH 3/9] Add native ARM64 pipeline coverage --- azure-pipelines.yml | 70 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 011a4a5..e1ed8cc 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,3 +1,10 @@ +resources: + repositories: + - repository: release_tools + type: git + name: release-tools/release-tools + ref: refs/heads/master + pool: vmImage: 'windows-latest' @@ -42,10 +49,6 @@ stages: testArchitecture: x86 testImage: windows-latest testProject: src/Lithnet.CredentialProvider.UnitTests.x86/Lithnet.CredentialProvider.UnitTests.x86.csproj - arm64: - testArchitecture: ARM64 - testImage: windows-11-vs2026-arm - testProject: src/Lithnet.CredentialProvider.UnitTests.arm64/Lithnet.CredentialProvider.UnitTests.arm64.csproj pool: vmImage: $(testImage) steps: @@ -76,6 +79,65 @@ stages: publishTestResults: true testRunTitle: Credential Provider $(testArchitecture) + - template: templates/azure-vm-start.yaml@release_tools + parameters: + jobName: start_arm64_test_vm + vmNames: cpw1125a64-ci + deployEnvironment: CredentialProvider + distroType: win + resourceGroup: rg-ams-testhosts + azureSubscription: CredentialProviderAzureTestLab + agentMode: pool + targetPoolName: CredentialProvider + + - job: test_windows_arm64 + displayName: Test Windows ARM64 + dependsOn: start_arm64_test_vm + pool: + name: CredentialProvider + demands: + - arch -equals arm64 + - role -equals unittest + - product -equals credential-provider + steps: + - task: UseDotNet@2 + displayName: Install .NET 8 SDK + inputs: + packageType: sdk + version: 8.0.x + + - task: UseDotNet@2 + displayName: Install .NET 9 SDK + inputs: + packageType: sdk + version: 9.0.x + + - task: UseDotNet@2 + displayName: Install .NET 10 SDK + inputs: + packageType: sdk + version: 10.0.x + + - task: DotNetCoreCLI@2 + displayName: Test ARM64 + inputs: + command: test + projects: src/Lithnet.CredentialProvider.UnitTests.arm64/Lithnet.CredentialProvider.UnitTests.arm64.csproj + arguments: '--configuration $(buildConfiguration) --arch arm64' + publishTestResults: true + testRunTitle: Credential Provider ARM64 + + - template: templates/azure-vm-stop.yaml@release_tools + parameters: + jobName: stop_arm64_test_vm + vmNames: cpw1125a64-ci + deployEnvironment: CredentialProvider + distroType: win + resourceGroup: rg-ams-testhosts + azureSubscription: CredentialProviderAzureTestLab + dependsOn: test_windows_arm64 + condition: always() + - stage: build_provider displayName: Build credential provider dependsOn: test_provider From c5065308dbc49c0af34519533efb18150530d492 Mon Sep 17 00:00:00 2001 From: Ryan Newington Date: Fri, 28 Aug 2026 20:56:41 +1000 Subject: [PATCH 4/9] Fix hosted test architecture selection --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e1ed8cc..9920bb6 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -75,7 +75,7 @@ stages: inputs: command: test projects: $(testProject) - arguments: '--configuration $(buildConfiguration)' + arguments: '--configuration $(buildConfiguration) --arch $(testArchitecture)' publishTestResults: true testRunTitle: Credential Provider $(testArchitecture) From 8b12b4d078171ec9c67fcd12f67fd92a50803e88 Mon Sep 17 00:00:00 2001 From: Ryan Newington Date: Fri, 28 Aug 2026 21:07:35 +1000 Subject: [PATCH 5/9] Install x86 runtimes for hosted tests --- azure-pipelines.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9920bb6..7a6b4d2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -70,8 +70,31 @@ stages: packageType: sdk version: 10.0.x + - task: PowerShell@2 + displayName: Install x86 .NET runtimes + condition: eq(variables['testArchitecture'], 'x86') + inputs: + targetType: inline + script: | + $ErrorActionPreference = 'Stop' + $installScriptPath = "$(Agent.TempDirectory)\dotnet-install.ps1" + $runtimePath = "$(Agent.TempDirectory)\dotnet-x86" + + Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' -OutFile $installScriptPath + + foreach ($channel in @('8.0', '9.0', '10.0')) + { + & $installScriptPath -Runtime dotnet -Channel $channel -Architecture x86 -InstallDir $runtimePath -NoPath + if (-not $?) + { + throw "Failed to install the .NET $channel x86 runtime" + } + } + - task: DotNetCoreCLI@2 displayName: Test $(testArchitecture) + env: + DOTNET_ROOT_X86: $(Agent.TempDirectory)\dotnet-x86 inputs: command: test projects: $(testProject) From b0d6ccc702f7f12d9875da7b6c91ed88b1775318 Mon Sep 17 00:00:00 2001 From: Ryan Newington Date: Sun, 30 Aug 2026 08:10:32 +1000 Subject: [PATCH 6/9] Document bitmap transparency and public API Explain image behavior for each credential tile version, package the README and XML documentation, and complete the public API comments. --- README.md | 36 ++++++++++++++++++- .../ChangePasswordResponse.cs | 2 +- .../ConsentUI/ConsentUIData.cs | 6 ++-- .../Controls/BitmapControl.cs | 19 +++++++--- .../Controls/CheckboxControl.cs | 6 ++-- .../Controls/ComboboxControl.cs | 6 ++-- .../Controls/CommandLinkControl.cs | 6 ++-- .../Controls/ControlBase.cs | 6 +++- .../CredentialProviderLabelControl.cs | 15 ++++---- .../Controls/CredentialProviderLogoControl.cs | 23 ++++++------ .../InsecurePasswordTextboxControl.cs | 6 ++-- .../Controls/LargeLabelControl.cs | 6 ++-- .../Controls/SecurePasswordTextboxControl.cs | 6 ++-- .../Controls/SimpleList.cs | 9 ++++- .../Controls/SmallLabelControl.cs | 6 ++-- .../Controls/SubmitButtonControl.cs | 6 ++-- .../Controls/TextboxControl.cs | 6 ++-- .../Controls/UserTileControl.cs | 23 ++++++------ .../CredentialProviderBase.cs | 26 +++++++++++--- .../CredentialTile.cs | 10 ++++-- .../CredentialTile2.cs | 9 +++++ .../CredentialTile3.cs | 13 +++++-- .../Enums/ConsentUIElevationReason.cs | 5 ++- .../Enums/ConsentUIFlags.cs | 20 +++++++++++ .../Enums/ConsentUIMsiAction.cs | 14 ++++++++ .../Enums/ConsentUIPromptType.cs | 18 ++++++++++ .../Enums/ConsentUIType.cs | 26 ++++++++++++++ .../Enums/CredUIWinFlags.cs | 3 ++ .../Enums/SerializationResponse.cs | 3 ++ .../Enums/UsageScenario.cs | 4 +-- .../Lithnet.CredentialProvider.csproj | 6 ++++ .../Logging/ICredentialProviderLogger.cs | 24 +++++++++++++ .../ICredentialProviderLoggerFactory.cs | 13 +++++++ .../Logging/TraceLogger.cs | 24 +++++++++++++ .../Logging/TraceLoggerFactory.cs | 16 +++++++++ .../TestCredentialProviderCoreX64.cs | 1 + .../TestCredentialProviderTile.cs | 3 ++ 37 files changed, 350 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index d9e9a13..683a176 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ public override CredentialTile2 CreateUserTile(CredentialProviderUser user) } ``` -* Create your tile class. Inherit from `CredentialTile2` if you want to create personalized tiles supported by Windows 8 and later, or `CredentialTile1` if you only want to implement a generic tile. Grab the instances of your controls in the `Initialize` method, so you can attach to their properties to read and respond to value changes. Finally, override the `GetCredentials` method, which is called when the user clicks the submit button. +* Create your tile class. Inherit from `CredentialTile2` if you want to create personalized tiles supported by Windows 8 and later, or `CredentialTile` if you only want to implement a generic tile. Use `CredentialTile3` when an image must preserve transparency. Grab the instances of your controls in the `Initialize` method, so you can attach to their properties to read and respond to value changes. Finally, override the `GetCredentials` method, which is called when the user clicks the submit button. ```cs public class MyTile : CredentialTile2 @@ -188,10 +188,44 @@ public class MyTile : CredentialTile2 }; } } +``` * Build your project and you have a functional credential provider! +## Bitmap transparency + +`CredentialProviderLogoControl` and `UserTileControl` both accept a `Bitmap`. The tile base class controls how Windows receives images that contain transparent or partially transparent pixels. + +| Tile base class | Image behaviour | +|-----------------|-----------------| +| `CredentialTile` | Renders transparency against the control's `BackgroundColor`. | +| `CredentialTile2` | Renders transparency against the control's `BackgroundColor`. | +| `CredentialTile3` | Preserves the image's alpha channel and ignores `BackgroundColor`. | + +Existing providers that inherit from `CredentialTile` or `CredentialTile2` keep their current behaviour. The default `BackgroundColor` is `#464646`. + +To preserve transparency, inherit your tile class from `CredentialTile3` and provide a `Bitmap` with an alpha channel. The library selects the image representation required by Windows, so your provider does not need to handle that conversion. + +```cs +public class MyTile : CredentialTile3 +{ + public MyTile(CredentialProviderBase credentialProvider) : base(credentialProvider) + { + } + + public MyTile(CredentialProviderBase credentialProvider, CredentialProviderUser user) : base(credentialProvider, user) + { + } +} + +public override IEnumerable GetControls(UsageScenario cpus) +{ + Bitmap image = LoadTransparentBitmap(); + yield return new UserTileControl("UserTile", "User tile image", image); + yield return new CredentialProviderLogoControl("ProviderLogo", "Credential provider logo", image); +} ``` + ## Installing the credential provider You can use the traditional methods of registering a credential provider (regasm, regsvr32, create registry keys etc), but we've provided a PowerShell module to automatically register your credential provider with a single command. diff --git a/src/Lithnet.CredentialProvider/ChangePasswordResponse.cs b/src/Lithnet.CredentialProvider/ChangePasswordResponse.cs index 13a9d70..a928226 100644 --- a/src/Lithnet.CredentialProvider/ChangePasswordResponse.cs +++ b/src/Lithnet.CredentialProvider/ChangePasswordResponse.cs @@ -1,7 +1,7 @@ namespace Lithnet.CredentialProvider { /// - /// The object is used to communicate the results of a password change operation to LogonUI + /// The object communicates the result of a password change operation to LogonUI. /// public class ChangePasswordResponse { diff --git a/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIData.cs b/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIData.cs index 34fe588..bb8d978 100644 --- a/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIData.cs +++ b/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIData.cs @@ -17,7 +17,7 @@ namespace Lithnet.CredentialProvider /// ConsentUIData is an abstract base class that represents all the different types of data structures that can be passed to the ConsentUI process for a UAC elevation prompt. /// The static members of the class can be used to retrieve the data structure passed to the ConsentUI process, or to determine if the current process is the ConsentUI process. /// The caller will be provided with one of the concrete implementations of this class, depending on the type of data structure that was passed to the ConsentUI process. - /// Use the property to determine the type of data structure and cast it to one of the concrete implementations. + /// Use the property to determine the type of data structure and cast it to one of the concrete implementations. /// public abstract class ConsentUIData { @@ -55,7 +55,7 @@ namespace Lithnet.CredentialProvider /// /// A series of flags that AppInfo passes to ConsentUI to signify actions that need to /// take place on the UI side. - /// This includes specifics around the UI that should be presented & signature verification settings. + /// This includes details about the UI that should be presented and the signature verification settings. /// public ConsentUIFlags Flags => this.header.Flags; @@ -356,4 +356,4 @@ namespace Lithnet.CredentialProvider return duplicatedToken; } } -} \ No newline at end of file +} diff --git a/src/Lithnet.CredentialProvider/Controls/BitmapControl.cs b/src/Lithnet.CredentialProvider/Controls/BitmapControl.cs index 35c71b5..b74a97a 100644 --- a/src/Lithnet.CredentialProvider/Controls/BitmapControl.cs +++ b/src/Lithnet.CredentialProvider/Controls/BitmapControl.cs @@ -8,13 +8,20 @@ using Lithnet.CredentialProvider.Interop; namespace Lithnet.CredentialProvider { /// - /// The base class of image-based controls + /// Provides common image behaviour for credential provider logo and user tile controls. /// public abstract class BitmapControl : ControlBase { private Bitmap bitmap; private Color backgroundColor; + /// + /// Initializes an image control. + /// + /// The unique key for the control. + /// The label associated with the control. + /// to identify the image as the credential provider logo; otherwise, . + /// The initial image displayed by the control. protected BitmapControl(string key, string label, bool isProviderLogo, Bitmap bitmap) : base(key, label, FieldType.TileImage, isProviderLogo ? Guid.Parse(CredProviderConstants.CPFG_CREDENTIAL_PROVIDER_LOGO) : Guid.Empty) { @@ -22,6 +29,10 @@ namespace Lithnet.CredentialProvider this.backgroundColor = Color.FromArgb(70, 70, 70); } + /// + /// Initializes an image control by copying an existing image control. + /// + /// The image control to copy. protected BitmapControl(BitmapControl source) : base(source) { this.bitmap = source.bitmap; @@ -29,9 +40,9 @@ namespace Lithnet.CredentialProvider } /// - /// Specifies the background color used to replace transparent pixels for and . This defaults to #707070. + /// Gets or sets the background color used when an image that contains transparency is displayed by a or . /// - /// This property does not apply to . + /// The default color is #464646. A preserves the image's alpha channel and does not use this property. public Color BackgroundColor { get { return this.backgroundColor; } @@ -46,7 +57,7 @@ namespace Lithnet.CredentialProvider } /// - /// The image to be displayed + /// Gets or sets the image displayed by the control. /// public Bitmap Bitmap { diff --git a/src/Lithnet.CredentialProvider/Controls/CheckboxControl.cs b/src/Lithnet.CredentialProvider/Controls/CheckboxControl.cs index 6128dfa..33b80ef 100644 --- a/src/Lithnet.CredentialProvider/Controls/CheckboxControl.cs +++ b/src/Lithnet.CredentialProvider/Controls/CheckboxControl.cs @@ -10,13 +10,13 @@ namespace Lithnet.CredentialProvider private bool isChecked; /// - /// Creates a new control + /// Creates a new control. /// /// The unique key for this control public CheckboxControl(string key) : this(key, null) { } /// - /// Creates a new control + /// Creates a new control. /// /// The unique key for this control /// The label associated with the control @@ -54,4 +54,4 @@ namespace Lithnet.CredentialProvider return clone; } } -} \ No newline at end of file +} diff --git a/src/Lithnet.CredentialProvider/Controls/ComboboxControl.cs b/src/Lithnet.CredentialProvider/Controls/ComboboxControl.cs index a8c8ef4..0598d2a 100644 --- a/src/Lithnet.CredentialProvider/Controls/ComboboxControl.cs +++ b/src/Lithnet.CredentialProvider/Controls/ComboboxControl.cs @@ -10,13 +10,13 @@ namespace Lithnet.CredentialProvider private int selectedItemIndex; /// - /// Creates a new control + /// Creates a new control. /// /// The unique key for this control public ComboboxControl(string key) : this(key, null) { } /// - /// Creates a new control + /// Creates a new control. /// /// The unique key for this control /// The label associated with the control @@ -107,4 +107,4 @@ namespace Lithnet.CredentialProvider return clone; } } -} \ No newline at end of file +} diff --git a/src/Lithnet.CredentialProvider/Controls/CommandLinkControl.cs b/src/Lithnet.CredentialProvider/Controls/CommandLinkControl.cs index a077f28..1e90090 100644 --- a/src/Lithnet.CredentialProvider/Controls/CommandLinkControl.cs +++ b/src/Lithnet.CredentialProvider/Controls/CommandLinkControl.cs @@ -9,13 +9,13 @@ namespace Lithnet.CredentialProvider public class CommandLinkControl : ControlBase { /// - /// Creates a new control + /// Creates a new control. /// /// The unique key for this control public CommandLinkControl(string key) : this(key, null) { } /// - /// Creates a new control + /// Creates a new control. /// /// The unique key for this control /// The label associated with the control @@ -35,4 +35,4 @@ namespace Lithnet.CredentialProvider return clone; } } -} \ No newline at end of file +} diff --git a/src/Lithnet.CredentialProvider/Controls/ControlBase.cs b/src/Lithnet.CredentialProvider/Controls/ControlBase.cs index d53d417..833e6f5 100644 --- a/src/Lithnet.CredentialProvider/Controls/ControlBase.cs +++ b/src/Lithnet.CredentialProvider/Controls/ControlBase.cs @@ -18,6 +18,10 @@ namespace Lithnet.CredentialProvider private string label; private FieldOptions options; private protected ICredentialProviderLogger logger; + + /// + /// Occurs when the value of a public property changes. + /// public event PropertyChangedEventHandler PropertyChanged; private protected ControlBase(ControlBase source) @@ -200,4 +204,4 @@ namespace Lithnet.CredentialProvider internal abstract ControlBase Clone(); } -} \ No newline at end of file +} diff --git a/src/Lithnet.CredentialProvider/Controls/CredentialProviderLabelControl.cs b/src/Lithnet.CredentialProvider/Controls/CredentialProviderLabelControl.cs index 5d1805e..cb8c36b 100644 --- a/src/Lithnet.CredentialProvider/Controls/CredentialProviderLabelControl.cs +++ b/src/Lithnet.CredentialProvider/Controls/CredentialProviderLabelControl.cs @@ -1,22 +1,21 @@ namespace Lithnet.CredentialProvider { /// - /// Represents a control that provides the credential UI with the name of this credential provider + /// Represents a control that provides the credential UI with the name of this credential provider. /// public class CredentialProviderLabelControl : SmallLabelControl { /// - /// Creates a new control + /// Creates a new control. /// - /// The unique key for this control - /// The label associated with the control + /// The unique key for this control. public CredentialProviderLabelControl(string key) : this(key, null) { } /// - /// Creates a new control + /// Creates a new control. /// - /// The unique key for this control - /// The label associated with the control + /// The unique key for this control. + /// The label associated with the control. public CredentialProviderLabelControl(string key, string label) : base(key, label, true) { this.State = FieldState.DisplayInDeselectedTile; @@ -29,4 +28,4 @@ return new CredentialProviderLabelControl(this); } } -} \ No newline at end of file +} diff --git a/src/Lithnet.CredentialProvider/Controls/CredentialProviderLogoControl.cs b/src/Lithnet.CredentialProvider/Controls/CredentialProviderLogoControl.cs index f8b11b2..cf22376 100644 --- a/src/Lithnet.CredentialProvider/Controls/CredentialProviderLogoControl.cs +++ b/src/Lithnet.CredentialProvider/Controls/CredentialProviderLogoControl.cs @@ -3,29 +3,30 @@ namespace Lithnet.CredentialProvider { /// - /// Represents a control that provides the credential UI with the logo of this credential provider + /// Represents a control that provides the credential UI with the logo of this credential provider. /// + /// See for the image transparency behaviour of each credential tile version. public class CredentialProviderLogoControl : BitmapControl { /// - /// Creates a new control + /// Creates a new control. /// - /// The unique key for this control + /// The unique key for this control. public CredentialProviderLogoControl(string key) : this(key, null, null) { } /// - /// Creates a new control + /// Creates a new control. /// - /// The unique key for this control - /// The label associated with the control. This value is not displayed to the user + /// The unique key for this control. + /// The label associated with the control. This value is not displayed to the user. public CredentialProviderLogoControl(string key, string label) : this(key, label, null) { } /// - /// Creates a new control + /// Creates a new control. /// - /// The unique key for this control - /// The label associated with the control - /// The bitmap to use as the logo + /// The unique key for this control. + /// The label associated with the control. + /// The bitmap to use as the logo. public CredentialProviderLogoControl(string key, string label, Bitmap bitmap) : base(key, label, true, bitmap) { this.State = FieldState.DisplayInDeselectedTile; @@ -38,4 +39,4 @@ namespace Lithnet.CredentialProvider return new CredentialProviderLogoControl(this); } } -} \ No newline at end of file +} diff --git a/src/Lithnet.CredentialProvider/Controls/InsecurePasswordTextboxControl.cs b/src/Lithnet.CredentialProvider/Controls/InsecurePasswordTextboxControl.cs index 0d59df5..6d7a906 100644 --- a/src/Lithnet.CredentialProvider/Controls/InsecurePasswordTextboxControl.cs +++ b/src/Lithnet.CredentialProvider/Controls/InsecurePasswordTextboxControl.cs @@ -12,13 +12,13 @@ namespace Lithnet.CredentialProvider private string password; /// - /// Creates a new control + /// Creates a new control. /// /// The unique key for this control public InsecurePasswordTextboxControl(string key) : this(key, null) { } /// - /// Creates a new control + /// Creates a new control. /// /// The unique key for this control /// The label associated with the control @@ -66,4 +66,4 @@ namespace Lithnet.CredentialProvider return clone; } } -} \ No newline at end of file +} diff --git a/src/Lithnet.CredentialProvider/Controls/LargeLabelControl.cs b/src/Lithnet.CredentialProvider/Controls/LargeLabelControl.cs index 5a9c294..d43fc24 100644 --- a/src/Lithnet.CredentialProvider/Controls/LargeLabelControl.cs +++ b/src/Lithnet.CredentialProvider/Controls/LargeLabelControl.cs @@ -8,13 +8,13 @@ namespace Lithnet.CredentialProvider public class LargeLabelControl : ControlBase { /// - /// Creates a new control + /// Creates a new control. /// /// The unique key for this control public LargeLabelControl(string key) : this(key, null) { } /// - /// Creates a new control + /// Creates a new control. /// /// The unique key for this control /// The label associated with the control @@ -27,4 +27,4 @@ namespace Lithnet.CredentialProvider return new LargeLabelControl(this); } } -} \ No newline at end of file +} diff --git a/src/Lithnet.CredentialProvider/Controls/SecurePasswordTextboxControl.cs b/src/Lithnet.CredentialProvider/Controls/SecurePasswordTextboxControl.cs index 78efab8..d760277 100644 --- a/src/Lithnet.CredentialProvider/Controls/SecurePasswordTextboxControl.cs +++ b/src/Lithnet.CredentialProvider/Controls/SecurePasswordTextboxControl.cs @@ -13,13 +13,13 @@ namespace Lithnet.CredentialProvider private SecureString password; /// - /// Creates a new control + /// Creates a new control. /// /// The unique key for this control public SecurePasswordTextboxControl(string key) : this(key, null) { } /// - /// Creates a new control + /// Creates a new control. /// /// The unique key for this control /// The label associated with the control @@ -67,4 +67,4 @@ namespace Lithnet.CredentialProvider return clone; } } -} \ No newline at end of file +} diff --git a/src/Lithnet.CredentialProvider/Controls/SimpleList.cs b/src/Lithnet.CredentialProvider/Controls/SimpleList.cs index b019ef9..3b0f41b 100644 --- a/src/Lithnet.CredentialProvider/Controls/SimpleList.cs +++ b/src/Lithnet.CredentialProvider/Controls/SimpleList.cs @@ -11,7 +11,14 @@ namespace Lithnet.CredentialProvider { private readonly List backingList = new List(); + /// + /// Occurs after an item is added to the list. + /// public event EventHandler ItemAdded; + + /// + /// Occurs after an item is removed from the list. The event value is the former zero-based index of the item. + /// public event EventHandler ItemRemoved; /// @@ -80,4 +87,4 @@ namespace Lithnet.CredentialProvider } } } -} \ No newline at end of file +} diff --git a/src/Lithnet.CredentialProvider/Controls/SmallLabelControl.cs b/src/Lithnet.CredentialProvider/Controls/SmallLabelControl.cs index 5be1e7a..d264c0c 100644 --- a/src/Lithnet.CredentialProvider/Controls/SmallLabelControl.cs +++ b/src/Lithnet.CredentialProvider/Controls/SmallLabelControl.cs @@ -9,13 +9,13 @@ namespace Lithnet.CredentialProvider public class SmallLabelControl : ControlBase { /// - /// Creates a new control + /// Creates a new control. /// /// The unique key for this control public SmallLabelControl(string key) : this(key, null, false) { } /// - /// Creates a new control + /// Creates a new control. /// /// The unique key for this control /// The label associated with the control @@ -37,4 +37,4 @@ namespace Lithnet.CredentialProvider return new SmallLabelControl(this); } } -} \ No newline at end of file +} diff --git a/src/Lithnet.CredentialProvider/Controls/SubmitButtonControl.cs b/src/Lithnet.CredentialProvider/Controls/SubmitButtonControl.cs index 8aa30c5..9351842 100644 --- a/src/Lithnet.CredentialProvider/Controls/SubmitButtonControl.cs +++ b/src/Lithnet.CredentialProvider/Controls/SubmitButtonControl.cs @@ -10,14 +10,14 @@ namespace Lithnet.CredentialProvider private ControlBase adjacentToControl; /// - /// Creates a new control + /// Creates a new control. /// /// The unique key for this control /// The control that the submit button should appear adjacent to public SubmitButtonControl(string key, ControlBase adjacentToControl) : this(key, null, adjacentToControl) { } /// - /// Creates a new control + /// Creates a new control. /// /// The unique key for this control /// The label associated with the control @@ -62,4 +62,4 @@ namespace Lithnet.CredentialProvider return clone; } } -} \ No newline at end of file +} diff --git a/src/Lithnet.CredentialProvider/Controls/TextboxControl.cs b/src/Lithnet.CredentialProvider/Controls/TextboxControl.cs index 046f6a5..e780148 100644 --- a/src/Lithnet.CredentialProvider/Controls/TextboxControl.cs +++ b/src/Lithnet.CredentialProvider/Controls/TextboxControl.cs @@ -13,13 +13,13 @@ namespace Lithnet.CredentialProvider private TextboxControl(TextboxControl source) : base(source) { } /// - /// Creates a new control + /// Creates a new control. /// /// The unique key for this control public TextboxControl(string key) : this(key, null) { } /// - /// Creates a new control + /// Creates a new control. /// /// The unique key for this control /// The label associated with the control @@ -58,4 +58,4 @@ namespace Lithnet.CredentialProvider return clone; } } -} \ No newline at end of file +} diff --git a/src/Lithnet.CredentialProvider/Controls/UserTileControl.cs b/src/Lithnet.CredentialProvider/Controls/UserTileControl.cs index f37deb3..d37ab75 100644 --- a/src/Lithnet.CredentialProvider/Controls/UserTileControl.cs +++ b/src/Lithnet.CredentialProvider/Controls/UserTileControl.cs @@ -3,29 +3,30 @@ namespace Lithnet.CredentialProvider { /// - /// A control that displays the user's tile image + /// Represents a control that displays the user's tile image. /// + /// See for the image transparency behaviour of each credential tile version. public class UserTileControl : BitmapControl { /// - /// Creates a new control + /// Creates a new control. /// - /// The unique key for this control + /// The unique key for this control. public UserTileControl(string key) : this(key, null, null) { } /// - /// Creates a new control + /// Creates a new control. /// - /// The unique key for this control - /// The label associated with the control + /// The unique key for this control. + /// The label associated with the control. public UserTileControl(string key, string label) : this(key, label, null) { } /// - /// Creates a new control + /// Creates a new control. /// - /// The unique key for this control - /// The label associated with the control - /// The bitmap to use as the user's tile image + /// The unique key for this control. + /// The label associated with the control. + /// The bitmap to use as the user's tile image. public UserTileControl(string key, string label, Bitmap bitmap) : base(key, label, false, bitmap) { } private UserTileControl(UserTileControl source) : base(source) { } @@ -35,4 +36,4 @@ namespace Lithnet.CredentialProvider return new UserTileControl(this); } } -} \ No newline at end of file +} diff --git a/src/Lithnet.CredentialProvider/CredentialProviderBase.cs b/src/Lithnet.CredentialProvider/CredentialProviderBase.cs index b1fb490..7290b2a 100644 --- a/src/Lithnet.CredentialProvider/CredentialProviderBase.cs +++ b/src/Lithnet.CredentialProvider/CredentialProviderBase.cs @@ -67,6 +67,10 @@ namespace Lithnet.CredentialProvider /// public CredentialSerialization InboundSerialization { get; private set; } + /// + /// Initializes a credential provider and obtains its identifier from the on the derived class. + /// + /// The derived credential provider class does not have a . protected CredentialProviderBase() { this.LoggerFactory = this.GetLoggerFactory(); @@ -84,13 +88,13 @@ namespace Lithnet.CredentialProvider } /// - /// Gets a logger factory. Override this method and provide an implementation of to enable credential provider logging + /// Gets a logger factory. Override this method and provide an implementation of to enable credential provider logging. /// - /// An ILoggerFactory instance + /// An instance. protected virtual ICredentialProviderLoggerFactory GetLoggerFactory() { return TraceLoggerFactory.Instance; } /// - /// Gets a value indicating if the credential provider supports the provided by LogonUI or CredUI + /// Gets a value indicating whether the credential provider supports the provided by LogonUI or CredUI. /// /// The usage scenario /// Additional flags provided by CredUI @@ -116,10 +120,22 @@ namespace Lithnet.CredentialProvider /// public abstract bool ShouldIncludeGenericTile(); + /// + /// Gets or sets the tile that the credential provider reports as the default credential. + /// protected internal CredentialTile DefaultTile { get; set; } + /// + /// Gets or sets a value that indicates whether Logon UI or Credential UI should immediately request serialization from the default tile. + /// protected internal bool DefaultTileAutoLogon { get; set; } + /// + /// Sets the default tile, configures its automatic logon request, and notifies the credential UI to enumerate the tiles again. + /// + /// A tile in the current collection. + /// to make Logon UI or Credential UI immediately request serialization from the default tile; otherwise, . + /// is not in the current collection. public void SetDefaultTile(CredentialTile tile, bool autoLogon) { if (this.DefaultTile == tile && this.DefaultTileAutoLogon == autoLogon) @@ -188,7 +204,7 @@ namespace Lithnet.CredentialProvider } /// - /// This method is used to generate the generic tile for this credential provider. This is called when return true + /// Creates the generic tile for this credential provider. This method is called when returns . /// public abstract CredentialTile CreateGenericTile(); @@ -308,4 +324,4 @@ namespace Lithnet.CredentialProvider } } } -} \ No newline at end of file +} diff --git a/src/Lithnet.CredentialProvider/CredentialTile.cs b/src/Lithnet.CredentialProvider/CredentialTile.cs index 2252f47..82862c2 100644 --- a/src/Lithnet.CredentialProvider/CredentialTile.cs +++ b/src/Lithnet.CredentialProvider/CredentialTile.cs @@ -15,6 +15,10 @@ namespace Lithnet.CredentialProvider private protected ICredentialProviderCredentialEvents2 events2; private protected ControlCollection controls; + /// + /// Initializes a version 1 credential tile. + /// + /// The credential provider that owns this tile. protected CredentialTile(CredentialProviderBase credentialProvider) { this.CredentialProvider = credentialProvider; @@ -34,7 +38,7 @@ namespace Lithnet.CredentialProvider public bool IsSelected { get; private set; } /// - /// Gets a value that indicates if the user should be automatically logged on when the tile is selected. The tile must also have IsDefault set to true. + /// Gets a value that indicates whether Logon UI or Credential UI should immediately request serialization from this tile. The tile must also be the default tile. /// public bool IsDefaultTileAutoLogon { @@ -111,7 +115,7 @@ namespace Lithnet.CredentialProvider } /// - /// Indicates to the host that multiple updates need to be made to the fields, and that it should delay updating the UI until + /// Indicates to the host that multiple fields will be updated and that it should delay updating the UI until is called. /// /// public void BeginBulkFieldUpdate() @@ -176,7 +180,7 @@ namespace Lithnet.CredentialProvider protected virtual void OnDeselected() { } /// - /// Called just before credentials are serialized and returned to the host + /// Called immediately before credentials are serialized and returned to the host /// protected virtual void OnBeforeSerialize() { } diff --git a/src/Lithnet.CredentialProvider/CredentialTile2.cs b/src/Lithnet.CredentialProvider/CredentialTile2.cs index e0a266d..bde3e4c 100644 --- a/src/Lithnet.CredentialProvider/CredentialTile2.cs +++ b/src/Lithnet.CredentialProvider/CredentialTile2.cs @@ -23,8 +23,17 @@ /// This does not apply in scenarios where a personalized tile is provided public GenericTileDisplayMode GenericTileDisplayMode { get; set; } + /// + /// Initializes a generic version 2 credential tile. + /// + /// The credential provider that owns this tile. protected CredentialTile2(CredentialProviderBase credentialProvider) : this(credentialProvider, null) { } + /// + /// Initializes a version 2 credential tile for a user. + /// + /// The credential provider that owns this tile. + /// The user represented by this tile, or for a generic tile. protected CredentialTile2(CredentialProviderBase credentialProvider, CredentialProviderUser user) : base(credentialProvider) { this.User = user; diff --git a/src/Lithnet.CredentialProvider/CredentialTile3.cs b/src/Lithnet.CredentialProvider/CredentialTile3.cs index fa0f365..1807e5d 100644 --- a/src/Lithnet.CredentialProvider/CredentialTile3.cs +++ b/src/Lithnet.CredentialProvider/CredentialTile3.cs @@ -4,13 +4,22 @@ using Lithnet.CredentialProvider.Interop; namespace Lithnet.CredentialProvider { /// - /// Represents a user credential tile that implements the functionality of and , but includes support for dynamically updating bitmap images. + /// Represents a version 3 credential tile that preserves transparency in bitmap controls. /// - /// This interface is public, but undocumented by Microsoft. It is recommended to use tiles unless this specific functionality is needed + /// Inherit from this class when a or must preserve the image's alpha channel. The property does not apply to this tile type. Microsoft does not publish documentation for the underlying version 3 credential interfaces, so use unless you need image transparency. public abstract partial class CredentialTile3 : CredentialTile2 { + /// + /// Initializes a generic version 3 credential tile. + /// + /// The credential provider that owns this tile. protected CredentialTile3(CredentialProviderBase credentialProvider) : this(credentialProvider, null) { } + /// + /// Initializes a version 3 credential tile for a user. + /// + /// The credential provider that owns this tile. + /// The user represented by this tile, or for a generic tile. protected CredentialTile3(CredentialProviderBase credentialProvider, CredentialProviderUser user) : base(credentialProvider, user) { } } } diff --git a/src/Lithnet.CredentialProvider/Enums/ConsentUIElevationReason.cs b/src/Lithnet.CredentialProvider/Enums/ConsentUIElevationReason.cs index 9c07c1e..c72d217 100644 --- a/src/Lithnet.CredentialProvider/Enums/ConsentUIElevationReason.cs +++ b/src/Lithnet.CredentialProvider/Enums/ConsentUIElevationReason.cs @@ -1,5 +1,8 @@ namespace Lithnet.CredentialProvider { + /// + /// Specifies the reason that Consent UI received an elevation request. + /// public enum ConsentUIElevationReason { /// @@ -86,4 +89,4 @@ /// NumReasons = 9, } -} \ No newline at end of file +} diff --git a/src/Lithnet.CredentialProvider/Enums/ConsentUIFlags.cs b/src/Lithnet.CredentialProvider/Enums/ConsentUIFlags.cs index 04a82bf..4d80e7f 100644 --- a/src/Lithnet.CredentialProvider/Enums/ConsentUIFlags.cs +++ b/src/Lithnet.CredentialProvider/Enums/ConsentUIFlags.cs @@ -2,9 +2,15 @@ namespace Lithnet.CredentialProvider { + /// + /// Specifies how Consent UI should display and verify an elevation request. + /// [Flags] public enum ConsentUIFlags { + /// + /// The purpose of the 0x01 flag is not documented. + /// SkipSignatureVerification = 0x01, /// @@ -12,8 +18,19 @@ namespace Lithnet.CredentialProvider /// SecureDesktop = 0x02, + /// + /// The purpose of the 0x04 flag is not documented. + /// Unknown1 = 0x04, + + /// + /// The purpose of the 0x08 flag is not documented. + /// Unknown2 = 0x08, + + /// + /// The purpose of the 0x10 flag is not documented. + /// Unknown3 = 0x10, /// @@ -42,6 +59,9 @@ namespace Lithnet.CredentialProvider /// AutoElevationOther = 0x100, + /// + /// The purpose of the 0x200 flag is not documented. + /// Unknown4 = 0x200, /// diff --git a/src/Lithnet.CredentialProvider/Enums/ConsentUIMsiAction.cs b/src/Lithnet.CredentialProvider/Enums/ConsentUIMsiAction.cs index 24e7ad1..d4aba4c 100644 --- a/src/Lithnet.CredentialProvider/Enums/ConsentUIMsiAction.cs +++ b/src/Lithnet.CredentialProvider/Enums/ConsentUIMsiAction.cs @@ -2,10 +2,24 @@ namespace Lithnet.CredentialProvider { + /// + /// Specifies the Windows Installer action described by Consent UI data. + /// public enum ConsentUIMsiAction : uint { + /// + /// Installs a Windows Installer package. + /// Install = 0, + + /// + /// Uninstalls a Windows Installer package. + /// Uninstall = 1, + + /// + /// Updates or repairs an installed Windows Installer package. + /// Update = 2 } } diff --git a/src/Lithnet.CredentialProvider/Enums/ConsentUIPromptType.cs b/src/Lithnet.CredentialProvider/Enums/ConsentUIPromptType.cs index eee927a..0a5920f 100644 --- a/src/Lithnet.CredentialProvider/Enums/ConsentUIPromptType.cs +++ b/src/Lithnet.CredentialProvider/Enums/ConsentUIPromptType.cs @@ -1,10 +1,28 @@ namespace Lithnet.CredentialProvider { + /// + /// Specifies how Consent UI obtains approval for an elevation request. + /// public enum ConsentUIPromptType { + /// + /// The prompt type is not known. + /// Unknown = 0, + + /// + /// Uses the Consent UI automatic administrator mode. + /// AutomaticAdmin = 1, + + /// + /// Requests consent from an administrator. + /// Consent = 2, + + /// + /// Requests administrator credentials. + /// Credentials = 3 } } diff --git a/src/Lithnet.CredentialProvider/Enums/ConsentUIType.cs b/src/Lithnet.CredentialProvider/Enums/ConsentUIType.cs index 91446e7..24ac003 100644 --- a/src/Lithnet.CredentialProvider/Enums/ConsentUIType.cs +++ b/src/Lithnet.CredentialProvider/Enums/ConsentUIType.cs @@ -1,12 +1,38 @@ namespace Lithnet.CredentialProvider { + /// + /// Identifies the type of data supplied to Consent UI for an elevation request. + /// public enum ConsentUIType { + /// + /// The data describes an executable file. + /// Exe = 0, + + /// + /// The data describes an elevated COM object. + /// Com = 1, + + /// + /// The data describes a Windows Installer package. + /// Msi = 2, + + /// + /// The data describes an ActiveX installation. + /// ActiveX = 3, + + /// + /// The data uses the CredCollect structure. The purpose of this structure is not documented. + /// CredCollect = 4, + + /// + /// The data describes a packaged application. + /// Msix = 5 } } diff --git a/src/Lithnet.CredentialProvider/Enums/CredUIWinFlags.cs b/src/Lithnet.CredentialProvider/Enums/CredUIWinFlags.cs index 182f8ad..948f68c 100644 --- a/src/Lithnet.CredentialProvider/Enums/CredUIWinFlags.cs +++ b/src/Lithnet.CredentialProvider/Enums/CredUIWinFlags.cs @@ -6,6 +6,9 @@ using System.Threading.Tasks; namespace Lithnet.CredentialProvider { + /// + /// Specifies options that control the Windows credential user interface. + /// [Flags] public enum CredUIWinFlags { diff --git a/src/Lithnet.CredentialProvider/Enums/SerializationResponse.cs b/src/Lithnet.CredentialProvider/Enums/SerializationResponse.cs index d6a6927..4593df5 100644 --- a/src/Lithnet.CredentialProvider/Enums/SerializationResponse.cs +++ b/src/Lithnet.CredentialProvider/Enums/SerializationResponse.cs @@ -1,5 +1,8 @@ namespace Lithnet.CredentialProvider { + /// + /// Specifies how the credential UI should continue after a credential provider handles a serialization request. + /// public enum SerializationResponse { /// diff --git a/src/Lithnet.CredentialProvider/Enums/UsageScenario.cs b/src/Lithnet.CredentialProvider/Enums/UsageScenario.cs index 9f43157..1bdbf06 100644 --- a/src/Lithnet.CredentialProvider/Enums/UsageScenario.cs +++ b/src/Lithnet.CredentialProvider/Enums/UsageScenario.cs @@ -18,7 +18,7 @@ /// /// Workstation unlock. Credential providers that implement this scenario should be prepared to serialize credentials to the local authority for authentication. These credential providers also need to enumerate the currently logged-in user as the default tile. /// - /// Starting in Windows 10, the CPUS_LOGON and CPUS_UNLOCK_WORKSTATION user scenarios have been combined. This enables the system to support multiple users logging into a machine without creating and switching sessions unnecessarily. Any user on the machine can log into it once it has been locked without needing to back out of a current session and create a new one. Because of this, CPUS_LOGON can be used both for logging onto a system or when a workstation is unlocked. However, CPUS_LOGON cannot be used in all cases. Because of policy restrictions imposed by various systems, sometimes it is necessary for the user scenario to be CPUS_UNLOCK_WORKSTATION. Your credential provider should be robust enough to create the appropriate credential structure based on the scenario given to it. Windows will request the appropriate user scenario based on the situation. Some of the factors that impact whether or not a CPUS_UNLOCK_WORKSTATION scenario must be used include the following. Note that this is just a subset of possibilities. + /// Starting in Windows 10, the CPUS_LOGON and CPUS_UNLOCK_WORKSTATION user scenarios have been combined. This enables the system to support multiple users logging into a machine without creating and switching sessions unnecessarily. Any user on the machine can log into it once it has been locked without needing to back out of a current session and create a new one. Because of this, CPUS_LOGON can be used both for logging onto a system or when a workstation is unlocked. However, CPUS_LOGON cannot be used in all cases. Because of policy restrictions imposed by various systems, sometimes it is necessary for the user scenario to be CPUS_UNLOCK_WORKSTATION. Your credential provider should be robust enough to create the appropriate credential structure based on the scenario given to it. Windows will request the appropriate user scenario based on the situation. Some of the factors that impact whether or not a CPUS_UNLOCK_WORKSTATION scenario must be used include the following. This is a subset of the possible factors. /// - The operating system of the device. /// - Whether this is a console or remote session. /// - Group policies such as hiding entry points for fast user switching, or interactive logon that does not display the user's last name. @@ -31,7 +31,7 @@ ChangePassword, /// - /// Credential UI. This scenario enables you to use credentials serialized by the credential provider to be used as authentication on remote machines. This is also the scenario used for over-the-shoulder prompting in User Access Control. This scenario uses a different instance of the credential provider than the one used for , , and , so the state of the credential provider cannot be maintained across the different scenarios. + /// Credential UI. This scenario enables you to use credentials serialized by the credential provider as authentication on remote machines. This is also the scenario used for over-the-shoulder prompting in User Access Control. This scenario uses a different instance of the credential provider than the one used for , , and , so the state of the credential provider cannot be maintained across the different scenarios. /// CredUI, diff --git a/src/Lithnet.CredentialProvider/Lithnet.CredentialProvider.csproj b/src/Lithnet.CredentialProvider/Lithnet.CredentialProvider.csproj index a3a8312..672f392 100644 --- a/src/Lithnet.CredentialProvider/Lithnet.CredentialProvider.csproj +++ b/src/Lithnet.CredentialProvider/Lithnet.CredentialProvider.csproj @@ -10,6 +10,7 @@ true 9 true + true @@ -25,6 +26,7 @@ true Lithnet.CredentialProvider MIT + README.md https://github.com/lithnet/windows-credential-provider https://github.com/lithnet/windows-credential-provider D:\dev\nuget\packages @@ -50,6 +52,10 @@ + + + + all diff --git a/src/Lithnet.CredentialProvider/Logging/ICredentialProviderLogger.cs b/src/Lithnet.CredentialProvider/Logging/ICredentialProviderLogger.cs index 874a906..75d9cde 100644 --- a/src/Lithnet.CredentialProvider/Logging/ICredentialProviderLogger.cs +++ b/src/Lithnet.CredentialProvider/Logging/ICredentialProviderLogger.cs @@ -2,16 +2,40 @@ namespace Lithnet.CredentialProvider { + /// + /// Receives log messages from a credential provider and its credential tiles. + /// public interface ICredentialProviderLogger { + /// + /// Logs an error message and its associated exception. + /// + /// The exception associated with the error. + /// The error message. void LogError(Exception ex, string message); + /// + /// Logs an error message. + /// + /// The error message. void LogError(string message); + /// + /// Logs a trace message. + /// + /// The trace message. void LogTrace(string message); + /// + /// Logs an informational message. + /// + /// The informational message. void LogInformation(string message); + /// + /// Logs a warning message. + /// + /// The warning message. void LogWarning(string message); } } diff --git a/src/Lithnet.CredentialProvider/Logging/ICredentialProviderLoggerFactory.cs b/src/Lithnet.CredentialProvider/Logging/ICredentialProviderLoggerFactory.cs index 4f03efe..cda6b61 100644 --- a/src/Lithnet.CredentialProvider/Logging/ICredentialProviderLoggerFactory.cs +++ b/src/Lithnet.CredentialProvider/Logging/ICredentialProviderLoggerFactory.cs @@ -2,10 +2,23 @@ namespace Lithnet.CredentialProvider { + /// + /// Creates loggers for credential provider components. + /// public interface ICredentialProviderLoggerFactory { + /// + /// Creates a logger for the specified component type. + /// + /// The type that will write log messages. + /// A logger for the specified type. ICredentialProviderLogger CreateLogger(Type type); + /// + /// Creates a logger for the specified component type. + /// + /// The type that will write log messages. + /// A logger for the specified type. ICredentialProviderLogger CreateLogger(); } } diff --git a/src/Lithnet.CredentialProvider/Logging/TraceLogger.cs b/src/Lithnet.CredentialProvider/Logging/TraceLogger.cs index cbbbc06..d8249f2 100644 --- a/src/Lithnet.CredentialProvider/Logging/TraceLogger.cs +++ b/src/Lithnet.CredentialProvider/Logging/TraceLogger.cs @@ -3,28 +3,52 @@ using System.Diagnostics; namespace Lithnet.CredentialProvider { + /// + /// Writes credential provider log messages to . + /// public class TraceLogger : ICredentialProviderLogger { + /// + /// Writes an error message and its associated exception to . + /// + /// The exception associated with the error. + /// The error message. public void LogError(Exception ex, string v) { Trace.WriteLine($"{v}\r\n\r\n{ex?.ToString()}"); } + /// + /// Writes an error message to . + /// + /// The error message. public void LogError(string v) { Trace.WriteLine(v); } + /// + /// Writes an informational message to . + /// + /// The informational message. public void LogInformation(string message) { Trace.WriteLine(message); } + /// + /// Writes a trace message to . + /// + /// The trace message. public void LogTrace(string v) { Trace.WriteLine(v); } + /// + /// Writes a warning message to . + /// + /// The warning message. public void LogWarning(string message) { Trace.WriteLine(message); diff --git a/src/Lithnet.CredentialProvider/Logging/TraceLoggerFactory.cs b/src/Lithnet.CredentialProvider/Logging/TraceLoggerFactory.cs index 6b46753..38bf084 100644 --- a/src/Lithnet.CredentialProvider/Logging/TraceLoggerFactory.cs +++ b/src/Lithnet.CredentialProvider/Logging/TraceLoggerFactory.cs @@ -2,13 +2,26 @@ namespace Lithnet.CredentialProvider { + /// + /// Creates instances. + /// public class TraceLoggerFactory : ICredentialProviderLoggerFactory { + /// + /// Creates a trace logger for the specified component type. + /// + /// The type that will write log messages. + /// A trace logger for the specified type. public ICredentialProviderLogger CreateLogger(Type type) { return new TraceLogger(); } + /// + /// Creates a trace logger for the specified component type. + /// + /// The type that will write log messages. + /// A trace logger for the specified type. public ICredentialProviderLogger CreateLogger() { return new TraceLogger(); @@ -16,6 +29,9 @@ namespace Lithnet.CredentialProvider private static readonly TraceLoggerFactory loggerFactory = new TraceLoggerFactory(); + /// + /// Gets the shared trace logger factory. + /// public static ICredentialProviderLoggerFactory Instance => loggerFactory; } } diff --git a/src/samples/Lithnet.CredentialProvider.Sample.Core.x64/TestCredentialProviderCoreX64.cs b/src/samples/Lithnet.CredentialProvider.Sample.Core.x64/TestCredentialProviderCoreX64.cs index cd155ec..4eacd22 100644 --- a/src/samples/Lithnet.CredentialProvider.Sample.Core.x64/TestCredentialProviderCoreX64.cs +++ b/src/samples/Lithnet.CredentialProvider.Sample.Core.x64/TestCredentialProviderCoreX64.cs @@ -101,6 +101,7 @@ namespace Lithnet.CredentialProvider.Samples private static Bitmap CreateTransparentUserTile() { + // CredentialTile3 preserves the alpha channel in this image. CredentialTile and CredentialTile2 render it against the control's BackgroundColor. Bitmap image = new Bitmap(128, 128, PixelFormat.Format32bppArgb); using (Graphics graphics = Graphics.FromImage(image)) diff --git a/src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/TestCredentialProviderTile.cs b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/TestCredentialProviderTile.cs index 204d0c1..20cd9ea 100644 --- a/src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/TestCredentialProviderTile.cs +++ b/src/samples/Lithnet.CredentialProvider.Sample.Framework.x64/TestCredentialProviderTile.cs @@ -5,6 +5,9 @@ using Microsoft.Extensions.Logging; namespace Lithnet.CredentialProvider.Samples { + /// + /// Demonstrates a version 3 credential tile that preserves image transparency. + /// public class TestCredentialProviderTile : CredentialTile3 { private TextboxControl UsernameControl; From 4d20fa9d4391d4775c002645a086bfc6d7051893 Mon Sep 17 00:00:00 2001 From: Ryan Newington Date: Sun, 30 Aug 2026 09:31:31 +1000 Subject: [PATCH 7/9] Skip release stages for pull request builds --- azure-pipelines.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7a6b4d2..f8ed43e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -164,6 +164,9 @@ stages: - stage: build_provider displayName: Build credential provider dependsOn: test_provider + # Pull request builds validate every target framework in test_provider. Release + # stages require signing secrets and can publish packages and GitHub releases. + condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) jobs: - job: "build_provider_job" steps: From 67013abca4bc8e56f52a2c75b7f284682c189390 Mon Sep 17 00:00:00 2001 From: Ryan Newington Date: Sun, 30 Aug 2026 09:37:48 +1000 Subject: [PATCH 8/9] Revert "Skip release stages for pull request builds" This reverts commit 4d20fa9d4391d4775c002645a086bfc6d7051893. --- azure-pipelines.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f8ed43e..7a6b4d2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -164,9 +164,6 @@ stages: - stage: build_provider displayName: Build credential provider dependsOn: test_provider - # Pull request builds validate every target framework in test_provider. Release - # stages require signing secrets and can publish packages and GitHub releases. - condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) jobs: - job: "build_provider_job" steps: From 8bb728c77daa9cfd75468e10c04fb0f8817883fd Mon Sep 17 00:00:00 2001 From: Ryan Newington Date: Sun, 30 Aug 2026 10:04:45 +1000 Subject: [PATCH 9/9] Align release pipeline with shared release tools --- azure-pipelines.yml | 251 ++++++++++++++++---------------------------- 1 file changed, 93 insertions(+), 158 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7a6b4d2..8c8d392 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -27,10 +27,14 @@ variables: value: $(build.version.major).$(build.version.minor).$(build.version.revision) - name: build.date value: $[format('{0:yyyy}-{0:MM}-{0:dd}T{0:HH}:{0:mm}:{0:ss}', pipeline.startTime)] - - group: Azure KeyVault Code Signing + - name: azure.subscription + value: 'ProductionServices' name: $(build.version.major).$(build.version.minor).$(build.version.revision)$(build.version.suffix) + +# This pipeline is manual only. Repository pushes and pull requests must not start builds. trigger: none +pr: none stages: - stage: test_provider @@ -167,6 +171,8 @@ stages: jobs: - job: "build_provider_job" steps: + - template: templates/checkout.yaml@release_tools + - task: DotNetCoreCLI@2 displayName: dotnet build inputs: @@ -174,41 +180,13 @@ stages: arguments: '-c $(buildConfiguration) -p:Version=$(build.version) -p:GeneratePackageOnBuild=false' projects: 'src/Lithnet.CredentialProvider/Lithnet.CredentialProvider.csproj' - - task: DotNetCoreCLI@2 - inputs: - command: 'custom' - custom: 'tool' - arguments: 'update --global azuresigntool' - displayName: Install AzureSignTool - - - task: PowerShell@2 - displayName: 'Sign files with AzureSignTool' - inputs: - targetType: 'inline' - script: | - $files = @() - $files += (Get-ChildItem -Recurse -Path "$(Build.SourcesDirectory)\Lithnet*.dll").FullName - - write-host "Signing $($files.Length) files:" - write-output $files - - $cmdargs = @( - "sign", - "-d", "Lithnet Windows Credential Provider", - "-kvu", "$(akv.url)", - "-kvi", "$(akv.applicationID)", - "-kvs", "$(akv.secret)", - "-kvt", "$(akv.tenantId)", - "-kvc", "$(akv.certificateName)", - "-tr", "http://timestamp.digicert.com", - "-td", "sha256" - ) - - $cmdargs += $files - - & AzureSignTool $cmdargs - failOnStderr: true - showWarnings: true + - template: templates/codesign.yaml@release_tools + parameters: + path: + - '$(Build.SourcesDirectory)/**/Lithnet.CredentialProvider.dll' + environment: 'prod' + azureSubscription: '$(azure.subscription)' + description: 'Lithnet Windows Credential Provider' - task: DotNetCoreCLI@2 displayName: dotnet pack @@ -221,70 +199,14 @@ stages: versioningScheme: 'byEnvVar' versionEnvVar: 'build.version' - - task: DotNetCoreCLI@2 - inputs: - command: 'custom' - custom: 'tool' - arguments: 'update --global NuGetKeyVaultSignTool' - displayName: Install NugetKeyVaultSignTool - - - task: PowerShell@2 - displayName: 'Sign Nuget package' - inputs: - targetType: 'inline' - script: | - $cmdargs = @( - "sign", "$(Build.ArtifactStagingDirectory)\cp\Lithnet.CredentialProvider.$(build.version).nupkg" - "-fd", "sha256", - "-kvu", "$(akv.url)", - "-kvi", "$(akv.applicationID)", - "-kvs", "$(akv.secret)", - "-kvt", "$(akv.tenantId)", - "-kvc", "$(akv.certificateName)", - "-tr", "http://timestamp.digicert.com", - "-td", "sha256" - ) - - & NuGetKeyVaultSignTool $cmdargs - failOnStderr: true - showWarnings: true - - - task: PowerShell@2 - displayName: 'Sign Nuget symbols package' - inputs: - targetType: 'inline' - script: | - $cmdargs = @( - "sign", "$(Build.ArtifactStagingDirectory)\cp\Lithnet.CredentialProvider.$(build.version).snupkg" - "-fd", "sha256", - "-kvu", "$(akv.url)", - "-kvi", "$(akv.applicationID)", - "-kvs", "$(akv.secret)", - "-kvt", "$(akv.tenantId)", - "-kvc", "$(akv.certificateName)", - "-tr", "http://timestamp.digicert.com", - "-td", "sha256" - ) - - & NuGetKeyVaultSignTool $cmdargs - failOnStderr: true - showWarnings: true - - - task: DotNetCoreCLI@2 - displayName: Publish package to internal feed - inputs: - command: 'push' - packagesToPush: '$(Build.ArtifactStagingDirectory)/cp/*.nupkg' - nuGetFeedType: 'internal' - publishVstsFeed: '91a552bc-359d-4f28-bdbd-f36f71cfdf81' - - - task: DotNetCoreCLI@2 - displayName: Publish symbols to internal feed - inputs: - command: 'push' - packagesToPush: '$(Build.ArtifactStagingDirectory)/cp/*.snupkg' - nuGetFeedType: 'internal' - publishVstsFeed: '91a552bc-359d-4f28-bdbd-f36f71cfdf81' + - template: templates/codesign.yaml@release_tools + parameters: + path: + - '$(Build.ArtifactStagingDirectory)/cp/*.nupkg' + - '$(Build.ArtifactStagingDirectory)/cp/*.snupkg' + environment: 'prod' + azureSubscription: '$(azure.subscription)' + description: 'Lithnet Windows Credential Provider' - task: PublishPipelineArtifact@1 displayName: Publish nuget artifact @@ -293,65 +215,78 @@ stages: publishLocation: 'pipeline' artifact: cp - - task: GitHubRelease@1 - inputs: - gitHubConnection: github.com_lithnet # string. Required. GitHub connection (OAuth or PAT). - repositoryName: '$(Build.Repository.Name)' # string. Required. Repository. Default: $(Build.Repository.Name). - action: 'create' # 'create' | 'edit' | 'delete'. Required. Action. Default: create. - #target: '$(Build.SourceVersion)' # string. Required when action = create || action = edit. Target. Default: $(Build.SourceVersion). - tagSource: 'userSpecifiedTag' # 'gitTag' | 'userSpecifiedTag'. Required when action = create. Tag source. Default: gitTag. - #tagPattern: # string. Optional. Use when tagSource = gitTag. Tag Pattern. - tag: v$(build.version) # string. Required when action = edit || action = delete || tagSource = userSpecifiedTag. Tag. - title: v$(build.version) # string. Optional. Use when action = create || action = edit. Release title. - #releaseNotesSource: 'filePath' # 'filePath' | 'inline'. Optional. Use when action = create || action = edit. Release notes source. Default: filePath. - #releaseNotesFilePath: # string. Optional. Use when releaseNotesSource = filePath. Release notes file path. - #releaseNotesInline: # string. Optional. Use when releaseNotesSource = inline. Release notes. - assets: | # string. Optional. Use when action = create || action = edit. Assets. Default: $(Build.ArtifactStagingDirectory)/*. - $(Build.ArtifactStagingDirectory)/cp/*.nupkg - #assetUploadMode: 'delete' # 'delete' | 'replace'. Optional. Use when action = edit. Asset upload mode. Default: delete. - #isDraft: false # boolean. Optional. Use when action = create || action = edit. Draft release. Default: false. - isPreRelease: true # boolean. Optional. Use when action = create || action = edit. Pre-release. Default: false. - addChangeLog: true # boolean. Optional. Use when action = create || action = edit. Add changelog. Default: true. - # Changelog configuration - changeLogCompareToRelease: 'lastFullRelease' # 'lastFullRelease' | 'lastNonDraftRelease' | 'lastNonDraftReleaseByTag'. Required when addChangeLog = true. Compare to. Default: lastFullRelease. - #changeLogCompareToReleaseTag: # string. Required when changeLogCompareToRelease = lastNonDraftReleaseByTag && addChangeLog = true. Release Tag. - changeLogType: 'commitBased' # 'commitBased' | 'issueBased'. Required when addChangeLog = true. Changelog type. Default: commitBased. - #changeLogLabels: '[{ "label" : "bug", "displayName" : "Bugs", "state" : "closed" }]' # string. Optional. Use when changeLogType = issueBased && addChangeLog = true. Categories. Default: [{ "label" : "bug", "displayName" : "Bugs", "state" : "closed" }]. - -- stage: publish_nuget - displayName: Publish CredProvider to nuget.org - dependsOn: "build_provider" +# Publish signed packages to Azure Artifacts only after the test and build stages succeed. +- stage: publish_internal + displayName: Publish to internal feed + dependsOn: build_provider jobs: - - deployment: 'PublishPackages' - environment: 'Public nuget feed' - displayName: Publish packages to public nuget feed - pool: - vmImage: windows-2022 - strategy: - runOnce: - deploy: - steps: + - job: publish_internal_job + displayName: Publish packages to internal feed + steps: + - checkout: none + + - download: current + artifact: cp + + - template: templates/publish-nuget.yaml@release_tools + parameters: + nugetPackagePath: '$(Pipeline.Workspace)/cp/*.nupkg' + symbolPackagePath: '$(Pipeline.Workspace)/cp/*.snupkg' + publishInternal: true + publishExternal: false + +# Public release is limited to main and waits for approval on the Public nuget feed environment. +- stage: publish_prod + displayName: Publish to nuget.org and GitHub + dependsOn: publish_internal + condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) + jobs: + - deployment: publish_nuget + displayName: Publish package to public nuget feed + environment: 'Public nuget feed' + pool: + vmImage: windows-2022 + strategy: + runOnce: + deploy: + steps: - checkout: none + - download: current artifact: cp - - task: NuGetToolInstaller@1 - inputs: - versionSpec: '>=4.9.0-0' - - task: NuGetCommand@2 - displayName: 'Publish nuget package to public feed' - inputs: - command: 'push' - packagesToPush: '$(Pipeline.Workspace)/cp/*.nupkg' - nuGetFeedType: 'external' - publishFeedCredentials: 'WindowsCredentialProviderNuget' - - task: GitHubRelease@1 - inputs: - gitHubConnection: github.com_lithnet # string. Required. GitHub connection (OAuth or PAT). - repositoryName: '$(Build.Repository.Name)' # string. Required. Repository. Default: $(Build.Repository.Name). - action: 'edit' # 'create' | 'edit' | 'delete'. Required. Action. Default: create. - target: '$(Build.SourceVersion)' # string. Required when action = create || action = edit. Target. Default: $(Build.SourceVersion). - tagSource: 'userSpecifiedTag' # 'gitTag' | 'userSpecifiedTag'. Required when action = create. Tag source. Default: gitTag. - #tagPattern: # string. Optional. Use when tagSource = gitTag. Tag Pattern. - tag: v$(build.version) # string. Required when action = edit || action = delete || tagSource = userSpecifiedTag. Tag. - isPreRelease: false # boolean. Optional. Use when action = create || action = edit. Pre-release. Default: false. - addChangeLog: false # boolean. Optional. Use when action = create || action = edit. Add changelog. Default: true. + + - template: templates/publish-nuget.yaml@release_tools + parameters: + nugetPackagePath: '$(Pipeline.Workspace)/cp/*.nupkg' + publishInternal: false + publishExternal: true + externalFeedCredentials: 'WindowsCredentialProviderNuget' + + # Create the GitHub release only after nuget.org accepts the package. + - job: publish_github + displayName: Publish package to GitHub releases + dependsOn: publish_nuget + condition: succeeded() + pool: + vmImage: windows-2022 + steps: + - checkout: none + + - download: current + artifact: cp + + - task: GitHubRelease@1 + displayName: Create GitHub release (v$(build.version)) + inputs: + gitHubConnection: github.com_lithnet + repositoryName: '$(Build.Repository.Name)' + action: create + target: '$(Build.SourceVersion)' + tagSource: userSpecifiedTag + tag: v$(build.version) + title: v$(build.version) + assets: '$(Pipeline.Workspace)/cp/*.nupkg' + isPreRelease: false + addChangeLog: true + changeLogCompareToRelease: lastFullRelease + changeLogType: commitBased