¿Cómo validar las credenciales de dominio?

86

Quiero validar un conjunto de credenciales con el controlador de dominio. p.ej:

Username: STACKOVERFLOW\joel
Password: splotchy

Método 1. Consultar Active Directory con suplantación

Mucha gente sugiere consultar Active Directory por algo. Si se lanza una excepción, entonces sabrá que las credenciales no son válidas, como se sugiere en esta pregunta de stackoverflow .

Sin embargo, existen serios inconvenientes en este enfoque :

  1. No solo está autenticando una cuenta de dominio, sino que también está realizando una verificación de autorización implícita. Es decir, está leyendo propiedades del AD utilizando un token de suplantación. ¿Qué sucede si la cuenta que de otro modo es válida 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 (o grupos) restringidos.

  2. La vinculación contra AD tiene una sobrecarga importante, la caché del esquema de AD debe cargarse en el cliente (caché ADSI en el proveedor ADSI utilizado por DirectoryServices). Esto consume recursos tanto de la red como del servidor AD, y es demasiado caro para una operación simple como autenticar una cuenta de usuario.

  3. Está confiando en una falla de excepción para un caso no excepcional, y asumiendo que eso significa un nombre de usuario y contraseña no válidos. Otros problemas (por ejemplo, falla de red, falla de conectividad de AD, error de asignación de memoria, etc.) se interpretan erróneamente como falla de autenticación.

Método 2. API LogonUser Win32

Otros han sugerido usar la LogonUser()función API. Esto suena bien, pero desafortunadamente, el usuario que llama a veces necesita un permiso que generalmente solo se otorga al sistema operativo:

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 habilitado el privilegio SE_CHANGE_NOTIFY_NAME; 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.

Distribuir el privilegio " Actuar como parte del sistema operativo " no es algo que quiera hacer, como lo señala Microsoft en un artículo de la base de conocimientos :

... el proceso que llama 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 las credenciales.

Además, una llamada a LogonUser()fallará si se especifica una contraseña en blanco.


¿Cuál es la forma correcta de autenticar un conjunto de credenciales de dominio?


Resulta que estoy llamando desde un código administrado, pero esta es una pregunta general de Windows. Se puede suponer que los clientes tienen instalado .NET Framework 2.0.

Ian Boyd
fuente
1
Los lectores deben tener en cuenta que a partir de Windows XP, LogonUser ya no requiere SE_TCB_NAME (a menos que inicie sesión en una cuenta Passport).
Harry Johnston

Respuestas:

130

C # en .NET 3.5 usando System.DirectoryServices.AccountManagement .

 bool valid = false;
 using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
 {
     valid = context.ValidateCredentials( username, password );
 }

Esto validará contra el dominio actual. Consulte el constructor PrincipalContext parametrizado para ver otras opciones.

tvanfosson
fuente
@tvanfosson: ¿DirectoryServices no usa AD?
Mitch Wheat
1
Si. Pero la documentación indica que esta es una forma rápida de validar las credenciales. También es diferente al método de enlace mencionado en la pregunta, ya que no está leyendo ninguna propiedad del objeto. Tenga en cuenta que el método está en el contexto, no en un objeto de directorio.
tvanfosson
Corrección: System.DirectoryServices.AccountManagement requiere .NET 3.5. ( msdn.microsoft.com/en-us/library/… )
Ian Boyd
19
También funciona con usuarios locales si lo usó en su new PrincipalContext(ContextType.Machine)lugar.
VansFannel
¿Alguien sabe si esto funciona con las credenciales almacenadas en caché o si requiere conexión al DC? Necesito saber esto para alguna implementación en la que estoy trabajando ahora y actualmente no estoy en ningún dominio para probar
Jcl
21
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security;
using System.DirectoryServices.AccountManagement;

public struct Credentials
{
    public string Username;
    public string Password;
}

public class Domain_Authentication
{
    public Credentials Credentials;
    public string Domain;

    public Domain_Authentication(string Username, string Password, string SDomain)
    {
        Credentials.Username = Username;
        Credentials.Password = Password;
        Domain = SDomain;
    }

    public bool IsValid()
    {
        using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, Domain))
        {
            // validate the credentials
            return pc.ValidateCredentials(Credentials.Username, Credentials.Password);
        }
    }
}
kantanomo
fuente
7
¿Contiene esto alguna diferencia significativa con la respuesta de @ tvanfosson 3 años antes?
gbjbaanb
5
@gbjbaanb Sí, ya que contiene el Domainparámetro al crear el PrincipalContext, algo que estaba interesado en conocer y encontré en esta respuesta.
Rudi Visser
1
@RudiVisser tvanfosson le sugirió que "consulte el constructor PrincipalContext parametrizado para ver otras opciones". Siempre lea los documentos, ¡nunca tome solo la palabra de Internet para nada! :)
gbjbaanb
4
@gbjbaanb Sí, por supuesto, pero proporcionando un ejemplo de trabajo en lugar de un enlace y la sugerencia de leer en otro lugar es el mantra Stackoverflow, es por eso que aceptamos múltiples presentaciones de respuestas: D Simplemente decir que esto no proporcionar más.
Rudi Visser
¿Alguien sabe cómo podríamos hacer algo similar en una aplicación para UWP? (con AD normal y no con Azure AD). He hecho una pregunta aquí: stackoverflow.com/questions/42821447
slayernoah
7

Estoy usando el siguiente código para validar las credenciales. El método que se muestra a continuación confirmará si las credenciales son correctas y, en caso contrario, si la contraseña ha caducado o debe cambiarse.

He estado buscando algo como esto durante años ... ¡Así que espero que esto ayude a alguien!

using System;
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
using System.Runtime.InteropServices;

namespace User
{
    public static class UserValidation
    {
        [DllImport("advapi32.dll", SetLastError = true)]
        static extern bool LogonUser(string principal, string authority, string password, LogonTypes logonType, LogonProviders logonProvider, out IntPtr token);
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool CloseHandle(IntPtr handle);
        enum LogonProviders : uint
        {
            Default = 0, // default for platform (use this!)
            WinNT35,     // sends smoke signals to authority
            WinNT40,     // uses NTLM
            WinNT50      // negotiates Kerb or NTLM
        }
        enum LogonTypes : uint
        {
            Interactive = 2,
            Network = 3,
            Batch = 4,
            Service = 5,
            Unlock = 7,
            NetworkCleartext = 8,
            NewCredentials = 9
        }
        public  const int ERROR_PASSWORD_MUST_CHANGE = 1907;
        public  const int ERROR_LOGON_FAILURE = 1326;
        public  const int ERROR_ACCOUNT_RESTRICTION = 1327;
        public  const int ERROR_ACCOUNT_DISABLED = 1331;
        public  const int ERROR_INVALID_LOGON_HOURS = 1328;
        public  const int ERROR_NO_LOGON_SERVERS = 1311;
        public  const int ERROR_INVALID_WORKSTATION = 1329;
        public  const int ERROR_ACCOUNT_LOCKED_OUT = 1909;      //It gives this error if the account is locked, REGARDLESS OF WHETHER VALID CREDENTIALS WERE PROVIDED!!!
        public  const int ERROR_ACCOUNT_EXPIRED = 1793;
        public  const int ERROR_PASSWORD_EXPIRED = 1330;

        public static int CheckUserLogon(string username, string password, string domain_fqdn)
        {
            int errorCode = 0;
            using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, domain_fqdn, "ADMIN_USER", "PASSWORD"))
            {
                if (!pc.ValidateCredentials(username, password))
                {
                    IntPtr token = new IntPtr();
                    try
                    {
                        if (!LogonUser(username, domain_fqdn, password, LogonTypes.Network, LogonProviders.Default, out token))
                        {
                            errorCode = Marshal.GetLastWin32Error();
                        }
                    }
                    catch (Exception)
                    {
                        throw;
                    }
                    finally
                    {
                        CloseHandle(token);
                    }
                }
            }
            return errorCode;
        }
    }
Kevinrr3
fuente
este es el "método 2" descrito en la pregunta ... así que ... realmente no responde a la pregunta
Robert Levy
1

A continuación, se explica cómo determinar un usuario local:

    public bool IsLocalUser()
    {
        return windowsIdentity.AuthenticationType == "NTLM";
    }

Editar por Ian Boyd

Ya no debería utilizar NTLM. Es tan antiguo y tan malo que el Verificador de aplicaciones de Microsoft (que se usa para detectar errores comunes de programación) lanzará una advertencia si detecta que usa NTLM.

Aquí hay un capítulo de la documentación del Verificador de aplicaciones sobre por qué tienen una prueba si alguien está usando NTLM por error:

Por qué se necesita el complemento NTLM

NTLM es un protocolo de autenticación obsoleto con fallas que potencialmente comprometen la seguridad de las aplicaciones y el sistema operativo. La deficiencia más importante es la falta de autenticación del servidor, que podría permitir que un atacante engañe a los usuarios para que se conecten a un servidor falsificado. Como corolario de la falta de autenticación del servidor, las aplicaciones que utilizan NTLM también pueden ser vulnerables a un tipo de ataque conocido como ataque de "reflexión". Esto último permite a un atacante secuestrar la conversación de autenticación de un usuario en un servidor legítimo y usarla para autenticar al atacante en la computadora del usuario. Las vulnerabilidades de NTLM y las formas de explotarlas son el objetivo de una creciente actividad de investigación en la comunidad de seguridad.

Aunque Kerberos ha estado disponible durante muchos años, muchas aplicaciones todavía están escritas para usar solo NTLM. Esto reduce innecesariamente la seguridad de las aplicaciones. Sin embargo, Kerberos no puede reemplazar a NTLM en todos los escenarios, principalmente en aquellos en los que un cliente necesita autenticarse en sistemas que no están unidos a un dominio (una red doméstica quizás sea la más común). El paquete de seguridad Negotiate permite un compromiso compatible con versiones anteriores que usa Kerberos siempre que sea posible y solo vuelve a NTLM cuando no hay otra opción. Cambiar el código para usar Negotiate en lugar de NTLM aumentará significativamente la seguridad para nuestros clientes al tiempo que presenta pocas o ninguna compatibilidad de aplicaciones. Negociar por sí solo no es una solución milagrosa: hay casos en los que un atacante puede forzar la degradación a NTLM, pero estos son significativamente más difíciles de explotar. Sin embargo, una mejora inmediata es que las aplicaciones escritas para usar Negotiate correctamente son automáticamente inmunes a los ataques de reflexión NTLM.

A modo de advertencia final contra el uso de NTLM: en futuras versiones de Windows será posible deshabilitar el uso de NTLM en el sistema operativo. Si las aplicaciones tienen una dependencia estricta de NTLM, simplemente no se autenticarán cuando NTLM esté deshabilitado.

Cómo funciona el complemento

El enchufe Verifier detecta los siguientes errores:

  • El paquete NTLM se especifica directamente en la llamada a AcquireCredentialsHandle (o API contenedora de nivel superior).

  • El nombre de destino en la llamada a InitializeSecurityContext es NULL.

  • El nombre de destino en la llamada a InitializeSecurityContext no es un SPN, UPN o un nombre de dominio estilo NetBIOS correctamente formado.

Los dos últimos casos obligarán a Negotiate a recurrir a NTLM ya sea directamente (el primer caso) o indirectamente (el controlador de dominio devolverá un error de "principal no encontrado" en el segundo caso, lo que hará que Negotiate retroceda).

El complemento también registra advertencias cuando detecta cambios a NTLM; por ejemplo, cuando el controlador de dominio no encuentra un SPN. Estos solo se registran como advertencias, ya que a menudo son casos legítimos, por ejemplo, cuando se autentica en un sistema que no está unido a un dominio.

Paradas NTLM

5000: la aplicación tiene un paquete NTLM seleccionado explícitamente

Severidad: error

La aplicación o subsistema selecciona explícitamente NTLM en lugar de Negotiate en la llamada a AcquireCredentialsHandle. Aunque es posible que el cliente y el servidor se autentiquen mediante Kerberos, esto se evita mediante la selección explícita de NTLM.

Cómo arreglar este error

La solución para este error es seleccionar el paquete Negociar en lugar de NTLM. La forma de hacerlo dependerá del subsistema de red particular que esté utilizando el cliente o servidor. A continuación se dan algunos ejemplos. Debe consultar la documentación sobre la biblioteca o el conjunto de API en particular que está utilizando.

APIs(parameter) Used by Application    Incorrect Value  Correct Value  
=====================================  ===============  ========================
AcquireCredentialsHandle (pszPackage)  “NTLM”           NEGOSSP_NAME “Negotiate”
Alan Nicholas
fuente
-1
using System;
using System.Collections.Generic;
using System.Text;
using System.DirectoryServices.AccountManagement;

class WindowsCred
{
    private const string SPLIT_1 = "\\";

    public static bool ValidateW(string UserName, string Password)
    {
        bool valid = false;
        string Domain = "";

        if (UserName.IndexOf("\\") != -1)
        {
            string[] arrT = UserName.Split(SPLIT_1[0]);
            Domain = arrT[0];
            UserName = arrT[1];
        }

        if (Domain.Length == 0)
        {
            Domain = System.Environment.MachineName;
        }

        using (PrincipalContext context = new PrincipalContext(ContextType.Domain, Domain)) 
        {
            valid = context.ValidateCredentials(UserName, Password);
        }

        return valid;
    }
}

Kashif Mushtaq Ottawa, Canadá

Markus Safar
fuente
El espacio de nombres System.DirectoryServices.AccountManagement era nuevo en .NET 3.5
Jeremy Gray
1
Sé que esto tiene casi 4 años, pero si está validando un usuario local, deberá asegurarse de establecer ContextType en ContextType.Machine cuando construya un PrincipalContext. De lo contrario, pensará que el nombre de la máquina proporcionado en la variable Dominio es en realidad un servidor de dominio.
SolidRegardless