We write our own Credential Provider in C# for authorization in Windows

Credential Provider, used to pass user credentials to the Windows security stack. By default, the system has password, PIN, smart card, and Windows Hello login providers. However, what if they do not suit us?

Credential Providers are based on COM technology and run in the winlogon user interface process. The creation of such a provider in C# has already been described in the article Steve Cyfushowever, in its implementation, the unlocking of the workstation was not correctly processed, and there was a desire to rewrite the code to the Net Core framework, which I most often have to work with.

In order to start developing, you need to set up COM interop that allows you to call .NET code from COM components. For interaction, you need to correctly configure the interfaces, for this you can use the definitions MSDN, or take advantage of the lightweight option using the IDL file that defines the required interfaces from the Windows SDK. All you need to do is convert it to a type library and then convert it to a .NET assembly.

The midl.exe utility is used to compile the type library, but before using it, you need to edit the credentialprovider.idl file. As it turned out, if you use this file without changing some of the methods in the interfaces are not available, in order to fix this, you need to move the CLSID declaration to the beginning of the file.

// C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\um\credentialprovider.idl
[
    uuid(d545db01-e522-4a63-af83-d8ddf954004f), // LIBID_CredentialProviders
]
library CredentialProviders
{
	// код credentialprovider.idl
}

Then you can run the command:

midl .\credentialprovider.idl -target NT100 /x64

After execution, we will receive several files, but we will only be interested in the type library, which needs to be converted to a .NET assembly. Usually the tlbimp.exe utility is used for conversion, but by default this utility expects you to throw exceptions instead of returning an HRESULT. To overcome this problem, the tlbimp2.exe utility was created, unfortunately I could not find its original, so I used the file from Steve’s repository. After executing the command, we get the OTP.Provider.Interop.dll library at the output, which must be linked to the project by adding a link to the resulting file.

./TlbImp2.exe .\credentialprovider.tlb /out:OTP.Provider.Interop.dll /unsafe /preservesig namespace:OTP.Provider

After that, the following interfaces will become available to it, the methods of which must be described:

  • ICredentialProvider – Provides methods used in configuring and managing the credential provider.

  • ICredentialProviderCredential – Provides methods to handle credentials.

  • ICredentialProviderCredential2 – Extends the ICredentialProviderCredential interface by adding a method that retrieves the security identifier (SID) of the user. Credentials are associated with that user and can be grouped on the user’s tile.

  • ICredentialProviderSetUserArray – Provides a method that allows a credential provider to get a set of users to display in the login or credential UI.

The class that implements the ICredentialProvider interface must be made visible to the COM subsystem, and a unique class identifier must be generated for it. Thus, the system will be able to access the library when choosing an authorization method that points to the same identifier.

 [ComVisible(true)]															
 [Guid("D26F523C-A346-4FC8-B9B4-2B57EAEDA723")]
 [ClassInterface(ClassInterfaceType.None)]
 [ProgId("OTP.Provider")]
 public class CredentialProvider : ICredentialProvider
 {
 		// код CredentialProvider
 }

When building the project, you also need to enable the EnableComHosting option to generate the COM class, as well as the correct manifest. When this parameter is enabled, in addition to the main library, a library with the comhost prefix is ​​created, which we register in the system for work. The second parameter that is needed for proper assembly is CopyLocalLockFileAssemblies, so all dependencies will be in the output directory and the library can easily access them. For example, the System.Drawing.Common package is used to display a picture of a tile, and at first I could not understand why I had an empty square displayed instead of an image.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net6.0-windows</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>annotations</Nullable>
    <EnableComHosting>true</EnableComHosting>
    <PlatformTarget>x64</PlatformTarget>
    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
    <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
  </PropertyGroup>

// ...

</Project>

There are not so many methods that we are interested in changing the configuration, one of them is the Initialize method, it is responsible for passing the list of fields that are displayed on the screen. If we want to add a field, we must use the AddField method, all that is needed is to specify a couple of parameters, by default Windows gives us 6 kinds of field types, which include text field, password, label, etc. By default, it is necessary to list the fields for displaying login input, password, password confirmation, label and icon image for the provider, but this list can be changed.

protected override CredentialView Initialize(_CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus, uint dwFlags)
{
	var flags = (CredentialFlag)dwFlags;

	Logger.Write($"cpus: {cpus}; dwFlags: {flags}");

	var isSupported = IsSupportedScenario(cpus);
            
	if (!isSupported)
	{
		if (NotActive == null) NotActive = new CredentialView(this) { Active = false };
			return NotActive;
	}

	var view = new CredentialView(this) { Active = true };
	var userNameState = (cpus == _CREDENTIAL_PROVIDER_USAGE_SCENARIO.CPUS_CREDUI) ?
		_CREDENTIAL_PROVIDER_FIELD_STATE.CPFS_DISPLAY_IN_SELECTED_TILE : _CREDENTIAL_PROVIDER_FIELD_STATE.CPFS_HIDDEN;
	var confirmPasswordState = (cpus == _CREDENTIAL_PROVIDER_USAGE_SCENARIO.CPUS_CHANGE_PASSWORD) ?
		_CREDENTIAL_PROVIDER_FIELD_STATE.CPFS_DISPLAY_IN_BOTH : _CREDENTIAL_PROVIDER_FIELD_STATE.CPFS_HIDDEN;

						view.AddField(
                cpft: _CREDENTIAL_PROVIDER_FIELD_TYPE.CPFT_TILE_IMAGE,
                pszLabel: "Icon",
                state: _CREDENTIAL_PROVIDER_FIELD_STATE.CPFS_DISPLAY_IN_BOTH,
                guidFieldType: Guid.Parse(CredentialView.CPFG_CREDENTIAL_PROVIDER_LOGO)
            );

            view.AddField(
                cpft: _CREDENTIAL_PROVIDER_FIELD_TYPE.CPFT_EDIT_TEXT,
                pszLabel: "Username",
                state: userNameState
            );

            view.AddField(
                cpft: _CREDENTIAL_PROVIDER_FIELD_TYPE.CPFT_PASSWORD_TEXT,
                pszLabel: "Password",
                state: _CREDENTIAL_PROVIDER_FIELD_STATE.CPFS_DISPLAY_IN_SELECTED_TILE,
                guidFieldType: Guid.Parse(CredentialView.CPFG_LOGON_PASSWORD_GUID)
            );

            view.AddField(
                cpft: _CREDENTIAL_PROVIDER_FIELD_TYPE.CPFT_PASSWORD_TEXT,
                pszLabel: "Confirm password",
                state: confirmPasswordState,
                guidFieldType: Guid.Parse(CredentialView.CPFG_LOGON_PASSWORD_GUID)
            );

            view.AddField(
                cpft: _CREDENTIAL_PROVIDER_FIELD_TYPE.CPFT_LARGE_TEXT,
                pszLabel: "Custom Provider",
                defaultValue: "Custom Provider",
                state: _CREDENTIAL_PROVIDER_FIELD_STATE.CPFS_DISPLAY_IN_DESELECTED_TILE
            );

		return view;
	}

The next method we are interested in is IsSupportedScenario, which lists the use cases for our provider. In it, we can, for example, allow using it for authorization, but prohibit using it to unlock the workstation or not using it to change the password, such a case is relevant, for example, for authorization via smart cards.

private static bool IsSupportedScenario(_CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus)
{
	switch (cpus)
  {
  	case _CREDENTIAL_PROVIDER_USAGE_SCENARIO.CPUS_CREDUI:
    case _CREDENTIAL_PROVIDER_USAGE_SCENARIO.CPUS_UNLOCK_WORKSTATION:
    case _CREDENTIAL_PROVIDER_USAGE_SCENARIO.CPUS_LOGON:
    case _CREDENTIAL_PROVIDER_USAGE_SCENARIO.CPUS_CHANGE_PASSWORD:
    	return true;
                
    case _CREDENTIAL_PROVIDER_USAGE_SCENARIO.CPUS_PLAP:
    case _CREDENTIAL_PROVIDER_USAGE_SCENARIO.CPUS_INVALID:
    default:
    	return false;
	}
}

And the last interesting method that responds directly when sending authentication credentials. This method should return an indication of the success or failure of the credential serialization attempt, as well as a pointer to the credential. Steve’s example uses the method from the CredPackAuthenticationBuffer library from the credui.dll library, with its help we can implement standard authorization through the credentials of a local or domain user. However, there is a problem with the implementation of this method, in Windows 10 the authorization and unlock scripts were combined into a single CPUS_LOGON script, which this method successfully processes, but according to the documentation, in some cases the CPUS_UNLOCK_WORKSTATION script is used, with which this method does not work. When I tried to test the provider written by Steve, I ran into this problem, when I tried to unlock the workstation, there was a problem, but when I used the Switch User button, the authorization was successful. To solve this problem, you need to refer to the example provided directly by the company Microsoft and in the implementation of the authorization method, you can find an interesting comment, where the Microsoft programmers themselves indicate that the standard function is not suitable for successful login. It is more likely that the Windows developers themselves use a crutch for authorization.

HRESULT KerbInteractiveUnlockLogonInit(
    _In_ PWSTR pwzDomain,
    _In_ PWSTR pwzUsername,
    _In_ PWSTR pwzPassword,
    _In_ CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,
    _Out_ KERB_INTERACTIVE_UNLOCK_LOGON *pkiul
    )
{
    // Note: this method uses custom logic to pack a KERB_INTERACTIVE_UNLOCK_LOGON with a
    // serialized credential.  We could replace the calls to UnicodeStringInitWithString
    // and KerbInteractiveUnlockLogonPack with a single cal to CredPackAuthenticationBuffer,
    // but that API has a drawback: it returns a KERB_INTERACTIVE_UNLOCK_LOGON whose
    // MessageType is always KerbInteractiveLogon.
    //
    // If we only handled CPUS_LOGON, this drawback would not be a problem.  For
    // CPUS_UNLOCK_WORKSTATION, we could cast the output buffer of CredPackAuthenticationBuffer
    // to KERB_INTERACTIVE_UNLOCK_LOGON and modify the MessageType to KerbWorkstationUnlockLogon,
    // but such a cast would be unsupported -- the output format of CredPackAuthenticationBuffer
    // is not officially documented.
}

Since the method we need is not in the standard libraries, to solve the problem, we need to write a small C ++ library, into which we need to copy the functions for packing credentials, namely KerbInteractiveUnlockLogonPack and KerbInteractiveUnlockLogonInit. Next, we can call the desired function through the use of PInvoke and the problem with the unsupported script will be solved. Interestingly, the main difference is the setting of the serialization message type, namely the authorization script, for the ntsecapi library, since it uses different approaches for serializing credentials.

switch (cpus)
{
	case CPUS_UNLOCK_WORKSTATION:
  	pkil->MessageType = KerbWorkstationUnlockLogon;
    hr = S_OK;
    break;

  case CPUS_LOGON:
  	pkil->MessageType = KerbInteractiveLogon;
    hr = S_OK;
    break;

  case CPUS_CREDUI:
  	pkil->MessageType = (KERB_LOGON_SUBMIT_TYPE)0; // MessageType does not apply to CredUI
    hr = S_OK;
    break;

  default:
  	hr = E_FAIL;
    break;
}

After compiling our small C++ library, it is enough to include it in the main Credential Provider library

[DllImport("./OTP.Provider.Helper.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern uint ProtectIfNecessaryAndCopyPassword(
	string pwzPassword,
  _CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,
  ref string ppwzProtectedPassword
);

[DllImport("./OTP.Provider.Helper.dll", CharSet = CharSet.Unicode, SetLastError = true)]
	public static extern uint KerbInteractiveUnlockLogonInit(
  	string pwzDomain,
    string pwzUsername,
    string pwzPassword,
    _CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,
    ref IntPtr prgb,
    ref int pcb
);

And call it already in the GetSerialization method

try
{
  PInvoke.ProtectIfNecessaryAndCopyPassword(password, usage, ref protectedPassword); 
}
catch (Exception ex)
{
	Logger.Write(ex.Message);
}

var inCredSize = 0;
var inCredBuffer = Marshal.AllocCoTaskMem(0);
try
{
	Marshal.FreeCoTaskMem(inCredBuffer);
  inCredBuffer = Marshal.AllocCoTaskMem(inCredSize);
  PInvoke.KerbInteractiveUnlockLogonInit(domain, shortUsername, protectedPassword, usage, ref inCredBuffer, ref inCredSize);

	pcpcs.clsidCredentialProvider = Guid.Parse("D26F523C-A346-4FC8-B9B4-2B57EAEDA723");
  pcpcs.rgbSerialization = inCredBuffer;
  pcpcs.cbSerialization = (uint)inCredSize;
  pcpcs.ulAuthenticationPackage = authPackage;
  pcpgsr = _CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE.CPGSR_RETURN_CREDENTIAL_FINISHED;

	return HRESULT.S_OK;

}
catch (Exception ex)
{
  Logger.Write(ex.Message);
}

After we have compiled the library, in order to use it, you need to add a key to the registry. In branch [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\] we create a branch with the GUID of our library, which we registered in the CredentialProvider class, then during authorization a tile with the choice of your provider will appear. It is also necessary to register the CLSID of our provider in the HKEY_CLASSES_ROOT branch.

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{D26F523C-A346-4FC8-B9B4-2B57EAEDA723}] 
@="OTP.Provider"
[HKEY_CLASSES_ROOT\OTP.Provider\CLSID]
@="{D26F523C-A346-4FC8-B9B4-2B57EAEDA723}"
Display custom provider tile
Display custom provider tile

Experiments and debugging are recommended to be done in a virtual machine, otherwise you may lose the possibility of authorization in the system and you will have to enter safe mode in order to remove the library and its mention from the registry. The source code can be found at github.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *