Coding from the field

Info series to share interesting findings or projects regarding platforms / infrastructure coding.

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

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

  • Comments 3
  • Likes

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
   
{
               string dn = "CN=TheCN,OU=TheOU,DC=contoso,DC=com";

               /* 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();

                   // change pwd
                   PasswordChanger(ldapCon,
                                             dn,
                                             pwdDepricate: @"0ldPa55W0rd",
                                             pwdSet: @"N3wPa55W0rdH15t0ryT3st");

                   // reset pwd without utilizing pwd history
                   PasswordChanger(ldapCon,
                                             dn,
                                             pwdSet: @"N3wP@55W0rdH15t0ryT3st");

                   // reset pwd utilizing pwd history
                   PasswordChanger(ldapCon,
                                             dn,
                                             pwdSet: @"N3wPa55W0rdH15t0ryT3st,
                                             enforceHistory: true);
                }
      }

    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="ldapCon">established LdapConnection</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(LdapConnection ldapCon,
                                                           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 LDAPConnection and receive the response */
                   DirectoryResponse drResult = ldapCon.SendRequest(mrCall);

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

               catch (DirectoryOperationException doex)
                  {
                     if (doex.Response.ResultCode == ResultCode.UnwillingToPerform)
                     { Console.WriteLine("Pwd violates pwd-history: {0}", doex.Response.ErrorMessage); }

                     else if (doex.Response.ResultCode == ResultCode.ConstraintViolation)
                     { Console.WriteLine("Pwd constraints: {0}", doex.Response.ErrorMessage); }

                     else 
                     { 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.


3/31/2014

Since Windows Server 2012 we do have a new OID for the Extended Control LDAP_SERVER_POLICY_HINTS_OID = 1.2.840.113556.1.4.2239. The OID in the code above is still valid on Windows Server 2012 / R2 ADs but it's now called LDAP_SERVER_POLICY_HINTS_DEPRECATED_OID.

Suggest you check the supportedControls attribute of a rootDSE call and check, whether you find LDAP_SERVER_POLICY_HINTS_OID = 1.2.840.113556.1.4.2239. If so you should use the new OID.

Updates:

    • Removed casting from LdapConnection to DirecotryConnection (not necessary and may cause unexpected behavior when sending DirectoryControls)

    • Extended error handling to differentiate the return codes

All the best

Michael

PFE | Have keyboard. Will travel.

Comments
  • Hi Mike,

    Thank you so much for this post. We follow the step you provided but unfortunately I stuck with an error @  DirectoryResponse drResult = dcCon.SendRequest(mrCall);

    "The object does not exist." and error message as

    "0000208D: NameErr: DSID-03100213, problem 2001 (NO_OBJECT), data 0, best match of:

    'DC=abc,DC=com'"

    Please guide me to resolve this issue.

    Regards

    Danish-

  • Hi,

    thx for your posting. The error you are facing should only be raised when the path (distinguishedName) to the user object could not be found.

    "best match of: 'DC=abc,DC=com'" indicates that the ou / container in the path just before DC=abc,DC=com does not exist.

    Example:

    correct distinguishedName:

         CN=Administrator,CN=Users,DC=abc,DC=com

    mistyped:

         CN=Administrator,OU=Users,DC=abc,DC=com

    saying OU=Users does not exist - it's CN=Users -> this will throw the error you've seen.

    Hope this helps - if not - do not hesitate to come back.

    All the best

    Michael

    PFE | Have keyboard. Will travel.

  • Thank you so much for the reply. It helped and issue is now resolved by passing the correct distinguished name.

    Thanks again your post, it really helped me to solve the issue which has been pending for so long.

    Regards

    Danish-

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment