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

joh*_*oop 7 c# com interop active-directory

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

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

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

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

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

我有这个同样的问题,发现的唯一的例子来自崇高的语音插件在这里.我使用的代码如下:

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
    }   
}
Run Code Online (Sandbox Code Playgroud)

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

cre*_*low 7

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

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

您方法的当前问题.

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

此外,您试图为结构分配"currentPassword"类型的值NET_VALIDATE_PASSWORD_CHANGE_INPUT_ARG.

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

有关于NetValidatePasswordPolicy 此处的更多信息.

在线和此线程中推荐的解决方案的问题

在线推荐的各种文章都使用了这个实现LogonUser,AdvApi32.dll但是这个实现带有一系列问题:

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

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

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

有关LogonUser 此处的更多信息.

此外,GetLastError()不应该使用,GetLastWin32Error()应该使用,因为它是不安全的使用GetLastError().

有关GetLastWin32Error() 此处的更多信息.

解决方案.

为了从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;
    }
Run Code Online (Sandbox Code Playgroud)

这些是你的"LdapBindAuthenticationErrors",你可以在MSDN中找到更多,这里.

    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
    }
Run Code Online (Sandbox Code Playgroud)

然后,您可以使用此枚举的返回类型,并在控制器中执行所需的操作.需要注意的重要一点是,您正在寻找您的"扩展错误消息"中字符串的"数据"部分,COMException因为它包含您正在寻找的全能错误代码.

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