Répertoire actif (LDAP) - vérifier que le compte est verrouillé / mot de passe expiré

actuellement, j'authentifie les utilisateurs contre certaines annonces en utilisant le code suivant:

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);
}

cela fonctionne parfaitement pour valider un mot de passe contre un nom d'utilisateur.

le problème vient du fait qu'une erreur générique est toujours retournée "échec de connexion: nom d'utilisateur inconnu ou mauvais mot de passe."lorsque l'authentification échoue.

Toutefois, l'authentification peut également échouer lorsqu'un compte est verrouillé.

comment le saurais-je s'il échoue parce qu'il est verrouillé?

j'ai trouvé des articles disant que vous pouvez utiliser:

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

ou de faire quelque chose comme expliqué ici

le problème est, chaque fois que vous essayez d'accéder à une propriété sur le DirectoryEntry, la même erreur serait lancée.

D'autres suggestions sur la façon d'obtenir la raison réelle pour laquelle l'authentification a échoué? (compte verrouillé / mot de passe expiré/etc.)

L'annonce à laquelle je me connecte pourrait ne pas être nécessairement une Windows server.

18
demandé sur Jabezz 2009-09-08 17:25:27

4 réponses

Un peu en retard mais je vais jeter ce.

si vous voulez vraiment être en mesure de déterminer la raison spécifique pour laquelle un compte échoue l'authentification (il y a beaucoup plus de raisons autres que le mauvais mot de passe, expiré, lock-out, etc.), vous pouvez utiliser l'API de Windows LogonUser. Ne vous laissez pas intimider par cela - c'est plus facile qu'il n'y paraît. Vous appelez simplement LogonUser, et si elle échoue, vous regardez le Marshal.GetLastWin32Error () qui vous donnera un code de retour qui indique (très) spécifique raison pour laquelle la connexion a échoué.

cependant, vous n'allez pas être en mesure d'appeler cela dans le contexte de l'utilisateur que vous authentifiez; vous allez avoir besoin d'un compte privilégié - je crois que l'exigence est SE_TCB_NAME (alias SeTcbPrivilege) - un compte utilisateur qui a le droit d '"agir en tant que partie du système d'exploitation".

//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 elle échoue, vous obtenez un des codes de retour (il y en a plus que vous pouvez consulter, mais ce sont les :

 //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;  

le reste est simplement copier / coller pour faire passer les dllimport et les valeurs dans

  //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);
16
répondu ScottBai 2013-01-28 10:29:57

je sais que cette réponse est en retard de quelques années, mais nous venons de rencontrer la même situation que l'affiche originale. Malheureusement, dans notre environnement, nous ne pouvons pas utiliser LogonUser -- nous avions besoin d'une solution LDAP pure. Il s'avère qu'il y est un moyen d'obtenir le code d'erreur étendue à partir d'une opération de liaison. C'est un peu moche, mais ça fonctionne:

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;
}

et vous aurez besoin de définitions pour les codes d'erreur (il y en a beaucoup plus à 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,
}

Je n'ai pu trouver cette information nulle part ailleurs -- tout le monde dit juste que vous devriez utiliser LogonUser. S'il y a une meilleure solution, j'aimerais l'entendre. Sinon, j'espère que cela aidera D'autres personnes qui ne peuvent pas appeler LogonUser.

7
répondu Rand Scullard 2013-05-28 16:00:48

la vérification " password expires "est relativement facile - au moins sur Windows (Je ne sais pas comment les autres systèmes gèrent cela): lorsque la valeur Int64 de" pwdLastSet " est 0, alors l'Utilisateur devra changer son (ou ses) mot de passe lors de la prochaine connexion. La façon la plus simple de vérifier ceci est d'inclure cette propriété dans votre 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];

pour ce qui est du chèque" compte bloqué " - cela semble facile au début, mais ce n'est pas le cas.... Le drapeau "UF_Lockout" sur "userAccountControl" ne fait pas son travail fiable.

A partir de Windows 2003 AD, il y a un nouvel attribut computé que vous pouvez vérifier pour: msDS-User-Account-Control-Computed.

Donné un DirectoryEntry user, vous pouvez faire:

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 vous pouvez utiliser .NET 3.5, les choses sont devenues beaucoup plus facile - découvrez article MSDN sur la façon de traiter avec les utilisateurs et les groupes dans .NET 3.5 en utilisant le System.DirectoryServices.AccountManagement espace de noms. Par exemple: vous avez maintenant une propriété IsAccountLockedOut sur la classe UserPrincipal qui vous indique de manière fiable si un compte est verrouillé.

Espérons que cette aide!

Marc

4
répondu marc_s 2009-09-08 15:46:11

Voici les attributs AD LDAP qui changent pour un utilisateur lorsqu'un mot de passe est verrouillé (première valeur) par rapport à lorsqu'un mot de passe n'est pas verrouillé (deuxième valeur). badPwdCount et lockoutTime sont évidemment les plus pertinents. Je ne suis pas sûr si uSNChanged et when changed doivent être mis à jour manuellement ou non.

$ diff LockedOut.ldif NotLockedOut.ldif:

< badPwdCount: 3
> badPwdCount: 0

< lockoutTime: 129144318210315776
> lockoutTime: 0

< uSNChanged: 8064871
> uSNChanged: 8065084

< whenChanged: 20100330141028.0Z
> whenChanged: 20100330141932.0Z
1
répondu joeforker 2010-03-30 14:28:34