windows - Win32: ¿Cómo validar credenciales contra Active Directory?
security winapi (5)
Se ha asked y se ha respondido para .NET , pero ahora es el momento de obtener una respuesta para el código Win32 nativo:
¿Cómo validar un nombre de usuario y contraseña de Windows?
Hice esta pregunta antes para el código administrado . Ahora es el momento de la solución nativa.
Es necesario señalar las dificultades con algunas de las soluciones más comúnmente propuestas:
Método no válido 1. Consultar Active Directory con personificación
Mucha gente sugiere consultar el Active Directory para algo. Si se lanza una excepción, entonces sabe que las credenciales no son válidas, como se sugiere en esta pregunta de stackoverflow .
Sin embargo, hay algunos inconvenientes serios a este enfoque :
No solo está autentificando una cuenta de dominio, sino que también está realizando una verificación de autorización implícita. Es decir, estás leyendo las propiedades del AD utilizando un token de suplantación. ¿Qué sucede si la cuenta válida de otro modo no tiene derechos de lectura del AD? De forma predeterminada, todos los usuarios tienen acceso de lectura, pero las políticas de dominio se pueden configurar para deshabilitar los permisos de acceso para cuentas restringidas (y / o grupos).
El enlace contra el AD tiene una gran sobrecarga, el caché del esquema del AD debe cargarse en el cliente (caché ADSI en el proveedor ADSI utilizado por DirectoryServices). Esto es tanto la red como el servidor AD, que consumen recursos y son demasiado costosos para una operación simple como autenticar una cuenta de usuario.
Confía en un error de excepción para un caso no excepcional, y suponiendo que eso signifique un nombre de usuario y contraseña no válidos. Otros problemas (por ejemplo, fallo de red, fallo de conectividad AD, error de asignación de memoria, etc.) se interpretan erróneamente como fallo de autenticación.
El uso de la clase DirectoryEntry
es .NET es un ejemplo de una forma incorrecta de verificar las credenciales:
Método no válido 1a - .NET
DirectoryEntry entry = new DirectoryEntry("persuis", "iboyd", "Tr0ub4dor&3");
object nativeObject = entry.NativeObject;
Método no válido 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;
}
}
}
}
Además de consultar Active Directory a través de una conexión ADO:
Método no válido 1c - Consulta nativa
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''
Ambos fallan incluso cuando sus credenciales son válidas , pero no tiene permiso para ver su entrada de directorio:
Método no válido 2. LogonUser Win32 API
Others han sugerido utilizar la función de API LogonUser() . Esto suena bien, pero desafortunadamente el usuario que llama a veces necesita un permiso usualmente dado al sistema operativo en sí:
El proceso que llama a LogonUser requiere el privilegio SE_TCB_NAME. Si el proceso de llamada no tiene este privilegio, LogonUser falla y GetLastError devuelve ERROR_PRIVILEGE_NOT_HELD.
En algunos casos, el proceso que llama a LogonUser también debe tener el privilegio SE_CHANGE_NOTIFY_NAME habilitado; de lo contrario, LogonUser falla y GetLastError devuelve ERROR_ACCESS_DENIED. Este privilegio no es necesario para la cuenta del sistema local o las cuentas que son miembros del grupo de administradores. De forma predeterminada, SE_CHANGE_NOTIFY_NAME está habilitado para todos los usuarios, pero algunos administradores pueden deshabilitarlo para todos.
Entregar el programa " Actuar como parte del sistema operativo " no es algo que quiera hacer a la ligera, como señala Microsoft en un artículo de la base de conocimientos :
... el proceso que está llamando a LogonUser debe tener el privilegio SE_TCB_NAME (en el Administrador de usuarios, este es el derecho " Actuar como parte del sistema operativo "). El privilegio SE_TCB_NAME es muy poderoso y no debe otorgarse a ningún usuario arbitrario solo para que pueda ejecutar una aplicación que necesita validar credenciales.
Además, una llamada a LogonUser () fallará si se especifica una contraseña en blanco.
Método válido de .NET 3.5 - PrincipalContext
Existe un método de validación, solo disponible en .NET 3.5 y más reciente, que permite la autenticación de un usuario sin realizar una verificación de autorización:
// 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")
}
Desafortunadamente, este código solo está disponible en .NET 3.5 y versiones posteriores.
Es hora de encontrar el equivalente nativo .
Autentiqué al usuario, por nombre de usuario y contraseña así:
nombre de usuario es usuario valor de atributo sn en el servidor Ldap, como U12345
userDN es el usuario DistinguishedName en 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;
}
}
Hay una función de la API de win32 llamada ldap_bind_s. La función ldap_bind_s autentica un cliente contra LDAP. Consulte la documentación de MSDN para obtener más información.
Para el equivalente nativo de su solución .NET válida, vea this página de MSDN y ldap_bind
Sin embargo, creo que LogonUser
es la API correcta para la tarea cuando se utiliza con LOGON32_LOGON_NETWORK
. Tenga en cuenta que la limitación de SE_CHANGE_NOTIFY_NAME
es solo para Windows 2000 (por lo que Windows XP y las versiones más recientes no requieren este privilegio) y que, de forma predeterminada, SE_CHANGE_NOTIFY_NAME está habilitado para todos los usuarios. También la página de MSDN dice
El privilegio SE_TCB_NAME no es necesario para esta función, a menos que inicie sesión en una cuenta de Passport.
En este caso, está iniciando sesión en una cuenta de AD, por lo que no se requiere SE_TCB_NAME.
También podría publicar el código nativo para validar un conjunto de credenciales de Windows. Tomó un tiempo para implementar.
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''s 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;
Nota : Cualquier código liberado en el dominio público. No se requiere atribución.
Aquí está la recomendación de Microsoft .
En cuanto a las otras respuestas, no estoy realmente seguro de por qué las estás derribando. Se está quejando de fallas (caso relativamente avanzado) al intentar validar las credenciales, pero si realmente va a hacer algo con esas credenciales, entonces esa operación simplemente va a fallar. Si no va a hacer algo con esas credenciales, ¿por qué necesita validarlas en primer lugar? Parece una situación algo artificial, pero obviamente no sé lo que estás tratando de lograr.