Calling NetValidatePasswordPolicy with C # always returns the password that needs to be changed

We have an mvc application that uses Active Directory to authenticate our users. We use System.DirectoryServices and using PricipalContext for authentication:

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

However, this method returns only bool, and we want to return better messages or even redirect the user to the reset password for such instances as:

  • The user is locked out of his account.
  • User password has expired.
  • The user must change his password at the next login.

So, if the user cannot log in, we call NetValidatePasswordPolicy to find out why the user was unable to log in. It seemed to work fine, but we realized that this method returned NET_API_STATUS.NERR_PasswordMustChange no matter what state the Active Directory user is.

The only example I found with the same problem is the Sublime Speech plugin here . The code I use is as follows:

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

The code always succeeds, so why is the value of outputArgs.ValidationStatus equal to one result each time, regardless of the state of the Active Directory user?

+7
c # interop com active-directory
source share
1 answer

I will debug the answer to this question in three different sections:

  • Current issue with your methodology
  • Problems with recommended solutions both on the Internet and in this topic
  • Decision

Current issue with your methodology.

NetValidatePasswordPolicy requires that its InputArgs parameter accept a pointer to a structure, and the structure you pass in depends on your ValidationType . In this case, you pass NET_VALIDATE_PASSWORD_TYPE.NetValidateAuthentication , which requires InputArgs NET_VALIDATE_AUTHENTICATION_INPUT_ARG , but you pass a pointer to NET_VALIDATE_PASSWORD_CHANGE_INPUT_ARG .

In addition, you are trying to assign a type of currentPassword type to the structure NET_VALIDATE_PASSWORD_CHANGE_INPUT_ARG .

However, there is a big fundamental problem with using NetValidatePasswordPolicy , and you are trying to use this function to check passwords in Active Directory, but that’s not what it is used for. NetValidatePasswordPolicy used to allow applications to check for the authentication database provided by the application.

More information about NetValidatePasswordPolicy here .

Problems with recommended solutions both on the Internet and in this thread

Various articles on the Internet recommend using the LogonUser function found in AdvApi32.dll , but this implementation has its own set of problems:

Firstly, LogonUser checks the local cache, which means that you will not get immediate accurate account information unless you use the "Network" mode.

Secondly, the use of LogonUser in a web application, in my opinion, is a bit hacky, as it is intended for desktop applications running on client machines. However, given the limitations provided by Microsoft, if LogonUser gives the desired results, I don’t understand why it should not be used - a ban on caching problems.

Another problem with LogonUser is that how well it works for your use case depends on how your server is configured, for example: There are certain permissions that must be enabled in the domain that you authenticate against this need. to work in the "Network" mode.

Read more about LogonUser here .

In addition, GetLastError() should not be used; instead, GetLastWin32Error() should be used, since it is unsafe to use GetLastError() .

Read more about GetLastWin32Error() here .

Decision.

To get the exact error code from Active Directory, without any caching problems and directly from directory services, this is what you need to do: rely on COMException returning from AD when an account problem occurs, because ultimately, mistakes are what you are looking for.

First, here you raise an error from Active Directory when authenticating the current username and password:

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

And these are your "LdapBindAuthenticationErrors", you can find more on 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 } 

Then you can use the return type of this Enum and do what you need with it in your controller. It is important to note that you are looking for a piece of the “data” line in the “Extended error message” of your COMException , because it contains the pop-up error code you are looking for.

Good luck and I hope this helps. I tested it and it works great for me.

+6
source share

All Articles