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 10
  • 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 DirectoryConnection (not necessary and may cause unexpected behavior when sending DirectoryControls)

    • Extended error handling to differentiate the return codes

7/29/2014

Updated .Net S.DS architecture illustration:

    • S.DS.AM wraps S.DS.P calls as well
    • S.DS.Pwraps DNS API calls as well

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-

  • Hi,

    Just a general question about password history. At change password, I see the benefit of validating the password history. But at a reset , is there any benefit to compare the history?

    In a website , where a reset email is sent for the user to rest the password with some security questions, what is the benefit of comparing password history? It might give the user more information about a possible password or a bad user experience (frustrating to enter many passwords, if I look as last 8).

    Any help greatly appreciated.

    Best Regards
    Indu

  • Hi Mike,

    I implemented this solution to enforepasswordHistory as true, and the installation hotfix and even testing you code i can still set the password which was in past history.

    Please adivce

    Regards
    Shashi

  • Did any one of you guys had this enforcePasswordHistory working ?

    Regards
    Shashi

  • @ Indu Ganti
    Just imagine these scenarios:

    A) You have a custom NetworkProvider which allows the user to reset his password by himself after answering some security questions without logging in

    B) you have set up a SelfService-PWDReset-Website that a user can access within a login session from any colleague where he can reset his pwd afte answering some security questions.

    In both cases the user would be able to pretend having forgotten his pwd and reset the pwd to the one he is currently using if you do not check pwd history - and that's for eternity. This will break your PWD policy implementation in your AD.

    A real life (not joking) scenario:
    Helpdesk uses an Identitymanagement WebSIte to reset pwds.
    Since there are pwd complexity rules in place the Helpdesk always uses the same pwd for reset to match the complexity rules - let's say "Start$12".
    If a user had to get his pwd resettet several times he will find out that the new pwd will always be the same ->
    He just calls the helpdesk, pretending to be another user who is on holidays. Then he waits for the callbackand answers with "Sorry colleague not here in the moment - please try later".
    Now he knows the new pwd of the other user, log's in as the user, set's a new pwd an can do whatever he want's as this user.

    Hth.

    If you have any concerns do not hesitat to come back.

    Michael

    PFE | Have keyboard. Will travel.

  • @ Shashi
    I got feedback from several people who implemented this successfully (like Mohammad above).
    Just retested it in W2K8 R2, W2K8 R2 Sp1, W2K12 and W2K12 R2 domains with success.

    Some checks:

    Did you check the presence of the control "1.2.840.113556.1.4.2066" via ldp.exe -> Connect -> supportedControls attribute?

    When debugging your code - does the following code path get hit?
    // 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);

    Did you run the assembly elevated?

    If you need more support - don't hesitate to come back :-)

    Michael

    PFE | Have keyboard. Will travel.

  • I am trying to implement reset password functionality for accounts in Windows 2012 R2 AD LDS via java ldap api. But it is not honoring password history constraint. When I tried to implement change password it is enforcing password history. I am using the following code to reset password.

    @Override
    public void updatePassword(String password) throws LdapException {
    try {
    String quotedPassword = "\"" + password + "\"";
    char unicodePwd[] = quotedPassword.toCharArray();
    byte pwdArray[] = new byte[unicodePwd.length * 2];
    for (int i=0; i pwdArray[i*2 + 1] = (byte) (unicodePwd[i] >>> 8);
    pwdArray[i*2 + 0] = (byte) (unicodePwd[i] & 0xff);
    }
    ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.REPLACE_ATTRIBUTE,new BasicAttribute("UnicodePwd", pwdArray))};
    LdapContext ldapContext = (LdapContext)ldapTemplate.getContextSource().getReadWriteContext();

    final byte[] controlData = {48,(byte)132,0,0,0,3,2,1,1};
    BasicControl[] controls = new BasicControl[1];
    final String LDAP_SERVER_POLICY_HINTS_OID = "1.2.840.113556.1.4.2239";
    controls[0] = new BasicControl(LDAP_SERVER_POLICY_HINTS_OID, true, controlData);
    ldapContext.setRequestControls(controls);

    ldapContext.modifyAttributes(getRelativeDistinguishedName(), mods);
    } catch (Exception e) {
    throw new LdapException("Failed to update password for:" + this.getDistinguishedName(), e);
    }
    }
    Please let me know if I am doing anything wrong.

  • Hi,
    two things to check here:

    1. open ldp.exe on a DC in your AD and - you will see the rootDSE attributes returned.
    In supportedControls attribute you'll find a list of all control OIDS supported on this DC - check whether you have 1.2.840.113556.1.4.2239. If not check for presence of 1.2.840.113556.1.4.2066. Use the one you find.
    If none of the two is present - you should install http://support.microsoft.com/?id=2386717 - this will introduce 1.2.840.113556.1.4.2066 OID to the DC.

    2. a BasicControl in Java LDAP implementation contains the string OID (correct), boolean critical (could be false or true - correct), byte[] value (should be berencoded from '1' -> 0x30 0x84 0x00 0x00 0x00 0x03 0x02 0x01 0x01 -> correct).
    What I'm actually missing in the BasicControl in Java as corresponding class to DirectoryControl in .Net is the serverSide switch - this control must be declared serverside otherwise the control will not be recognized correctly by NTDS.

    Do you get any exception - and if yes - what does it tell you?

    All the best
    Michael

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