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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1394025/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-06 16:01:55  来源:igfitidea点击:

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

c#active-directoryldap

提问by Jabezz

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 服务器。

采纳答案by ScottBai

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.

如果您希望真正能够确定帐户身份验证失败的具体原因(除了密码错误、过期、锁定等之外还有更多原因),您可以使用 Windows API LogonUser。不要被它吓倒 - 它比看起来容易。您只需调用 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'.

但是,您将无法在您进行身份验证的用户的上下文中调用它;您将需要一个特权帐户 - 我相信要求是 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);

回答by marc_s

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:

“密码过期”检查相对容易 - 至少在 Windows 上(不确定其他系统如何处理):当“pwdLastSet”的 Int64 值为 0 时,用户将不得不在下一次更改他(或她)的密码登录。检查这一点的最简单方法是在 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 articleon how to deal with users and groups in .NET 3.5 using the System.DirectoryServices.AccountManagementnamespace. E.g. you now do have a property IsAccountLockedOuton the UserPrincipal class which reliably tells you whether or not an account is locked out.

如果您可以使用 .NET 3.5,事情就会变得容易得多 - 查看MSDN 文章,了解如何使用System.DirectoryServices.AccountManagement命名空间处理 .NET 3.5 中的用户和组。例如,您现在确实IsAccountLockedOut在 UserPrincipal 类上有一个属性,它可以可靠地告诉您帐户是否被锁定。

Hope this helps!

希望这可以帮助!

Marc

马克

回答by joeforker

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). badPwdCountand lockoutTimeare obviously the most relevant. I'm not sure whether uSNChanged and whenChanged must be updated manually or not.

以下是在密码被锁定(第一个值)与密码未锁定(第二个值)时为用户更改的 AD LDAP 属性。badPwdCount并且lockoutTime显然是最相关的。我不确定 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

回答by Rand Scullard

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:

我知道这个答案晚了几年,但我们遇到了与原始海报相同的情况。不幸的是,在我们的环境中,我们不能使用 LogonUser —— 我们需要一个纯 LDAP 解决方案。事实证明,有一种方法可以从绑定操作中获取扩展错误代码。这有点难看,但它有效:

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. 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。如果有更好的解决方案,我很乐意听到。如果没有,我希望这可以帮助无法调用 LogonUser 的其他人。