2009-09-08 55 views
16

Actualmente me autenticar usuarios en algunos AD utilizando el siguiente código:Active Directory (LDAP) - Comprobar la cuenta bloqueada/contraseña expiró

DirectoryEntry entry = new DirectoryEntry(_path, username, pwd); 

try 
{ 
    // Bind to the native AdsObject to force authentication. 
    Object obj = entry.NativeObject; 

    DirectorySearcher search = new DirectorySearcher(entry) { Filter = "(sAMAccountName=" + username + ")" }; 
    search.PropertiesToLoad.Add("cn"); 
    SearchResult result = search.FindOne(); 
    if (result == null) 
    { 
     return false; 
    } 
    // Update the new path to the user in the directory 
    _path = result.Path; 
    _filterAttribute = (String)result.Properties["cn"][0]; 
} 
catch (Exception ex) 
{ 
    throw new Exception("Error authenticating user. " + ex.Message); 
} 

Esto funciona perfectamente para validar una contraseña contra un nombre de usuario.

El problema viene en que siempre se devuelve un error genérico "Error de inicio de sesión: nombre de usuario desconocido o contraseña incorrecta". cuando la autenticación falla

Sin embargo, la autenticación también puede fallar cuando una cuenta está bloqueada.

¿Cómo puedo saber si está fallando debido a que está bloqueado?

me he encontrado artículos diciendo que puede utilizar:

Convert.ToBoolean(entry.InvokeGet("IsAccountLocked")) 

o hacer algo como explicó here

El problema es que cada vez que intente acceder a cualquier propiedad en el DirectoryEntry, el mismo error sería arrojado.

¿Alguna otra sugerencia de cómo llegar al motivo real por el que falló la autenticación? (cuenta bloqueada/contraseña expirada/etc.)

El AD al que me conecto no necesariamente es un servidor de Windows.

Respuesta

15

Un poco tarde pero voy a tirar esto por ahí.

Si quiere REALMENTE poder determinar el motivo específico por el que una cuenta está fallando la autenticación (hay muchas más razones que la incorrecta, caducada, bloqueo, etc.), puede usar el API de Windows LogonUser. No se deje intimidar por eso, es más fácil de lo que parece. Simplemente llama a LogonUser y, si falla, miras al Marshal.GetLastWin32Error() que le proporcionará un código de retorno que indica la razón (muy) específica por la que falló el inicio de sesión.

Sin embargo, no podrá llamar a esto en el contexto del usuario que está autenticando; Necesitará una cuenta privada: creo que el requisito es SE_TCB_NAME (también conocido como SeTcbPrivilege), una cuenta de usuario que tiene derecho a "Actuar como parte del sistema operativo".

//Your new authenticate code snippet: 
     try 
     { 
      if (!LogonUser(user, domain, pass, LogonTypes.Network, LogonProviders.Default, out token)) 
      { 
       errorCode = Marshal.GetLastWin32Error(); 
       success = false; 
      } 
     } 
     catch (Exception) 
     { 
      throw; 
     } 
     finally 
     { 
      CloseHandle(token);  
     }    
     success = true; 

si falla, se obtiene uno de los códigos de retorno (hay más que se puede mirar hacia arriba, pero estos son los más importantes:

//See http://support.microsoft.com/kb/155012 
    const int ERROR_PASSWORD_MUST_CHANGE = 1907; 
    const int ERROR_LOGON_FAILURE = 1326; 
    const int ERROR_ACCOUNT_RESTRICTION = 1327; 
    const int ERROR_ACCOUNT_DISABLED = 1331; 
    const int ERROR_INVALID_LOGON_HOURS = 1328; 
    const int ERROR_NO_LOGON_SERVERS = 1311; 
    const int ERROR_INVALID_WORKSTATION = 1329; 
    const int ERROR_ACCOUNT_LOCKED_OUT = 1909;  //It gives this error if the account is locked, REGARDLESS OF WHETHER VALID CREDENTIALS WERE PROVIDED!!! 
    const int ERROR_ACCOUNT_EXPIRED = 1793; 
    const int ERROR_PASSWORD_EXPIRED = 1330; 

El resto es sólo copiar/pegar para obtener los DLLImports y valores que pasen en

//here are enums 
    enum LogonTypes : uint 
     { 
      Interactive = 2, 
      Network =3, 
      Batch = 4, 
      Service = 5, 
      Unlock = 7, 
      NetworkCleartext = 8, 
      NewCredentials = 9 
     } 
     enum LogonProviders : uint 
     { 
      Default = 0, // default for platform (use this!) 
      WinNT35,  // sends smoke signals to authority 
      WinNT40,  // uses NTLM 
      WinNT50  // negotiates Kerb or NTLM 
     } 

//Paste these DLLImports 

[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); 
+0

Gracias por esto. He encontrado pruebas en contra de un Servidor AD de Windows 2008 que para las contraseñas expiradas pero válidas, el resultado será 'ERROR_PASSWORD_MUST_CHANGE', pero si la contraseña está vencida y la contraseña proporcionada no es válida, el resultado será' ERROR_LOGON_FAILURE'. – Alpha

+0

Ver mi respuesta (http://stackoverflow.com/a/16796531/1230982) si puede ' t use LogonUser y necesita una solución LDAP. –

4

La comprobación de "contraseña expira" es relativamente fácil, al menos en Windows (no estoy seguro de cómo otros sistemas manejan esto): cuando el valor Int64 de "pwdLastSet" es 0, el usuario tendrá que cambiar su (o) contraseña en el siguiente inicio de sesión. La forma más sencilla de comprobar esto es incluir esta propiedad en su DirectorySearcher:

DirectorySearcher search = new DirectorySearcher(entry) 
     { Filter = "(sAMAccountName=" + username + ")" }; 
search.PropertiesToLoad.Add("cn"); 
search.PropertiesToLoad.Add("pwdLastSet"); 

SearchResult result = search.FindOne(); 
if (result == null) 
{ 
    return false; 
} 

Int64 pwdLastSetValue = (Int64)result.Properties["pwdLastSet"][0]; 

En cuanto a la "cuenta está bloqueada" check - esto parece fácil al principio, pero no es .... El "UF_Lockout" indicador en "userAccountControl" no hace su trabajo de manera confiable.

A partir de Windows 2003 AD, hay un nuevo atributo calculado que puede verificar: msDS-User-Account-Control-Computed.

Dado un DirectoryEntry user, que puede hacer:

string attribName = "msDS-User-Account-Control-Computed"; 
user.RefreshCache(new string[] { attribName }); 

const int UF_LOCKOUT = 0x0010; 

int userFlags = (int)user.Properties[attribName].Value; 

if(userFlags & UF_LOCKOUT == UF_LOCKOUT) 
{ 
    // if this is the case, the account is locked out 
} 

Si puede utilizar .NET 3.5, las cosas se han vuelto mucho más fácil - echa un vistazo a la MSDN article sobre cómo tratar con los usuarios y grupos en .NET 3.5 utilizando el espacio de nombre System.DirectoryServices.AccountManagement. P.ej. ahora tiene una propiedad IsAccountLockedOut en la clase UserPrincipal que le dice confiablemente si una cuenta está bloqueada o no.

Espero que esto ayude!

Marc

+0

Gracias marc ... lo probaré. ¿La System.DirectoryServices.AccountManagement de .NET 3.5 no me limita a los directorios activos de Windows? ¿O sigue aplicando las mismas LDAP prinicipals? – Jabezz

+0

Ah, lo siento, sí, S.DS.AM es específico de Active Directory, lo siento. Pero también hay una biblioteca LDAP de "bajo nivel" en el espacio de nombres System.DirectoryServices.Protocols, desde .NET 2.0 (creo) –

+0

Hola Marc, probé las sugerencias, pero sigo teniendo el mismo problema. Ni siquiera puedo aplicar un DirectorySearcher si paso el nombre de usuario/pwd al constructor DirectoryEntry, ya que la autenticación fallará si la cuenta está bloqueada. Si no lo paso, puedo hacer una búsqueda, pero no acceder a ninguna de las propiedades mencionadas. "pwdLastSet" no está allí y user.Properties siempre está vacío. Supongo que tendré que poner esto en hielo por un tiempo. – Jabezz

1

Aquí están los LDAP AD atributos que el cambio de un usuario cuando una contraseña se bloquea (primer valor) frente cuando una contraseña no está bloqueada (segundo valor). badPwdCount y lockoutTime son obviamente los más relevantes. No estoy seguro de si uSNChanged y whenChanged deben actualizarse manualmente o no.

$ diff LockedOut.ldif NotLockedOut.ldif:

< badPwdCount: 3 
> badPwdCount: 0 

< lockoutTime: 129144318210315776 
> lockoutTime: 0 

< uSNChanged: 8064871 
> uSNChanged: 8065084 

< whenChanged: 20100330141028.0Z 
> whenChanged: 20100330141932.0Z 
6

Sé que esta respuesta es un par de años de retraso, pero acabo de encontrar con la misma situación que el cartel original. Desafortunadamente, en nuestro entorno, no podemos usar LogonUser, necesitábamos una solución LDAP pura. Resulta que hay una manera de obtener el código de error extendido de una operación de enlace. Es un poco feo, pero funciona:

catch(DirectoryServicesCOMException exc) 
{ 
    if((uint)exc.ExtendedError == 0x80090308) 
    { 
     LDAPErrors errCode = 0; 

     try 
     { 
      // Unfortunately, the only place to get the LDAP bind error code is in the "data" field of the 
      // extended error message, which is in this format: 
      // 80090308: LdapErr: DSID-0C09030B, comment: AcceptSecurityContext error, data 52e, v893 
      if(!string.IsNullOrEmpty(exc.ExtendedErrorMessage)) 
      { 
       Match match = Regex.Match(exc.ExtendedErrorMessage, @" data (?<errCode>[0-9A-Fa-f]+),"); 
       if(match.Success) 
       { 
        string errCodeHex = match.Groups["errCode"].Value; 
        errCode = (LDAPErrors)Convert.ToInt32(errCodeHex, fromBase: 16); 
       } 
      } 
     } 
     catch { } 

     switch(errCode) 
     { 
      case LDAPErrors.ERROR_PASSWORD_EXPIRED: 
      case LDAPErrors.ERROR_PASSWORD_MUST_CHANGE: 
       throw new Exception("Your password has expired and must be changed."); 

      // Add any other special error handling here (account disabled, locked out, etc...). 
     } 
    } 

    // If the extended error handling doesn't work out, just throw the original exception. 
    throw; 
} 

y necesitará definiciones de los códigos de error (hay muchos más de estos en http://www.lifeasbob.com/code/errorcodes.aspx):

private enum LDAPErrors 
{ 
    ERROR_INVALID_PASSWORD = 0x56, 
    ERROR_PASSWORD_RESTRICTION = 0x52D, 
    ERROR_LOGON_FAILURE = 0x52e, 
    ERROR_ACCOUNT_RESTRICTION = 0x52f, 
    ERROR_INVALID_LOGON_HOURS = 0x530, 
    ERROR_INVALID_WORKSTATION = 0x531, 
    ERROR_PASSWORD_EXPIRED = 0x532, 
    ERROR_ACCOUNT_DISABLED = 0x533, 
    ERROR_ACCOUNT_EXPIRED = 0x701, 
    ERROR_PASSWORD_MUST_CHANGE = 0x773, 
    ERROR_ACCOUNT_LOCKED_OUT = 0x775, 
    ERROR_ENTRY_EXISTS = 0x2071, 
} 

no pude encontrar esta información en otro lugar; todos dicen que debes usar LogonUser. Si hay una solución mejor, me encantaría escucharla. De lo contrario, espero que esto ayude a otras personas que no pueden llamar a LogonUser.

+0

Además de su enlace, encontré esta referencia de MS a mano: https://support.microsoft.com/en-us/kb/155012 –

+0

incluso si la cuenta está bloqueada, todavía me aparece solo COMException y no DirectoryServicesC OMException. Entonces este método no funciona. –

Cuestiones relacionadas