Win32: Как проверить учетные данные в Active Directory?
Он был спросил, а ответил для .NET, но теперь пришло время получить ответ для собственного кода Win32:
Как проверить имя пользователя и пароль Windows?
i задал этот вопрос раньше для управляемого кода. Теперь пришло время для собственного решения.
Нужно указать на подводные камни с некоторыми из наиболее распространенных решений:
Недействительный метод 1. Запрос Active Directory с олицетворением
Многие люди предлагают запрашивать Active Directory. Если выбрано исключение, то вы знаете, что учетные данные недействительны - как это предлагается в этом вопросе о стеке_потока.
Есть некоторые серьезные недостатки этого подхода:
-
Вы не только аутентифицируете учетную запись домена, но также выполняете неявную проверку авторизации. То есть вы читаете свойства из AD, используя маркер олицетворения. Что делать, если в противном случае действительная учетная запись не имеет права читать из AD? По умолчанию все пользователи имеют доступ на чтение, но для политик домена можно отключить разрешения доступа для ограниченных учетных записей (или групп).
-
Привязка к AD имеет серьезные накладные расходы, кеш AD-схемы должен быть загружен на клиенте (кеш ADSI в поставщике ADSI, используемом DirectoryServices). Это как сетевой, так и серверы AD, потребляющие ресурсы - и слишком дороги для простой операции, такой как аутентификация учетной записи пользователя.
-
Вы полагаетесь на отказ исключения для случая, отличного от исключительных, и считаете, что это означает неправильное имя пользователя и пароль. Другие проблемы (например, сбой в сети, отказ подключения AD, ошибка выделения памяти и т.д.) Затем неправильно интерпретируются как сбой аутентификации.
Использование класса DirectoryEntry
- это пример неправильного способа проверки учетных данных:
Недействительный метод 1a -.NET
DirectoryEntry entry = new DirectoryEntry("persuis", "iboyd", "Tr0ub4dor&3");
object nativeObject = entry.NativeObject;
Недействительный метод 1b -.NET # 2
public static Boolean CheckADUserCredentials(String accountName, String password, String domain)
{
Boolean result;
using (DirectoryEntry entry = new DirectoryEntry("LDAP://" + domain, accountName, password))
{
using (DirectorySearcher searcher = new DirectorySearcher(entry))
{
String filter = String.Format("(&(objectCategory=user)(sAMAccountName={0}))", accountName);
searcher.Filter = filter;
try
{
SearchResult adsSearchResult = searcher.FindOne();
result = true;
}
catch (DirectoryServicesCOMException ex)
{
const int SEC_E_LOGON_DENIED = -2146893044; //0x8009030C;
if (ex.ExtendedError == SEC_E_LOGON_DENIED)
{
// Failed to authenticate.
result = false;
}
else
{
throw;
}
}
}
}
Как и запрос Active Directory через соединение ADO:
Недействительный метод 1c - Исходный запрос
connectionString = "Provider=ADsDSOObject;
User ID=iboyd;Password=Tr0ub4dor&3;
Encrypt Password=True;Mode=Read;
Bind Flags=0;ADSI Flag=-2147483648';"
SELECT userAccountControl
FROM 'LDAP://persuis/DC=stackoverflow,DC=com'
WHERE objectClass='user' and sAMAccountName = 'iboyd'
Оба эти пользователя не работают, даже когда ваши учетные данные действительны, но у вас нет разрешения на просмотр записи в каталоге:
![enter image description here]()
Недействительный метод 2. LogonUser Win32 API
Другие предложили использовать LogonUser() API. Это звучит неплохо, но, к сожалению, вызывающему пользователю иногда требуется разрешение, доступное только для самой операционной системы:
Для вызова процесса LogonUser требуется привилегия SE_TCB_NAME. Если вызывающий процесс не имеет этого привилегии, LogonUser выходит из строя и GetLastError возвращает ERROR_PRIVILEGE_NOT_HELD.
В некоторых случаях, процесс, который вызывает LogonUser также должен иметь SE_CHANGE_NOTIFY_NAME привилегия включен; в противном случае LogonUser не работает и GetLastError возвращается ERROR_ACCESS_DENIED. Эта привилегия не требуется для локальной системы учетные записи или учетные записи, которые являются членами группы администраторов. От по умолчанию, SE_CHANGE_NOTIFY_NAME для всех пользователей, но некоторые администраторы могут отключить его для каждый.
Отправка "Акта как части операционной системы" - это не то, что вы хотите делать волей-неволей - как указывает Microsoft в статья базы знаний:
... процесс, вызывающий LogonUser должен иметь SE_TCB_NAME привилегии (в User Manager это "Закон как часть действующего Система" справа). SE_TCB_NAME привилегия очень сильна и не должны предоставляться произвольным пользователям, чтобы они могли запустите приложение, которое должно проверять учетные данные.
Кроме того, вызов LogonUser() завершится с ошибкой, если указан пустой пароль.
Действительный метод .NET 3.5 - PrincipalContext
Существует метод проверки, доступный только в .NET 3.5 и новее, который позволяет проверять подлинность пользователя без проверки авторизации:
// create a "principal context" - e.g. your domain (could be machine, too)
using(PrincipalContext pc = new PrincipalContext(ContextType.Domain, "stackoverflow.com"))
{
// validate the credentials
bool isValid = pc.ValidateCredentials("iboyd", "Tr0ub4dor&3")
}
К сожалению, этот код доступен только в .NET 3.5 и более поздних версиях.
Пришло время найти родной эквивалент.
Ответы
Ответ 1
Вот рекомендация Microsoft.
Что касается других ответов, я не совсем уверен, почему вы их снимаете. Вы жалуетесь на неудачи (относительно крайнего случая) при попытке проверить учетные данные, но если вы собираетесь что-то делать с этими учетными данными, тогда эта операция просто сработает. Если вы не собираетесь делать что-то с этими учетными данными, то зачем вам сначала их проверять? Это похоже на несколько надуманную ситуацию, но, очевидно, я не знаю, чего вы пытаетесь выполнить.
Ответ 2
Для собственного эквивалента вашего действительного .NET-решения см. this страница MSDN и ldap_bind
Howerver Я считаю, что LogonUser
является правильным API для задачи при использовании с LOGON32_LOGON_NETWORK
. Обратите внимание, что ограничение SE_CHANGE_NOTIFY_NAME
предназначено только для Windows 2000 (так что Windows XP и новее не требуют этой привилегии), и что по умолчанию SE_CHANGE_NOTIFY_NAME разрешен для всех пользователей. Также на странице MSDN
Для этой функции привилегия SE_TCB_NAME не требуется, если вы не входите в учетную запись Passport.
В этом случае вы регистрируетесь на учетной записи AD, поэтому SE_TCB_NAME не требуется.
Ответ 3
Я мог бы также опубликовать собственный код для проверки набора учетных данных Windows. Это потребовалось некоторое время для реализации.
function TSSPLogon.LogonUser(username, password, domain: string; packageName: string='Negotiate'): HRESULT;
var
ss: SECURITY_STATUS;
packageInfo: PSecPkgInfoA;
cbMaxToken: DWORD;
clientBuf: PByte;
serverBuf: PByte;
authIdentity: SEC_WINNT_AUTH_IDENTITY;
cbOut, cbIn: DWORD;
asClient: AUTH_SEQ;
asServer: AUTH_SEQ;
Done: boolean;
begin
{
If domain is blank will use the current domain.
To force validation against the local database use domain "."
sspiProviderName is the same of the Security Support Provider Package to use. Some possible choices are:
- Negotiate (Preferred)
Introduced in Windows 2000 (secur32.dll)
Selects Kerberos and if not available, NTLM protocol.
Negotiate SSP provides single sign-on capability called as Integrated Windows Authentication.
On Windows 7 and later, NEGOExts is introduced which negotiates the use of installed
custom SSPs which are supported on the client and server for authentication.
- Kerberos
Introduced in Windows 2000 and updated in Windows Vista to support AES) (secur32.dll)
Preferred for mutual client-server domain authentication in Windows 2000 and later.
- NTLM
Introduced in Windows NT 3.51 (Msv1_0.dll)
Provides NTLM challenge/response authentication for client-server domains prior to
Windows 2000 and for non-domain authentication (SMB/CIFS)
- Digest
Introduced in Windows XP (wdigest.dll)
Provides challenge/response based HTTP and SASL authentication between Windows and non-Windows systems where Kerberos is not available
- CredSSP
Introduced in Windows Vista and available on Windows XP SP3 (credssp.dll)
Provides SSO and Network Level Authentication for Remote Desktop Services
- Schannel
Introduced in Windows 2000 and updated in Windows Vista to support stronger AES encryption and ECC (schannel.dll)
Microsoft implementation of TLS/SSL
Public key cryptography SSP that provides encryption and secure communication for
authenticating clients and servers over the internet. Updated in Windows 7 to support TLS 1.2.
If returns false, you can call GetLastError to get the reason for the failure
}
// Get the maximum authentication token size for this package
ss := sspi.QuerySecurityPackageInfoA(PAnsiChar(packageName), packageInfo);
if ss <> SEC_E_OK then
begin
RaiseWin32Error('QuerySecurityPackageInfo "'+PackageName+'" failed', ss);
Result := ss;
Exit;
end;
try
cbMaxToken := packageInfo.cbMaxToken;
finally
FreeContextBuffer(packageInfo);
end;
// Initialize authorization identity structure
ZeroMemory(@authIdentity, SizeOf(authIdentity));
if Length(domain) > 0 then
begin
authIdentity.Domain := PChar(Domain);
authIdentity.DomainLength := Length(domain);
end;
if Length(userName) > 0 then
begin
authIdentity.User := PChar(UserName);
authIdentity.UserLength := Length(UserName);
end;
if Length(Password) > 0 then
begin
authIdentity.Password := PChar(Password);
authIdentity.PasswordLength := Length(Password);
end;
AuthIdentity.Flags := SEC_WINNT_AUTH_IDENTITY_ANSI; //SEC_WINNT_AUTH_IDENTITY_UNICODE
ZeroMemory(@asClient, SizeOf(asClient));
ZeroMemory(@asServer, SizeOf(asServer));
//Allocate buffers for client and server messages
GetMem(clientBuf, cbMaxToken);
GetMem(serverBuf, cbMaxToken);
try
done := False;
try
// Prepare client message (negotiate)
cbOut := cbMaxToken;
ss := Self.GenClientContext(@asClient, authIdentity, packageName, nil, 0, clientBuf, cbOut, done);
if ss < 0 then
begin
RaiseWin32Error('Error generating client context for negotiate', ss);
Result := ss;
Exit;
end;
// Prepare server message (challenge).
cbIn := cbOut;
cbOut := cbMaxToken;
ss := Self.GenServerContext(@asServer, packageName, clientBuf, cbIn, serverBuf, cbOut, done);
if ss < 0 then
begin
{
Most likely failure: AcceptServerContext fails with SEC_E_LOGON_DENIED in the case of bad username or password.
Unexpected Result: Logon will succeed if you pass in a bad username and the guest account is enabled in the specified domain.
}
RaiseWin32Error('Error generating server message for challenge', ss);
Result := ss;
Exit;
end;
// Prepare client message (authenticate).
cbIn := cbOut;
cbOut := cbMaxToken;
ss := Self.GenClientContext(@asClient, authIdentity, packageName, serverBuf, cbIn, clientBuf, cbOut, done);
if ss < 0 then
begin
RaiseWin32Error('Error generating client client for authenticate', ss);
Result := ss;
Exit;
end;
// Prepare server message (authentication).
cbIn := cbOut;
cbOut := cbMaxToken;
ss := Self.GenServerContext(@asServer, packageName, clientBuf, cbIn, serverBuf, cbOut, done);
if ss < 0 then
begin
RaiseWin32Error('Error generating server message for authentication', ss);
Result := ss;
Exit;
end;
finally
//Free resources in client message
if asClient.fHaveCtxtHandle then
sspi.DeleteSecurityContext(@asClient.hctxt);
if asClient.fHaveCredHandle then
sspi.FreeCredentialHandle(@asClient.hcred);
//Free resources in server message
if asServer.fHaveCtxtHandle then
sspi.DeleteSecurityContext(@asServer.hctxt);
if asServer.fHaveCredHandle then
sspi.FreeCredentialHandle(@asServer.hcred);
end;
finally
FreeMem(clientBuf);
FreeMem(serverBuf);
end;
Result := S_OK;
end;
Примечание. Любой код, выпущенный в общественное достояние. Не требуется атрибуция.
Ответ 4
Существует функция win32 API, называемая ldap_bind_s. Функция ldap_bind_s выполняет аутентификацию клиента
против LDAP. Дополнительную информацию см. В документации MSDN.
Ответ 5
Я аутентифицировал пользователя, используя имя пользователя и пароль:
имя пользователя - это значение атрибута пользователя sn на сервере Ldap, например U12345
userDN - это пользователь DistinguishedName в LdapServer
public bool AuthenticateUser(string username, string password)
{
try
{
var ldapServerNameAndPort = "Servername:389";
var userDN = string.Format("CN=0},OU=Users,OU=MyOU,DC=MyDC,DC=com",username);
var conn = new LdapConnection(ldapServerNameAndPort)
{
AuthType = AuthType.Basic
};
conn.Bind(new NetworkCredential(userDN , password));
return true;
}
catch (Exception e)
{
return false;
}
}