Active Directory (LDAP) - Check account locked out / Password expired

后端 未结 4 1304
轮回少年
轮回少年 2020-12-02 21:04

Currently I authenticate users against some AD using the following code:

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

try
{
    // Bind          


        
相关标签:
4条回答
  • 2020-12-02 21:36

    Here are the AD LDAP attributes that change for a user when a password is locked out (first value) versus when a password is not locked out (second value). badPwdCount and lockoutTime are obviously the most relevant. I'm not sure whether uSNChanged and whenChanged must be updated manually or not.

    $ diff LockedOut.ldif NotLockedOut.ldif:

    < badPwdCount: 3
    > badPwdCount: 0
    
    < lockoutTime: 129144318210315776
    > lockoutTime: 0
    
    < uSNChanged: 8064871
    > uSNChanged: 8065084
    
    < whenChanged: 20100330141028.0Z
    > whenChanged: 20100330141932.0Z
    
    0 讨论(0)
  • 2020-12-02 21:40

    I know this answer is a few years late, but we just ran into the same situation as the original poster. Unfortunately, in our environment, we can't use LogonUser -- we needed a pure LDAP solution. It turns out there is a way to get the extended error code from a bind operation. It's a bit ugly, but it works:

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

    And you'll need definitions for the error codes (there are a lot more of these at 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,
    }
    

    I couldn't find this information anywhere else -- everyone just says you should use LogonUser. If there's a better solution, I'd love to hear it. If not, I hope this helps other people who can't call LogonUser.

    0 讨论(0)
  • 2020-12-02 21:43

    The "password expires" check is relatively easy - at least on Windows (not sure how other systems handle this): when the Int64 value of "pwdLastSet" is 0, then the user will have to change his (or her) password at next logon. The easiest way to check this is include this property in your 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];
    

    As for the "account is locked out" check - this seems easy at first, but isn't.... The "UF_Lockout" flag on "userAccountControl" doesn't do its job reliably.

    Beginning with Windows 2003 AD, there's a new computed attribute which you can check for: msDS-User-Account-Control-Computed.

    Given a DirectoryEntry user, you can do:

    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
    }
    

    If you can use .NET 3.5, things have gotten a lot easier - check out the MSDN article on how to deal with users and groups in .NET 3.5 using the System.DirectoryServices.AccountManagement namespace. E.g. you now do have a property IsAccountLockedOut on the UserPrincipal class which reliably tells you whether or not an account is locked out.

    Hope this helps!

    Marc

    0 讨论(0)
  • A little late but I'll throw this out there.

    If you want to REALLY be able to determine the specific reason that an account is failing authentication (there are many more reasons other than wrong password, expired, lockout, etc.), you can use the windows API LogonUser. Don't be intimidated by it - it is easier than it looks. You simply call LogonUser, and if it fails you look at the Marshal.GetLastWin32Error() which will give you a return code that indicates the (very) specific reason that the logon failed.

    However, you're not going to be able to call this in the context of the user you're authenticating; you're going to need a priveleged account - I believe the requirement is SE_TCB_NAME (aka SeTcbPrivilege) - a user account that has the right to 'Act as part of the operating system'.

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

    if it fails, you get one of the return codes (there are more that you can look up, but these are the important ones:

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

    The rest is just copy/paste to get the DLLImports and values to pass in

      //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 讨论(0)
提交回复
热议问题