Todays topic:

Resetting passwords honoring password history
(or what's happening under the hood when changing / resetting passwords)

You may have already came across the task to programmatically change or reset passwords on user accounts in Active Directory. Thanks to the the ChangePassword() and SetPassword() macros of the Active Directory Service Interface (ADSI) implementation this is an easy and straight forward coding and in most cases you need not take care about what's happening on the Domain Controller performing the password handling for you.
Anyhow it still may come in handy knowing how this is processed from the Active Directory service (NTDS) on a DC – especially when we want to accomplish what's mentioned in the headline (Resetting passwords honoring password history).

First of all – and for the sake of completeness – let's list the usage of the two ADSI macros ChangePassword() and SetPassword() in VBS and .Net (note –.Net System.DirectoryService namespace is wrapping ADSI):

VBS:

Set IADsUser = GetObject("LDAP://CN=TheCN,OU=TheOU,DC=contoso,DC=com")

    IADsUser.ChangePassword"0ldPa55W0rd", "N3wPa55W0rd"

        IADsUser.SetPassword"N3wPa55W0rd"

.Net (System.DirectoryServices):

DirectoryEntry IADsUser = new DirectoryEntry("LDAP://CN=TheCN,OU=TheOU,DC=contoso,DC=com");

                         IADsUser.Invoke("ChangePassword", new object[] { "0ldPa55W0rd", "N3wPa55W0rd" });

                         IADsUser.Invoke("SetPassword", new object[] { "N3wPa55W0rd" });


Now let's have a look at what's actually happening when calling the ADSI macros:

The password change or reset call is actually an attribute modification request against the unicodePwd attribute of the user account which requests the NTDS service to handle the incoming modification request appropriately (note – the password is NOT stored in this attribute).


ChangePassword():

When changing the password we send a modification request to our directory connection, that was established against a domain controller when 'connecting' the user object, that contains:

  • the distinguishedName (internal directory path - in our sample "CN=TheCN,OU=TheOU,DC=contoso,DC=com")
  • the name of the attribute to modify (unicodePwd)
  • a delete attribute value modification containing the old password
  • an add attribute value modification containing the new password

When the modification request arrives at the domain controller the NTDS service does the following:

  • check whether the hash of the old password to be deleted is in the list of remembered password hashes (this is the verification part of the old password) -> if so proceed, if not return an error "password incorrect"
  • check whether the new password meets password policy rules (like comlpexity, password history, password length) -> if so proceed, if not return error "passwort does not meet pwd complexity rules"

Thus we see - the password rule checks are done when performing an add operation to the unicodPwd attribute.


SetPassword():

Resetting the password sends a modification request to our directory connection containing:

  • the distinguishedName (internal directory path - in our sample "CN=TheCN,OU=TheOU,DC=contoso,DC=com")
  • the name of the attribute to modify (unicodePwd)
  • a replace attribute value modification containing the new password

The only password rule checks that are done while proceeding a replace operation are password complexity and password length – password history is not checked here. Why? Because we only check the existance of a value while modifying an attribute when deleting or adding values.


Reset password honoring password history:

Knowing the above described functionalities this sounds easy – just send an add operation with the new password -  unfortunately you cannot send an add operation to the unicodePwd attribute without a preceding delete operation (means you have to know the old password). But we have an Identity Management solution in place that should be able to reset passwords and not reuse previously set passwords + the helpdesk will not and should not know the old password – so how can this be achieved?

We have to say good bye to the handy ADSI implementation and code closer to the LDAP APIs (no worries – we will still use managed code!). Since .Net 2.0 we have the namespace System.DirectoryServices.Protocols in place, wrapping the LDAP APIs directly.

See following illustration how the various implementations are talking to LDAP:

 

Here we can perform our modification request ourselves and control what has to be send and how this has to be handled.

When sending requests to a directory connection we can additionally send Extended Controls with the request (find a list of controls here: http://msdn.microsoft.com/en-us/library/cc223320.aspx) – you may know one from LDAP queries when using paged queries. In this case ADSI is sending a search request with the extended control for paged search to the DC.
If you check the list in the above link you will find an Extended Control called LDAP_SERVER_POLICY_HINTS_OID with the following description: "Used with an LDAP operation to enforce password history policies during password set.".

Cool – we have all there what we need – unfortunately not yet. If you check the answer of an UDP call against rootDSE in your domain (ex: ldp.exe -> Connect) you will see a list of OIDs behind the attribute supportedControl – but the POLICY_HINTS OID will be missing – it's not there by default. To enable the usage of this Extended Control you must first introduce the OID and it's usage to the DCs by applying the following hotfix: http://support.microsoft.com/?id=2386717

Having installed the hotfix we are now able to send our modification request containing the the Extended Control LDAP_SERVER_POLICY_HINTS_OID with the value 0x1 to honor password history when resetting passwords.

Sample Code:

class Program
{
   public static void Main()
  
{
      try
   
{
               /* initialize LdapConnection which inherites from DirectoryConnection  -
          * DirectoryConnection cannot be initialized passing a directory to connect to */
               using (LdapConnection ldapCon = new LdapConnection("contoso.com:389"))
                {  
                   // enable Kerberos encryption
                   ldapCon.SessionOptions.Sealing=true;

                   //bind
                   ldapCon.Bind();

                   /* we need a DirectoryConnection to send requests -> cast LDAPConnection to DirectoryConnection */
                   DirectoryConnection dcCon = (DirectoryConnection)ldapCon;

                   // change pwd
                   PasswordChanger(dcCon,
                                   "CN=TheCN,OU=TheOU,DC=contoso,DC=com",
                                   pwdDepricate: @"0ldPa55W0rd",
                                   pwdSet: @"N3wPa55W0rdH15t0ryT3st");

                   // reset pwd without utilizing pwd history
                   PasswordChanger(dcCon,
                                   "CN=TheCN,OU=TheOU,DC=contoso,DC=com",
                                   pwdSet: @"N3wP@55W0rdH15t0ryT3st");

                   // reset pwd utilizing pwd history
                   PasswordChanger(dcCon,
                                   "CN=TheCN,OU=TheOU,DC=contoso,DC=com",
                                    pwdSet: @"N3wPa55W0rdH15t0ryT3st,
                                    enforceHistory: true);

                   // cast DirectoryConnection to IDisposable - we want to dispose it
                   IDisposable idKill = dcCon as IDisposable;

                   // disopse DirectoryConnection
                   if (idKill! = null) idKill.Dispose();
                }
       }

    catch (Exception ex)
   
{ Console.WriteLine(ex.ToString()); }

    Console.WriteLine("Press any key");

       Console.ReadKey();
}

  ///<summary>
/// Change or reset pwds on given object
///</summary>
/
//<param name="dcCon">established DirectoryConnection</param>
///<param name="distinguishedName">path to the object</param>
///<param name="pwdDepricate">when changing pwds - pass the current pwd in here</param>
///<param name="pwdSet">new pwd to be set</param>
///<param name="enforceHistory">when resetting pwd -> should we utilize exetended control
/// for pwd history usage</param>
private static void PasswordChanger(DirectoryConnection dcCon,
                                           string distinguishedName,
                                           string pwdDepricate = null,
                                           string pwdSet = null,
                                           bool enforceHistory = false)

  {
           bool letsgo = false;

           // the 'unicodePWD' attribute is used to handle pwd handling requests
           string attribute ="unicodePwd";

           // our modification control
           DirectoryAttributeModification[] damList = null;

           // the modifiy request
           ModifyRequest mrCall = null;

           //do we have an old and a new pwd -> change pwd
           if (!String.IsNullOrEmpty(pwdDepricate) && !String.IsNullOrEmpty(pwdSet))
            {
               // modification control for the delete operation
               DirectoryAttributeModification damDelete = new DirectoryAttributeModification();

               // attribute to handle
               damDelete.Name=attribute;

               // value to be send with the request
               damDelete.Add(BuildBytePWD(pwdDepricate));

               // this is a delete operation
               damDelete.Operation = DirectoryAttributeOperation.Delete; 

               // modification control for the add operation
               DirectoryAttributeModification damAdd = new DirectoryAttributeModification();

               // attribute to handle
               damAdd.Name = attribute;

               // value to be send with the request
               damAdd.Add(BuildBytePWD(pwdSet));

               // this is an add operation
               damAdd.Operation = DirectoryAttributeOperation.Add;

               // combine modification controls
               damList = new DirectoryAttributeModification[] { damDelete, damAdd };

               // init modify request
               mrCall = new ModifyRequest(distinguishedName, damList);

               // we do have something to handle
               letsgo = true;
            }

           //do we have a pwd to set -> set pwd
           else if (!String.IsNullOrEmpty(pwdSet))
            {
               // modification control for the replace operation
               DirectoryAttributeModification damReplace = new DirectoryAttributeModification();

               // attribute to handle
               damReplace.Name = attribute;

               // value to be send with the request
               damReplace.Add(BuildBytePWD(pwdSet));

               // this is a replace operation
               damReplace.Operation = DirectoryAttributeOperation.Replace;

               // combine modification controls
               damList = new DirectoryAttributeModification[] { damReplace };

               // init modify request
               mrCall = new ModifyRequest(distinguishedName, damList);

               // should we utilize pwd history on the pwd reset?
               if (enforceHistory)
                {
                   // the actual extended control OID
                   string LDAP_SERVER_POLICY_HINTS_OID = "1.2.840.113556.1.4.2066";

                   // build value utilizing berconverter
                   byte[] value = BerConverter.Encode("{i}", newobject[] { 0x1 });

                   // init exetnded control
                   DirectoryControl pwdHistory = new DirectoryControl(LDAP_SERVER_POLICY_HINTS_OID, value, false, true);

                   // add extended control to modify request
                   mrCall.Controls.Add(pwdHistory);
                }

               // we do have something to handle
               letsgo = true;
            }

           // something to be handled?
           if (letsgo)
            {
               try
                {
                   /* send the request into the DirectoryConnection and receive the response */
                   DirectoryResponse drResult = dcCon.SendRequest(mrCall);

                   // display result code
                   Console.WriteLine("Update pwd result: {0}", drResult.ResultCode.ToString());
                }

               catch (DirectoryOperationException doex)
                { Console.WriteLine("Update pwd error: {0}", doex.Response.ErrorMessage); }

               catch (Exception ex)
                { Console.WriteLine("Update pwd error: {0}", ex.Message); }
            }
   }

  ///<summary>
  /// build byte array from string pwd
 
///</summary>
 
///<param name="pwd">pwd string</param>
 
///<returns>byte array</returns>
 
private static byte[] BuildBytePWD(string pwd)
 
{ return (Encoding.Unicode.GetBytes(String.Format("\"{0}\"", pwd))); }

}

Hope you had some fun reading and wish fun with testing.

All the best

Michael

PFE | Have keyboard. Will travel.