Программирование настраиваемых поставщиков учетных данных
В этом разделе описывается, как программировать настраиваемый поставщик учетных данных для отладчика Windows. Это позволяет использовать дополнительные символы и исходные серверы, для которых требуются уникальные типы проверки подлинности. Настройка позволяет использовать любой тип проверки подлинности, которую может потребовать сервер.
Существует два варианта:
- Пользовательский поставщик, реализованный через исполняемый файл (или запущенный с помощью скрипта CMD/BAT).
- Настраиваемый поставщик, реализованный в виде библиотеки DLL с помощью интерфейса API, описанного в этом документе.
Отладчики Windows запрашивают символы с сервера символов и если сервер символов не требует проверки подлинности, эти символы возвращаются без использования каких-либо поставщиков учетных данных. Если сервер символов возвращает код состояния HTTP_STATUS_DENIED 401 (несанкционированный доступ или отказ в доступе), это указывает отладчику, что требуется проверка подлинности. Код 401 «Unauthorized» означает, что в запросе отсутствуют допустимые учетные данные для аутентификации к целевому ресурсу. Это означает, что сервер отказывается выполнить запрос, так как клиент не предоставил необходимые сведения о проверке подлинности.
Если настроен пользовательский поставщик учетных данных, он будет использоваться, и возвращаемые учетные данные будут применены для повторной отправки запроса, завершившегося ошибкой 401. Настройка пользовательского поставщика учетных данных рассматривается в следующем разделе этого раздела.
Для настройки пользовательского поставщика учетных данных используются два XML-файла, которые указывают расположение файла конфигурации и второй файл, содержащий сведения о конфигурации для пользовательского поставщика учетных данных.
Когда возвращается ошибка 401 (Несанкционированный доступ), отладчик вызывает DbgCredentialProvider.dll. Эта библиотека DLL будет искать поставщиков учетных данных с помощью следующего процесса. Он открывает файл DbgCredentialProvider.config.xml, который должен находиться в том же каталоге, что и DbgCredentialProvider.dll, который предоставляет расположение папки 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. Папки будут просматриваться для поиска поставщиков в указанном порядке.
В текущем примере выше коллекция папок содержит только одну папку CredentialProviders, и это относительный путь.
После расположения указанной папки будут перечисляться все файлы с расширением "*.xml" в папке CredentialProviders. XML-файлы описывают, какие поставщики учетных данных отладчика доступны для отладчика. Местоположение поставщиков учетных данных (реализованных в файлах DLL, EXE или сценариях CMD/BAT) описано в XML-файле CredentialProviders. Пути могут быть относительными или абсолютными.
Поддерживаются несколько пользовательских поставщиков учетных данных. Отладчик запрашивает у каждого поставщика учетные данные и будет использовать учетные данные от первого поставщика, который возвращает успех.
Порядок перечисления 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, и он расположен в папке GCMW относительно расположения файла DbgCredentialProvider_gcmw.xml.
В этом разделе описывается, как настраиваемый поставщик учетных данных может быть реализован как EXE (или запущен с помощью скрипта команды CMD/BAT).
Если поставщик реализован в EXE или скрипте CMD, он должен иметь возможность обрабатывать следующие параметры командной строки (без учета регистра), которые нельзя комбинировать друг с другом.
- Получить
- Стереть
- Магазин
Команда 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 . Вызывающий или отладчик гарантирует, что Путь никогда не начинается с символа /. |
ResourceKind | LPCWSTR | Это могут быть "символы" или "источники". Дополнительные типы ресурсов могут быть добавлены в будущем. Реализация поставщика учетных данных может использовать это для настройки необходимых разрешений при получении учетных данных. Он также может использоваться для кэширования учетных данных для дальнейшего использования. |
Интерактивный | bool | True — для отображения пользовательского интерфейса, false — нет пользовательского интерфейса. |
ЭтоПовторнаяПопытка | bool | Если это условие верно, то поставщик должен пропустить чтение кэшей и получить новые учетные данные. |
ParentHwnd | HWND | Родительский HWND, если отображается пользовательский интерфейс проверки подлинности, например, 0x%Iили 64x. Приложения могут использовать переменную среды 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.>
Поставщик учетных данных может использовать эту команду для удаления учетных данных из кэша. Входные параметры совпадают с командой Get. Не требуется возвращаемое значение выходных данных. Может быть возвращена ошибка.
Поставщик учетных данных может использовать эту команду для хранения учетных данных в кэше. Входные параметры совпадают с командой Get. Не требуется возвращаемое значение выходных данных. Ошибка может быть возвращена.
Для самого первого запроса поставщику отладчик отправляет параметр isRetry=false
. Некоторые поставщики могут получать маркер из локального кэша. Когда отладчик повторно отправляет HTTP-запрос с этим маркером, сервер может снова вернуть ответ 401. Это может быть связано с истечением срока действия маркера. Затем отладчик запросит у поставщика учетных данных новый токен, и на этот раз флаг isRetry=true. В таком случае поставщик не должен использовать свой кэш, а должен получить новый токен.
В некоторых неинтерактивных средах, таких как лаборатории тестирования, не может быть пользователя для взаимодействия с пользовательским интерфейсом. В таком случае параметр issilent будет равен true. Поставщик не должен отображать проверку подлинности или другой пользовательский интерфейс, если этот параметр имеет значение true.
Скрипты в лабораториях тестирования или приложениях могут использовать следующие параметры для управления интерактивным флагом.
- Команды "!sym prompts off" или "!sym quiet". Дополнительные сведения см. в !sym подсказки.
- Параметр командной строки sflags WinDbg или cdb/kd (флаг SYMOPT_NO_PROMPTS).
- метод IDebugSymbols::SetSymbolOptions (флаг SYMOPT_NO_PROMPTS, описанный в опциях символов)
- Функция SymSetOptions из imagehlp/dbghelp с флагом SYMOPT_NO_PROMPTS.
Используйте функцию SymbolServerSetOptions для настройки беззвучного (неинтерактивного) сервера символов. Если для SSRVOPT_UNATTENDED
задано значение TRUE, SymSrv не будет отображать диалоговые окна или всплывающие окна. Если значение FALSE, SymSrv будет отображать эти графические элементы при подключении.
Некоторые поставщики учетных данных могут отображать пользовательский интерфейс проверки подлинности. Если это так, он должен использовать параметр ParentHwnd, чтобы этот пользовательский интерфейс отображался как модальное диалоговое окно в главном окне отладчика. В противном случае пользовательский интерфейс проверки подлинности может быть скрыт за основным окном отладчика, и пользователю может быть предоставлено впечатление, что отладчик "заморожен".
Клиентское приложение отладчика, аналогичное WinDbg, может использовать переменную среды DBG_CREDENTIAL_PROVIDER_PARENT_HWND или imagehlp/dbghelp метод SymSetParentWindow для настройки родительского HWND. Вы также можете использовать сообщение IDebugAdvanced2::RequestDEBUG_REQUEST_SET_PARENT_HWND
со значением приведения HWND к UINT32.
Скрипт EXE или CMD/BAT должен возвращать имя пользователя и пароль через выходной поток следующим образом:
username=aaa
password=bbb - where the password can be a password or PAT
Запрос проверки подлинности возвращает CredentialKind. Существует два варианта для CredentialKind.
- Базовая проверка подлинности: определена в RFC 7617. Учетные данные передаются в виде пар идентификатора пользователя и пароля, закодированных с помощью 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:
OAuth2CredentialProvider.xml файл, расположенный в папке CredentialProviders:
<?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
Затем дважды нажмите клавишу ВВОД, чтобы отправить пустую строку и указать на завершение ввода пользователя.
Поставщик отвечает через стандартный выходной поток.
[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
, где ключ — это один из следующих: протокол, узел, путь, имя пользователя, тип учетных данных или заголовок.
Регистр игнорируется в парах значений ключа. Отладчик обрабатывает пустую строку как конец входных данных.
Поставщики могут выбирать печать диагностической информации в выходном потоке. Отладчик не будет игнорировать его, и не будет отображать их пользователю. Примеры дополнительных сведений, показанные здесь, предназначены только для иллюстрации. Другие поставщики могут печатать другие диагностические сведения или ничего не печатать.
Ниже приведен пример скрипта PS для возврата маркера PAT.
Файл 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. Если поставщик реализован в библиотеке DLL, он должен экспортировать метод GetUserCredentials. Они расположены в namespace Debugger::CredentialProvider::Provider
.
Требуемый файл заголовка DbgCredentialProviderImpl.h публикуется с помощью пакета SDK для Windows. Сведения о скачивании пакета SDK см. в SDK для Windows и предварительной версии SDK Insider Preview.
// 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
};
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
};
Структура используется для хранения 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 используется для запроса учетных данных, которые будут отправлены на сервер символов в HTTP-запросе на символы.
HRESULT WINAPI GetUserCredentials(
_In_ GetUserCredentialsRequest const & request,
_Inout_ GetUserCredentialsResponse * pResponse);
Вызывающая сторона (отладчик) этого метода предоставит параметры запроса и ответа. Вызывающий объект гарантирует, что имя пользователя, пароль и сообщение об ошибке равны nullptr при входе в метод.
Реализация обязана заполнить поля: Имя пользователя, Пароль, Сообщение об ошибке (необязательно) и Результат.
Здесь показан класс 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;
}
}
};