简体   繁体   English

Active Directory(LDAP) - 检查帐户被锁定/密码已过期

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

Currently I authenticate users against some AD using the following code: 目前,我使用以下代码针对某些AD对用户进行身份验证:

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

This works perfectly for validating a password against a username. 这非常适合验证用户名的密码。

The problem comes in that a generic errors is always returned "Logon failure: unknown user name or bad password." 问题在于总是返回一般错误“登录失败:未知用户名或密码错误”。 when authentication fails. 验证失败时

However authentication might also fail when an account is locked out. 但是,当帐户被锁定时,身份验证也可能失败。

How would I know if it is failing because of it being locked out? 我怎么知道它是否因为被锁定而失败?

I've come across articles saying you can use: 我遇到过你可以使用的文章:

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

or do something like explained here 或做一些像这里解释的事情

The problem is, whenever you try to access any property on the DirectoryEntry, the same error would be thrown. 问题是,每当您尝试访问DirectoryEntry上的任何属性时,都会抛出相同的错误。

Any other suggestion of how to get to the actual reason that authentication failed? 关于如何找到认证失败的实际原因的任何其他建议? (account locked out / password expired / etc.) (帐户被锁定/密码已过期/等)

The AD I connect to might not neccesarily be a windows server. 我连接到的AD可能不一定是Windows服务器。

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. 如果您想真正确定帐户验证失败的具体原因(除了错误的密码,过期,锁定等等,还有更多原因),您可以使用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. 您只需调用LogonUser,如果失败,则查看Marshal.GetLastWin32Error(),它将为您提供返回代码,指示登录失败的(非常)具体原因。

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'. 您将需要一个priveleged帐户 - 我认为该要求是SE_TCB_NAME(又名SeTcbPrivilege) - 一个有权“作为操作系统的一部分”的用户帐户。

//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 其余的只是复制/粘贴以获取要传入的DLLImports和值

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

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. 不幸的是,在我们的环境中,我们无法使用LogonUser - 我们需要一个纯LDAP解决方案。 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 ): 并且您需要错误代码的定义( 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. 我无法在其他地方找到这些信息 - 每个人都说你应该使用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. 如果没有,我希望这可以帮助其他无法调用LogonUser的人。

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. “密码过期”检查相对容易 - 至少在Windows上(不确定其他系统如何处理):当“pwdLastSet”的Int64值为0时,用户必须在下一次更改其(或她)密码登录。 The easiest way to check this is include this property in your DirectorySearcher: 检查此问题的最简单方法是在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. 至于“帐户被锁定”检查 - 这看起来很容易,但不是......“userAccountControl”上的“UF_Lockout”标志不能可靠地完成其工作。

Beginning with Windows 2003 AD, there's a new computed attribute which you can check for: msDS-User-Account-Control-Computed . 从Windows 2003 AD开始,您可以检查一个新的计算属性: msDS-User-Account-Control-Computed

Given a DirectoryEntry user , you can do: 给定DirectoryEntry user ,您可以:

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. 如果你可以使用.NET 3.5,事情变得容易多了 - 请查看MSDN文章 ,了解如何使用System.DirectoryServices.AccountManagement命名空间处理.NET 3.5中的用户和组。 Eg you now do have a property IsAccountLockedOut on the UserPrincipal class which reliably tells you whether or not an account is locked out. 例如,您现在在UserPrincipal类上有一个属性IsAccountLockedOut ,可以可靠地告诉您帐户是否被锁定。

Hope this helps! 希望这可以帮助!

Marc

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). 以下是在锁定密码(第一个值)时密码为用户时更改的AD LDAP属性,而密码未锁定时(第二个值)。 badPwdCount and lockoutTime are obviously the most relevant. badPwdCountlockoutTime显然是最相关的。 I'm not sure whether uSNChanged and whenChanged must be updated manually or not. 我不确定是否必须手动更新uSNChanged和whenChanged。

$ diff LockedOut.ldif NotLockedOut.ldif : $ diff LockedOut.ldif NotLockedOut.ldif

< badPwdCount: 3
> badPwdCount: 0

< lockoutTime: 129144318210315776
> lockoutTime: 0

< uSNChanged: 8064871
> uSNChanged: 8065084

< whenChanged: 20100330141028.0Z
> whenChanged: 20100330141932.0Z

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM