【问题标题】:Calling NetValidatePasswordPolicy from C# always returns Password Must Change从 C# 调用 NetValidatePasswordPolicy 总是返回 Password Must Change
【发布时间】:2015-04-21 07:03:01
【问题描述】:

我们有一个 mvc 应用程序,它使用 Active Directory 来验证我们的用户。我们正在利用System.DirectoryServices 并使用PricipalContext 进行身份验证:

_principalContext.ValidateCredentials(userName, pass, ContextOptions.SimpleBind);

然而,这个方法只返回一个布尔值,我们希望返回更好的消息,甚至将用户重定向到密码重置屏幕,例如:

  1. 用户的帐户被锁定。
  2. 用户密码已过期。
  3. 用户需要在下次登录时更改密码。

因此,如果用户无法登录,我们调用NetValidatePasswordPolicy 来查看用户无法登录的原因。这似乎运作良好,但我们意识到无论状态如何,此方法都只会返回NET_API_STATUS.NERR_PasswordMustChange Active Directory 用户是。

我发现的唯一一个同样问题的例子来自一个 Sublime Speech 插件here。我使用的代码如下:

var outputPointer = IntPtr.Zero;
var inputArgs = new NET_VALIDATE_PASSWORD_CHANGE_INPUT_ARG { PasswordMatched = false, UserAccountName = username };
inputArgs.ClearPassword = Marshal.StringToBSTR(password);

var inputPointer = IntPtr.Zero;
inputPointer = Marshal.AllocHGlobal(Marshal.SizeOf(inputArgs));
Marshal.StructureToPtr(inputArgs, inputPointer, false);

using (new ComImpersonator(adImpersonatingUserName, adImpersonatingDomainName, adImpersonatingPassword))
{
    var status = NetValidatePasswordPolicy(serverName, IntPtr.Zero, NET_VALIDATE_PASSWORD_TYPE.NetValidateAuthentication, inputPointer, ref outputPointer);

    if (status == NET_API_STATUS.NERR_Success)
    {
        var outputArgs = (NET_VALIDATE_OUTPUT_ARG)Marshal.PtrToStructure(outputPointer, typeof(NET_VALIDATE_OUTPUT_ARG));
        return outputArgs.ValidationStatus;
    }
    else
    {
       //fail
    }   
}

代码总是成功,为什么outputArgs.ValidationStatus 的值每次都是相同的结果,而不管 Active Directory 用户的状态如何?

【问题讨论】:

  • 您在 ValidateCredentials() 之后尝试过 GetLastError() 吗?那么您可以使用错误代码来确定消息,例如 1907 是“用户必须重置密码”,1330 是“密码过期”等等......我可以考虑的另一个选项是 Logon 而不是 ValidateCredentials 然后GetLastError()
  • 我查看了ValidateCredentials()的源码,该方法使用LADP,如果有错误代码该方法将抛出异常,然后捕获异常并返回false。所以 GetLastError 可能会起作用。我还看到活动目录有一些特殊的错误代码。
  • 我们不想使用 LogonUser 方法,因为我们正在使用目录服务登录。如果用户输错密码,我们会尝试登录,失败,然后登录查看失败的原因。这将为用户的每次尝试提供两次失败的登录尝试。我将研究 GetLastError() 但 ValidateCredentials 每次都会返回 1907 并迫使开发人员查看 GetLastError() 以获取实际代码似乎很奇怪。

标签: c# com active-directory interop


【解决方案1】:

我将把这个问题的答案分成三个不同的部分:

  1. 您的方法目前存在的问题
  2. 在线和本主题中推荐解决方案的问题
  3. 解决方案

您的方法目前存在的问题。

NetValidatePasswordPolicy 需要它的InputArgs 参数来接收一个指向结构的指针,而你传入的结构取决于你传入的ValidationType。在这种情况下,你传入的是NET_VALIDATE_PASSWORD_TYPE.NetValidateAuthentication,这需要 NET_VALIDATE_AUTHENTICATION_INPUT_ARG 的 InputArgs,但您传递的是指向 NET_VALIDATE_PASSWORD_CHANGE_INPUT_ARG 的指针。

此外,您正在尝试将“currentPassword”类型的值分配给NET_VALIDATE_PASSWORD_CHANGE_INPUT_ARG 结构。

然而,使用NetValidatePasswordPolicy 有一个更大的基本问题,那就是您正试图使用​​此功能来验证 Active Directory 中的密码,但这不是它的用途。 NetValidatePasswordPolicy 用于允许应用程序根据应用程序提供的身份验证数据库进行验证。

有更多关于NetValidatePasswordPolicyhere的信息。

在线和本帖中推荐解决方案的问题

网上的各种文章都推荐使用AdvApi32.dll 中的LogonUser 函数,但这个实现有其自身的问题:

首先,LogonUser 会根据本地缓存进行验证,这意味着您不会立即获得有关该帐户的准确信息,除非您使用“网络”模式。

第二个是在 Web 应用程序上使用 LogonUser,在我看来有点 hacky,因为它是为在客户端计算机上运行的桌面应用程序设计的。但是,考虑到 Microsoft 提供的限制,如果 LogonUser 给出了预期的结果,我不明白为什么不应该使用它 - 除非缓存问题。

LogonUser 的另一个问题是,它对您的用例的效果取决于您的服务器的配置方式,例如:需要在您针对该需求进行身份验证的域上启用一些特定权限以使“网络”登录类型正常工作。

更多关于LogonUserhere的信息。

另外,不应使用GetLastError(),而应使用GetLastWin32Error(),因为使用GetLastError() 是不安全的。

更多关于GetLastWin32Error()here的信息。

解决方案。

为了从 Active Directory 中获取准确的错误代码,并且没有任何缓存问题并且直接从目录服务中获取,需要执行以下操作:当帐户出现问题时依赖从 AD 返回的 COMException,因为最终,错误就是您要查找的内容。

首先,在验证当前用户名和密码时从 Active Directory 触发错误的方法如下:

public LdapBindAuthenticationErrors AuthenticateUser(string domain, string username, string password, string ouString)
    {
        // The path (ouString) should not include the user in the directory, otherwise this will always return true
        DirectoryEntry entry = new DirectoryEntry(ouString, username, password);

        try
        {
            // Bind to the native object, this forces authentication.
            var obj = entry.NativeObject;
            var search = new DirectorySearcher(entry) { Filter = string.Format("({0}={1})", ActiveDirectoryStringConstants.SamAccountName, username) };
            search.PropertiesToLoad.Add("cn");
            SearchResult result = search.FindOne();

            if (result != null)
            {
                return LdapBindAuthenticationErrors.OK;
            }
        }
        catch (DirectoryServicesCOMException c)
        {
            LdapBindAuthenticationErrors ldapBindAuthenticationError = -1;
                    // These LDAP bind error codes are found in the "data" piece (string) of the extended error message we are evaluating, so we use regex to pull that string
                    if (Regex.Match(c.ExtendedErrorMessage, @" data (?<ldapBindAuthenticationError>[a-f0-9]+),").Success)
                    {
                        string errorHexadecimal = match.Groups["ldapBindAuthenticationError"].Value;
                        ldapBindAuthenticationError = (LdapBindAuthenticationErrors)Convert.ToInt32(errorHexadecimal , 16);
                        return ldapBindAuthenticationError;
                    }
            catch (Exception e)
            {
                throw;
            }
        }

        return LdapBindAuthenticationErrors.ERROR_LOGON_FAILURE;
    }

这些是您的“LdapBindAuthenticationErrors”,您可以在 MSDN 中找到更多信息,here

    internal enum LdapBindAuthenticationErrors
    {            
        OK = 0
        ERROR_INVALID_PASSWORD = 0x56,
        ERROR_PASSWORD_RESTRICTION = 0x52D,
        ERROR_LOGON_FAILURE = 0x52e,
        ERROR_ACCOUNT_RESTRICTION = 0x52f,
        ERROR_INVALID_LOGON_HOURS = 0x530,
        ERROR_PASSWORD_EXPIRED = 0x532,
        ERROR_ACCOUNT_DISABLED = 0x533,
        ERROR_ACCOUNT_EXPIRED = 0x701,
        ERROR_PASSWORD_MUST_CHANGE = 0x773,
        ERROR_ACCOUNT_LOCKED_OUT = 0x775
    }

然后你可以使用这个 Enum 的返回类型并在你的控制器中用它做你需要的事情。需要注意的重要一点是,您正在 COMException 的“扩展错误消息”中查找字符串的“数据”部分,因为其中包含您正在寻找的全能错误代码。

祝你好运,我希望这会有所帮助。我测试了它,它对我很有效。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-11-02
    • 2020-08-17
    • 2014-11-21
    • 1970-01-01
    • 1970-01-01
    • 2021-12-26
    • 1970-01-01
    相关资源
    最近更新 更多