Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling NetValidatePasswordPolicy from C# always returns Password Must Change

We have a mvc application that is using Active Directory to authenticate our users. We are leveraging System.DirectoryServices and using the PricipalContext to authenticate:

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

However this method only returns a bool and we want to return better messages or even redirect the user to a password reset screen for instances like:

  1. The user is locked out of their account.
  2. The users password is expired.
  3. The user needs to change their password at next login.

So if the user fails to login we call NetValidatePasswordPolicy to see why the user was not able to log in. This seemed to work well but we realized that this method was only returning NET_API_STATUS.NERR_PasswordMustChange no matter what the state of the Active Directory user was.

The only example I have found with this same problem comes from a Sublime Speech plugin here. The code I am using 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 the same result every time regardless of the state of the Active Directory user?

like image 536
johnnywhoop Avatar asked Feb 19 '15 19:02

johnnywhoop


1 Answers

I will break the answer to this question into three different sections:

  1. The Current Problem With Your Methodology
  2. The Issues With Recommended Solutions both Online, and in this Thread
  3. The Solution

The current problem with your methodology.

NetValidatePasswordPolicy requires its InputArgs parameter to take in a pointer to a structure, and the structure you pass in depend on the ValidationType your're passing in. In this case, you are passing NET_VALIDATE_PASSWORD_TYPE.NetValidateAuthentication, which requires an InputArgs of NET_VALIDATE_AUTHENTICATION_INPUT_ARG but you're passing in a pointer to NET_VALIDATE_PASSWORD_CHANGE_INPUT_ARG.

Furthermore, you are attempting to assign a "currentPassword' type of value to the NET_VALIDATE_PASSWORD_CHANGE_INPUT_ARG structure.

However, there's a bigger fundamental proble to the use of NetValidatePasswordPolicy and that is that you are trying to use this function to validate passwords in Active Directory, but this is not what it is used for. NetValidatePasswordPolicy is used to allow applications to validate against a authentication database provided by the application.

There's more information about NetValidatePasswordPolicy here.

The issues with recommended solutions both online, and in this thread

Various articles online recommend using the LogonUser function found in AdvApi32.dll but this implementation carries its own set of issues:

The first is that LogonUser validates against a local cache, and that means that you will not get immediate accurate information about the account, unless you use the "Network" mode.

The second is that using LogonUser on a Web application, in my opinion is a bit hacky, as it is designed for desktop applications running on client machines. However, considering the limitations provided Microsoft if LogonUser gives desired results, I don't see why it shouldn't be used - barring the caching issues.

Another issue with LogonUser is that how well it works for your use case depends on how your server is configured, for example: There are some particular permissions that need to be enabled on the domain you're authenticating against that need to be in place for 'Network' logon type to work.

More information about LogonUser here.

Also, GetLastError() should not be used, GetLastWin32Error() should be used instead, as it is not safe to use GetLastError().

More information about GetLastWin32Error() here.

The solution.

In order to get an accurate error code from Active Directory, without any caching issues and straight from directory services, this is what needs to be done: rely on COMException coming back from AD when there's an issue with the account, because ultimately, errors is what you're looking for.

First, here's how you trigger an error from Active Directory on authentication of a current user name and a 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 in 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. The important thing to note, is that you're looking for the "data" piece of the string in the "Extended Error Message" of your COMException because this contains the almighty error code you are hunting for.

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

like image 131
creativityoverflow Avatar answered Sep 29 '22 07:09

creativityoverflow