对自定义凭据提供程序进行编程

本主题介绍如何为 Windows 调试器编程自定义凭据提供程序。 这允许使用需要唯一身份验证类型的附加符号和源服务器。 自定义允许使用服务器可能需要的任何类型的身份验证。

有两个选项:

  • 通过可执行文件(或通过 CMD/BAT 脚本启动)实现的自定义提供程序。
  • 以 DLL 形式实现的自定义提供程序,使用本文档中描述的 API 接口。

Windows 调试器 HTTPS 身份验证请求

Windows 调试器从符号服务器请求符号,如果符号服务器不需要身份验证,则返回符号而不使用任何凭据提供程序。 如果符号服务器返回HTTP_STATUS_DENIED 401(未授权访问/拒绝访问)状态代码,则这向调试器指示身份验证是必需的。 401 未经授权的代码指示请求缺少目标资源的有效身份验证凭据。 这意味着服务器拒绝满足请求,因为客户端未提供所需的身份验证信息。

如果配置了自定义凭证提供程序,则将使用它并且它返回的凭证将用于重新发送失败的 401 错误请求。 本主题的下一部分介绍了如何配置自定义凭据提供程序。

自定义凭据提供程序的 XML 配置

两个 XML 文件用于配置自定义凭据提供程序,一个指示配置文件位置,另一个文件包含自定义凭据提供程序的配置信息。

当返回 401 未授权时,调试器会调用 DbgCredentialProvider.dll。 该 DLL 将使用以下过程查找凭据提供程序。 它将打开一个文件 DbgCredentialProvider.config.xml,该文件应与提供配置 XML 文件的文件夹位置 DbgCredentialProvider.dll 位于同一目录中。

请注意,用于查找 DbgCredentialProvider.config.xml 文件的搜索行为将来可能会更改。

XML 配置文件位置 - DbgCredentialProvider.config.xml

配置 XML 文件安装在 DbgCredentialProvider.config.xml中指定的文件夹位置。

<?xml version="1.0" encoding="utf-8"?>
<!--
The config file is located next to the DbgCredentialProvider.dll.
-->
<Settings>
    <Folders>
        <!--
        This is a list of the folders which should be provided as an absolute file path or
        relative to the location of this config file.
        -->
        <Folder>CredentialProviders</Folder>
    </Folders>
</Settings>

可以在 <Folders></Folders> 元素下列出多个文件夹。 文件夹可以是相对路径或绝对路径。 如果是相对路径,则是相对于DbgCredentialProvider.config.xml文件的位置。 文件夹将按列出的顺序被搜索以查找提供程序。

在上面所示的当前示例中,Folders 集合只有一个文件夹“CredentialProviders”,它是一个相对路径。

自定义凭据提供程序的 XML 配置信息

找到指定的文件夹位置后,将枚举 CredentialProviders 文件夹中扩展名为“*.xml”的所有文件。 XML 文件描述了哪些调试器凭据提供程序可用于调试器。 CredentialProviders XML 文件中介绍了凭据提供程序(在 DLL、EXE 或 CMD/BAT 脚本中实现)位置。 提供程序可能是相对路径或绝对路径。

支持多个自定义凭证提供商。 调试器将请求每个提供程序提供凭据,它将使用返回成功的第一个提供程序中的凭据。

未指定 XML 文件枚举的顺序。

示例 DbgCredentialProvider_gcmw.xml 文件显示了如何调用批处理文件。

<?xml version="1.0" encoding="utf-8"?>
<CredentialProviders>
    <!--
          This is a list of the provider modules which should be provided as an absolute file path or
          relative to the location of this config file.
          The provider is a DLL, EXE or CMD file.
    -->
    <CredentialProvider>PATCredentialProvider\PATCredentialProvider.bat</CredentialProvider>

</CredentialProviders>

此示例 XML 演示如何调用 dll。

<?xml version="1.0" encoding="utf-8"?>
<CredentialProviders>
    <!--
      This is a list of the provider modules which should be provided as an absolute file path or
      relative to the location of this config file.
      The provider is a DLL, EXE or CMD file.
    -->
   <CredentialProvider>GCMW\DbgCredentialProvider_gcmw.dll</CredentialProvider>

</CredentialProviders>

在此示例中,我们只有一个提供程序 DbgCredentialProvider_gcmw.dll,它位于相对于 DbgCredentialProvider_gcmw.xml 文件位置的 GCMW 文件夹中。

使用命令行调用自定义凭据提供程序

本部分介绍如何将自定义凭据提供程序实现为 EXE(或通过 CMD/BAT 文件命令脚本启动)。

如果提供程序是在 EXE 或 CMD 脚本中实现的,则应能够处理以下命令行参数(不区分大小写),这不能相互组合。

  • 获取
  • Erase
  • 存储

获取命令

Get 命令用于检索凭据。 其余数据将通过标准输入流传递到提供程序。

其他输入数据将通过标准输入流传递给提供程序,后跟一个空行来标记输入参数的末尾。

参数不区分大小写,可以使用大写和小写的任意组合。

错误可能返回通过error=zzzzz

Protocol=http or https
Host=xxx 
Path=yyy
ResourceKind=symbols or sources
Interactive=0 or 1
IsRetry=0 or 1
ParentHwnd=HWND
<empty line to mark the end of the input parameters>

获取参数

字段 类型 描述
协议 LPCWSTR HTTP 或 HTTPS。 为了提高安全性,强烈建议使用 HTTPS。
主机 LPCWSTR 主机服务器的名称,例如 contoso.symbols.com
路径 LPCWSTR 符号目录的路径,例如apis/symbol/symsrv。 调用方/调试器将确保 Path 永远不会以“/”字符开头
ResourceKind LPCWSTR 它可以是“符号”或“源”。 将来可能会添加其他资源类型。 凭据提供程序实现可用于在获取凭据时调整所需的权限。 它还可用于缓存凭据以供将来使用。
交互 布尔 true - 可以显示 UI,false - 无 UI。
IsRetry 布尔 如果为 true,提供程序必须跳过读取缓存并获取新凭据
ParentHwnd HWND 如果显示身份验证 UI,则为父 HWND,例如 0x%I64x。 应用程序可以使用 DBG_CREDENTIAL_PROVIDER_PARENT_HWND 环境变量或 imagehlp/dbghelp SymSetParentWindow 方法来设置父 HWND。

完整的 URI/URL 是通过使用列出的参数连接<protocol>://<host>/<path>来构建的。 例如:https://contoso.symbols.com/apis/symbol/symsrv。 请求如下所示:

protocol=https
host=contoso.symbols.com
path=apis/symbol/symsrv
resourceKind=symbols
isretry=false 
issilent=false
parenthwnd=593598
<Followed by an empty line to indicate the end of the input data.>

Erase

凭据提供程序可以选择使用此命令从其缓存中清除凭据。 输入参数与 Get 命令相同。 不需要输出返回值。 可能会返回错误。

存储

凭据提供程序可以选择使用此命令将凭据存储在其缓存中。 输入参数与 Get 命令相同。 不需要输出返回值。 可能会返回错误。

本地令牌缓存和 isRetry

对于对提供程序的第一个请求,调试器发送参数 isRetry=false。 某些提供程序可能会从其本地缓存中获取令牌。 调试器使用此令牌重新发送 HTTP 请求后,服务器可能会再次返回 401 响应。 这可能是因为令牌已过期。 然后,调试器将向凭证提供者请求一个新的令牌,这次 isRetry=true。 在这种情况下,提供程序不应使用其缓存,而是检索全新的令牌。

交互式设置 - 身份验证 UI

在某些非交互式环境中,例如测试实验室,可能没有用户与 UI 交互。 在这种情况下,参数 issilent 将为真。 如果此参数为 true,则提供程序不应显示任何身份验证或其他 UI。

测试实验室或应用程序中的脚本可以使用以下选项来控制交互式标志。

设置静默(非交互式)符号服务器

使用 SymbolServerSetOptions 函数 设置无提示(非交互式)符号服务器。 如果 SSRVOPT_UNATTENDED 设置为 TRUE,SymSrv 将不会显示对话框或弹出窗口。 如果数据为 FALSE,SymSrv 将在建立连接时显示这些图形功能。

管理 UI Windows

某些凭据提供程序可能会显示身份验证 UI。 如果是这样,它应使用“ParentHwnd”参数,以便该UI以模态对话框的形式显示在主调试器窗口中。 否则,身份验证 UI 可能隐藏在主调试器窗口后面,用户可能会误认为调试器已“卡住”。

类似于 WinDbg 的调试器客户端应用程序可以使用 DBG_CREDENTIAL_PROVIDER_PARENT_HWND 环境变量或 imagehlp/dbghelp SymSetParentWindow 方法来设置父 HWND。 您也可以使用IDebugAdvanced2::Request 信息DEBUG_REQUEST_SET_PARENT_HWND,将 HWND 值转换为 UINT32。

退回值要求

EXE 或 CMD/BAT 脚本必须通过输出流返回用户名和密码,如下所示:

username=aaa
password=bbb - where the password can be a password or PAT

CredentialKind

身份验证请求返回 CredentialKind。 CredentialKind 有两个可供选择的选项。

  • 基本身份验证:RFC 7617 中定义。 凭据作为用户ID/密码对,通过Base64编码进行传输。
username=xxx
credentialkind=Basic
password=yyy --> This can be a password or a PAT token
  • 持有者身份验证:在 RFC 6750 中定义。 持有者令牌用于 HTTP 请求来访问受 OAuth 2.0 保护的资源。
username=xxx
credentialkind=Bearer
header=Bearer <TOKEN_GOES_HERE> ---> Usually OAuth2 tokens begin with "ey" and it is a very long string

CMD 文件示例

下面是返回 HTTP 身份验证标头的 CMD 文件的示例:

CredentialProviders 文件夹中的 OAuth2CredentialProvider.xml 文件:

<?xml version="1.0" encoding="utf-8"?>
<CredentialProviders>
    <CredentialProvider ApiVersion="2.0.0" >OAuth2CredentialProvider\OAuth2CredentialProvider.cmd</CredentialProvider>
</CredentialProviders>	

OAuth2CredentialProvider.cmd位于 OAuth2CredentialProvider 文件夹中的文件:

@echo off
echo username=UserName@domain.com
echo header=Bearer <TOKEN_GOES_HERE>

测试自定义提供程序

如果要编写位于 CMD 或 EXE 文件中的自定义提供程序,只需使用命令从控制台窗口启动它即可对其进行测试。 例如:

DebuggerCredentialManager.exe Get

这会启动应用程序并打印类似内容,然后等待用户输入(空行指示用户输入的结束)。

[Information] [DebuggerCredentialProvider.102949]Microsoft Debugger Credential Manager version 2024.0409.02656.285 (Windows, .NET 6.0.29) 'get'

这是您在控制台窗口输入流中输入的信息示例。 可以以大写和小写的任意组合输入。

protocol=https
host=contoso.symbols.com
path=apis/symbol/symsrv
resourceKind=symbols
isretry=false 
issilent=false
parenthwnd=593598

然后按 Enter 键两次发送空白行并指示用户输入的结束。

提供程序通过标准输出流进行响应。

[Verbose] [DebuggerCredentialProvider.103258]AzureCredentialProvider - Attempting to acquire bearer token using provider 'Msal Cache'
[Verbose] [DebuggerCredentialProvider.103300]Token expiration data - current UTC time:9/18/2024 5:33:00 PM, ExpiresOn: 9/18/2024 6:43:25 PM
[Information] [DebuggerCredentialProvider.103300]AzureCredentialProvider - Acquired bearer token using 'Msal Cache'
protocol=https
host=contoso.symbols.com
path=apis/symbol/symsrv
username=UserName@domain.com
credentialkind=Bearer
header=Bearer eyJ0eXAi....
<empty line>

调试器将忽略任何与模式 key=value 不匹配的行,其中的键为以下其中之一:协议、主机、路径、用户名、凭证类型或标头。

键值对中忽略大小写。 调试器将空白行视为输入的末尾。

提供程序诊断信息

提供商可以选择在输出流上打印诊断信息。 调试器将忽略它,也不会向用户显示这些内容。 此处显示的额外信息示例仅用于说明目的。 其他提供程序可能会打印其他诊断信息,或者不打印任何信息。

返回 PAT 令牌的 PowerShell 示例脚本

下面是返回 PAT 令牌的 PS 脚本示例。

文件 PatCredentialProvider.xml 将 PATCredentialProvider.bat 配置为 CredentialProvider。

<?xml version="1.0" encoding="utf-8"?>
<CredentialProviders>
     <CredentialProvider ApiVersion="2.0.0">PATCredentialProvider\PATCredentialProvider.bat</CredentialProvider>
</CredentialProviders>

文件 PATCredentialProvider.bat 位于 PATCredentialProvider 文件夹中,并调用 PATCredentialProvider.ps1。

@echo off
<PATH_TO_POWERSHELL>\PowerShell.exe -NoProfile -executionpolicy Unrestricted -WindowStyle Hidden -File "%~dp0\PATCredentialProvider.ps1"

PATCredentialProvider.ps1 也位于 PATCredentialProvider 文件夹中。

<#
 .SYNOPSIS
    Given input, parses to find out which symbol server we want credentials for, and searches the Microsoft Credential Manager for those credentials.
    If found, prints the credentials to standard output. If not, prints error.

 .INPUT
    Delivered through standard input:
    protocol=http or https
    host=xxx ex. host=contoso.symbols.com
    path=yyy ex. path=apis/symbol/symsrv
    resourceKind=symbols
    isretry=false 
    issilent=false
    parenthwnd=593598
    <empty line to mark the end of the input parameters>

 .OUTPUT

    Delivered through standard output:

    username=aaa
    password=bbb - where the password can be a password or PAT. When PAT is returned the username will be any name (not necessarily the name of the currently logged in user)<!--[SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="It's an example")]-->
    <empty line to mark the end of the output parameters>

 #>

 $logDirectory = (Get-Item Env:LoggingDirectory).Value
 $logFile = Join-Path $logDirectory "credProviderLog.txt"

 try
 {
    "Entering Credential Provider" | Out-File $logFile -Append

    $lines = While($line=Read-Host) {$line}
    $lines | Out-File $logFile -Append
    if (!(Get-Module "CredentialManager"))
    {
        "Installing module" | Out-File $logFile -Append

        Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force

        Install-Module CredentialManager -force -Scope CurrentUser
    }

    $pathLine = $lines | Where-Object {$_.StartsWith("path=")} | Select-Object -First 1
    "Found path line: $pathLine" | Out-File $logFile -Append

    [regex]$regex="path=(?<ServerName>.*)"
    $pathLine -Match $regex

    $symbolPath = "symbol:$($Matches.ServerName)"

    "Found symbol path: $symbolPath" | Out-File $logFile -Append

    $PAT = Get-StoredCredential -Target $symbolPath -AsCredentialObject

    if ($PAT)
    {
        "Found PAT!" | Out-File $logFile -Append
        Write-Host "username=placeholder"
        Write-Host "password=$($PAT.Password)"; # For OAuth 2 tokens You can change to output header=Bearer TOKEN
        Write-Host
    }
    else
    {
        "Could not locate PAT for Symbol Server: $symbolPath" | Out-File $logFile -Append
        Write-Host "error=Could not locate PAT for Symbol Server: $symbolPath"
    }
 }
 catch [System.SystemException]
 {
    "ERROR" | Out-File $logFile -Append
 
    $_ | Out-File $logFile -Append
 }

以 DLL 形式实现的凭证提供程序的 C++ API

以下描述了在 DLL 中实现自定义凭据提供程序时,凭据提供程序必须遵循的公共接口。 如果提供程序在 DLL 中实现,则必须导出 GetUserCredentials 方法。 位于namespace Debugger::CredentialProvider::Provider

所需的 DbgCredentialProviderImpl.h 头文件随 Windows SDK 一起发布。 有关下载 SDK 的信息,请参阅 Windows SDKSDK Insider Preview

MessageErrorLevelKind enums

// The caller of this method should not terminate the message with '\r' or '\n' characters
// (which makes this method similar to PrintLine)
typedef void (*DbgPrintMessageFn)(_In_ MessageErrorLevelKind errorLevel, _In_ PCWSTR message);

enum class MessageErrorLevelKind : uint8_t
{
    Info = 0,
    Warning,
    Error
};

CredentialResponseResultKind

enum CredentialResponseResultKind
{
    Success = 0,
    NoProviders, // There are no installed providers or can't launch 3rd party provider
    ProviderNotApplicable, // the provider can't handle requests to the provided URL
    Error
};

struct GetUserCredentialsRequest

结构用于存储 GetUserCredentialsRequest。 它使用与上述 GetUserCredentialsRequest 参数表中所述相同的参数。

struct GetUserCredentialsRequest
{
    LPCWSTR Protocol;      // The full request URL can be built from Protocol, Host and Path as follows:
    LPCWSTR Host;          // Protocol://Host/Path
    LPCWSTR Path;          // The caller/debugger will make sure Path never starts with '/' character
    LPCWSTR ResourceKind;  // It can be "symbols", "sources", etc.
                           // The credential provider implementation may use this to adjust
                           // the required permissions when acquiring credentials. It also may be used
                           // to cache credentials for future use.
    bool Interactive;      // true - display UI is ok, false - no UI.
                           // Refer to the explanations above on how to setup a non interactive environment
    bool IsRetry;          // When true the provider may skip reading the caches and get new credentials
    HWND ParentHwnd;       // The parent HWND if an authentication UI is displayed.
                           // The applications can use DBG_CREDENTIAL_PROVIDER_PARENT_HWND environment variable
                           // or imagehlp/dbghelp SymSetParentWindow method to setup the parent HWND.

    DbgPrintMessageFn PrintMessageFn;
}

GetUserCredentials 函数

GetUserCredentials 函数用于请求凭据,这些凭据将作为获取符号的 HTTP 请求的一部分发送到符号服务器。

HRESULT WINAPI GetUserCredentials(
  _In_ GetUserCredentialsRequest const & request,
  _Inout_ GetUserCredentialsResponse * pResponse);

此方法的调用方(调试器)将提供请求和响应参数。 调用者将确保方法进入时 UserName、Password 和 ErrorMessage 为 nullptr。

实现应填入用户名、密码、错误信息(可选)和结果。

GetUserCredentialsResponse 类

此处显示了 GetUserCredentialsResponse 类。


class GetUserCredentialsResponse final
{
public:

    CredentialResponseResultKind Result = CredentialResponseResultKind::Error;
    BSTR UserName = nullptr;
    BSTR Password = nullptr;
    BSTR HttpAuthenticationHeader = nullptr;
    BSTR ErrorMessage = nullptr;

    GetUserCredentialsResponse() = default;

    GetUserCredentialsResponse(GetUserCredentialsResponse const&) = delete;
    GetUserCredentialsResponse& operator=(GetUserCredentialsResponse const&) = delete;

    GetUserCredentialsResponse(GetUserCredentialsResponse&& other) noexcept
    {
        Result = other.Result;
        UserName = other.UserName;
        Password = other.Password;
        HttpAuthenticationHeader = other.HttpAuthenticationHeader;
        ErrorMessage = other.ErrorMessage;

        other.Release();
    }

    GetUserCredentialsResponse& operator=(GetUserCredentialsResponse&& other) noexcept
    {
        if (this != addressof(other))
        {
            Clear();

            Result = other.Result;
            UserName = other.UserName;
            Password = other.Password;
            HttpAuthenticationHeader = other.HttpAuthenticationHeader;
            ErrorMessage = other.ErrorMessage;

            other.Release();
        }
        return (*this);
    }

    ~GetUserCredentialsResponse()
    {
        Clear();
    }

    void Clear()
    {
        SecureSysFreeString(UserName);
        SecureSysFreeString(Password);
        SecureSysFreeString(HttpAuthenticationHeader);
        SecureSysFreeString(ErrorMessage);
    }

private:
    template <class _Tp>
    _Tp* addressof(_Tp& __x) noexcept
    {
        return reinterpret_cast<_Tp*>(
            const_cast<char*>(&reinterpret_cast<const volatile char&>(__x)));
    }

    void Release() noexcept
    {
        Result = CredentialResponseResultKind::Error;
        UserName = nullptr;
        Password = nullptr;
        ErrorMessage = nullptr;
        HttpAuthenticationHeader = nullptr;
    }

    void SecureSysFreeString(_Inout_ BSTR& bstrString)
    {
        if (bstrString != nullptr)
        {
            size_t const length = wcslen(bstrString);
            SecureZeroMemory(bstrString, length * sizeof(wchar_t));
            SysFreeString(bstrString);
            bstrString = nullptr;
        }
    }
};

另请参阅

符号和符号文件

公共符号和专用符号