从C#调用NetValidatePasswordPolicy始终返回密码必须更改

时间:2015-02-19 19:04:27

标签: c# com active-directory interop

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

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

然而,此方法仅返回bool,我们希望返回更好的消息,甚至将用户重定向到密码重置屏幕,例如:

  1. 用户被锁定在帐户外。
  2. 用户密码已过期。
  3. 用户需要在下次登录时更改密码。
  4. 因此,如果用户登录失败,我们会调用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
        }   
    }
    

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

1 个答案:

答案 0 :(得分:7)

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

  1. 当前的方法论问题
  2. 在线和本主题中推荐解决方案的问题
  3. 解决方案
  4. 您方法的当前问题。

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

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

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

    有关NetValidatePasswordPolicy here的更多信息。

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

    各种在线文章建议使用LogonUser中的AdvApi32.dll函数,但此实现带有一系列问题:

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

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

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

    有关LogonUser here的更多信息。

    此外,不应使用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
        }
    

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

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