Resetting an AD DS password and honouring password history and age using the LDAP_SERVER_POLICY_HINTS control

I recently had to do some frantic experimenting around the area of password reset. I was working with a customer on a convoluted solution that necessitated a password synchronisation operation from the DMZ into a production network without a trust. We had to rule out the use of Password Change Notification Service (PCNS) as there was no way we were going to place the FIM Synchronization Service in the DMZ and we weren’t allowed to use a trust. However, I digress. Why I’m telling you this is to introduce the fact that I wrote a proof-of-concept (PoC) web site and web service that resets passwords across the no-trust-void (in a reasonably secure manner J) and utilises the new LDAP_SERVER_POLICY_HINTS_OID control to allow the password set operation to fully honour password policy. If you didn’t know an [administrative] password set operation bypasses password history and age. Well, to cut a long story short the FIM PG raised a DCR with the AD DS PG to allow this and the result is kb2386717 (a hotfix, which is part of Windows Server 2008 R2 Service Pack 1).

Why I’m waffling on about all this is because a colleague asked me to post the C# sample code that makes use of the new control and it’s indirectly related to the subjects discussed in a previous post.

So, let’s take some code written by far superior developers than I – Joe Kaplan and Ryan Dunn: http://directoryprogramming.net/files/3/csharp/entry24.aspx.

Download Joe and Ryan’s code samples and look at “Listing 10.16 Modified”. This is a class that sets or changes AD user passwords using the System.DirectoryServices.Protocols (S.DS.P) namespace.

OK, bear with me. I learned to program using Java 2 (1.1, 1.2 and 1.3) in 1999 and 2000 and haven’t done much since –I’m a scripter not a programmer. I’ve “overloaded” Joe and Ryan’s code with a new method to allow the original use of the method and permit the new use.

The following code modifies the great work published by Joe and Ryan and illustrates how to utilise the new LDAP extended control:

        /// <summary>
        /// Set password securely resets an LDAP object password using the Unicode Password "operation".
        /// </summary>
        /// <param name="userDN">The distinguished name of the target object, usually a user or inetOrgPerson object.</param>
        /// <param name="password">The string representation of the new password.</param>
        public void SetPassword(String userDN, String password)
        {
            this.SetPassword(userDN, password, false);
        }

        /// <summary>
        /// Set password securely resets an LDAP object password using the Unicode Password "operation".
        /// </summary>
        /// <param name="userDN">The distinguished name of the target object, usually a user or inetOrgPerson object.</param>
        /// <param name="password">The string representation of the new password.</param>
        /// <param name="enforcePasswordHistory">Whether or not to use the Windows Server 2008 R2 SP1 POLICY_HINTS LDAP
        /// control and allow the reset operation to honour password age and history.</param>
        /// <returns>[ResultCode] operation status/indicator.</returns>
        public ResultCode SetPassword(String userDN, String password, Boolean enforcePasswordHistory)
        //public void SetPassword(String userDN, String password, Boolean enforcePasswordHistory)
        {
            DirectoryAttributeModification pwdMod = new DirectoryAttributeModification();
            pwdMod.Name = "unicodePwd";
            pwdMod.Add(GetPasswordData(password));
            pwdMod.Operation = DirectoryAttributeOperation.Replace;

            ModifyRequest request = new ModifyRequest(userDN, pwdMod);

            if (enforcePasswordHistory)
            {
                if (ValidateCapabilities.LdapControlSupported(_connection, AddsSupportedControls.LDAP_SERVER_POLICY_HINTS_OID))
                {
                    byte[] ctrlData = BerConverter.Encode("{i}", new Object[] { 1 });
                    DirectoryControl LDAP_SERVER_POLICY_HINTS_OID = new DirectoryControl(
                            AddsSupportedControls.LDAP_SERVER_POLICY_HINTS_OID, //"1.2.840.113556.1.4.2066"
                            ctrlData,
                            true,
                            true
                        );

                    request.Controls.Add(LDAP_SERVER_POLICY_HINTS_OID);
                }
                else
                {
                    throw new LdapException("The connected directory server does not support the LDAP_SERVER_POLICY_HINTS extended control.  Unable to reset the object's password using this extension.");
                }
            }

            DirectoryResponse response = _connection.SendRequest(request);
            return response.ResultCode;
        }

And that’s it basically. My snippet above has a couple of changes –I return the ResultCode (this was for my web service) and I’ve made use of some simple utility methods I wrote to validate whether or not the capability is supported (1) and a pseudo-enumeration of valid controls (2):

(1) LdapControlSupported

        /// <summary>
        /// Ascertain whether or not the connected directory server supports a given LDAP control.
        /// The supportedControl attribute of the RootDSE object is compared against the input OID.
        /// </summary>
        /// <param name="connection">An LDAP connection.</param>
        /// <param name="control">String representation of the LDAP control OID.</param>
        /// <returns>True if the DS advertises the control.  False if not.</returns>
        public static Boolean LdapControlSupported(LdapConnection connection, String control)
        {
            String supportedControl = "supportedControl";
            SearchRequest dseSupportedControl = LdapSearchHelper.RootDSE(new String[] { supportedControl });

            return LdapSearchHelper.CompareAttributeValue(
                    connection,
                    dseSupportedControl,
                    supportedControl,
                    control
                );
        }

(2) AddsSupportedControls

    /// <summary>
    /// An "enumeration" of LDAP Supported Controls that can be held by an Active Direcory Domain
    /// Services (AD DS) domain controller (DC) or an Active Directory Lightweight Directory Services
    /// (AD LDS) directory server (DS).
    /// </summary>
    public class AddsSupportedControls
    {
        public const String LDAP_PAGED_RESULT_OID_STRING = "1.2.840.113556.1.4.319";
        public const String LDAP_SERVER_CROSSDOM_MOVE_TARGET_OID = "1.2.840.113556.1.4.521";
        public const String LDAP_SERVER_DIRSYNC_OID = "1.2.840.113556.1.4.841";
        public const String LDAP_SERVER_DOMAIN_SCOPE_OID = "1.2.840.113556.1.4.1339";
        public const String LDAP_SERVER_EXTENDED_DN_OID = "1.2.840.113556.1.4.529";
        public const String LDAP_SERVER_GET_STATS_OID = "1.2.840.113556.1.4.970";
        public const String LDAP_SERVER_LAZY_COMMIT_OID = "1.2.840.113556.1.4.619";
        public const String LDAP_SERVER_PERMISSIVE_MODIFY_OID = "1.2.840.113556.1.4.1413";
        public const String LDAP_SERVER_NOTIFICATION_OID = "1.2.840.113556.1.4.528";
        public const String LDAP_SERVER_RESP_SORT_OID = "1.2.840.113556.1.4.474";
        public const String LDAP_SERVER_SD_FLAGS_OID = "1.2.840.113556.1.4.801";
        public const String LDAP_SERVER_SEARCH_OPTIONS_OID = "1.2.840.113556.1.4.1340";
        public const String LDAP_SERVER_SORT_OID = "1.2.840.113556.1.4.473";
        public const String LDAP_SERVER_SHOW_DELETED_OID = "1.2.840.113556.1.4.417";
        public const String LDAP_SERVER_TREE_DELETE_OID = "1.2.840.113556.1.4.805";
        public const String LDAP_SERVER_VERIFY_NAME_OID = "1.2.840.113556.1.4.1338";
        public const String LDAP_CONTROL_VLVREQUEST = "2.16.840.1.113730.3.4.9";
        public const String LDAP_CONTROL_VLVRESPONSE = "2.16.840.1.113730.3.4.10";
        public const String LDAP_SERVER_ASQ_OID = "1.2.840.113556.1.4.1504";
        public const String LDAP_SERVER_QUOTA_CONTROL_OID = "1.2.840.113556.1.4.1852";
        public const String LDAP_SERVER_RANGE_OPTION_OID = "1.2.840.113556.1.4.802";
        public const String LDAP_SERVER_SHUTDOWN_NOTIFY_OID = "1.2.840.113556.1.4.1907";
        public const String LDAP_SERVER_FORCE_UPDATE_OID = "1.2.840.113556.1.4.1974";
        public const String LDAP_SERVER_RANGE_RETRIEVAL_NOERR_OID = "1.2.840.113556.1.4.1948";
        public const String LDAP_SERVER_RODC_DCPROMO_OID = "1.2.840.113556.1.4.1341";
        public const String LDAP_SERVER_INPUT_DN_OID = "1.2.840.113556.1.4.2026";
        public const String LDAP_SERVER_SHOW_DEACTIVATED_LINK_OID = "1.2.840.113556.1.4.2065";
        public const String LDAP_SERVER_SHOW_RECYCLED_OID = "1.2.840.113556.1.4.2064";
        public const String LDAP_SERVER_POLICY_HINTS_OID = "1.2.840.113556.1.4.2066";
    }

(3) CompareAttributeValue (used in (1))

        public static Boolean CompareAttributeValue(LdapConnection connection,
            SearchRequest request, String attributeName, String attributeValue)
        {
            String[] result = LdapSearchHelper.GetSingleAttributeValue(connection, request) as String[];

            if (result != null)
            {
                foreach (String attrVal in result)
                {
                    if (attrVal == attributeValue)
                    {
                        return true;
                    }
                }
            }

            return false;
        }
    }

(4) GetSingleAttributeValue (used in (3))

        public static Object GetSingleAttributeValue(LdapConnection connection, SearchRequest request)
        {
            Object returnValue = null;
            SearchResponse response = LdapSearchHelper.LdapQuery(connection, request);
            if (response.Entries != null)
            {
                String attrName = request.Attributes[0];
                if (response.Entries[0] != null)
                {
                    SearchResultEntry res = response.Entries[0];
                    if (res.Attributes[attrName] != null) //cater for invalid attribute
                    {
                        String[] attrValues = res.Attributes[attrName].GetValues(typeof(String)) as String[];
                        if (attrValues.Length == 1)
                        {
                            returnValue = attrValues[0];
                        }
                        else
                        {
                            returnValue = attrValues;
                        }
                    }
                    else
                    {
                        throw new LdapException("Invalid attribute name specified (or insufficient permissions).");
                    }
                }
            }

            return returnValue;
        }

Wrap-up

The crux of this post, I hope, is this: you can now, via the LDAP_POLICY_HINTS_OID control reset a user password and honour all aspects of password history.  Programatically, the extended control is implemented as follows.  Using this control requires either Windows Server 2008 R2 Service Pack 1 or the kb2386717 hotfix for either Windows Server 2008 or Windows Server 2008 R2.

byte[] ctrlData = BerConverter.Encode("{i}", new Object[] { 1 });
DirectoryControl LDAP_SERVER_POLICY_HINTS_OID = new DirectoryControl(
 AddsSupportedControls.LDAP_SERVER_POLICY_HINTS_OID, //"1.2.840.113556.1.4.2066"
 ctrlData,
 true,
 true
);

request.Controls.Add(LDAP_SERVER_POLICY_HINTS_OID);

Hopefully this helps someone out there. Be warned the error message when you don’t meet complexity isn’t immediately apparent!

Advertisements

About Paul Williams

IT consultant working for Microsoft specialising in Identity Management and Directory Services.
This entry was posted in Active Directory, Programming and tagged , , , , , , , , . Bookmark the permalink.

10 Responses to Resetting an AD DS password and honouring password history and age using the LDAP_SERVER_POLICY_HINTS control

  1. John Morland says:

    Hi Paul,

    I have used your code above. However, I can’t get it working as it still allows me to change the password with the current or old passwords. Are there any AD LDS configurations I should set to enable it ?
    I’m using Win2k8 R2 SP1.

    Regards,

    John

    • HI John, I only tested the code on AD DS. I’m not in a position to check now, but does your AD LDS instance support the LDAP_SERVER_POLICY_HINTS_OID control? You can verify by connecting to RootDSE using, for example, LDP and inspecting the values of the supportedControl attribute. You’ll see the OID and LDP will provide a display name, in brackets, of POLICY_HINTS.

      If your instance supports POLICY_HINTS then you should check the password policy that applies to the computer that hosts the AD LDS instance. The domain policy (the GPO linked to the domain that the DCs process) might not be in effect on your AD LDS instance, as the password policy is taken from the host which will apply the most appropriate policy based on GPO scope.

      So, on your AD LDS instance run RSOP.MSC and take a peek at Computer Configuration | Policies | Windows Settings | Security Settings | Account Policies/Password Policy. When I test I usually turn minimum age off and have a password history of 24 (the default IIRC).

  2. mhddanish says:

    Hi, I am not able to pen the link http://directoryprogramming.net/files/3/csharp/entry24.aspx.
    Please send me some other link for “resetting the AD password honouring the history policy”.
    Thanks
    Danish-

    • I spoke to Joe Kaplan and he’s given me permission to post the sample in question here as the directoryprogramming.net site won’t be back online for a while.

      Excuse the formatting. Here’s the original listing from the book. I modified this as per the post you have been reading.

      using System;
      using System.Collections.Generic;
      using System.Text;
      using System.DirectoryServices.Protocols;
      using System.Net;

      namespace DotNetDevGuide.DirectoryServices.Chapter10
      {
      ///

      /// Listing 10.16 Modified
      ///

      public class LdapPasswordModifier : IDisposable
      {
      LdapConnection _connection;

      public LdapPasswordModifier(string server, NetworkCredential creds, bool useSSL)
      {
      _connection = CreateConnection(server, creds, useSSL);
      _connection.Bind();
      }

      private LdapConnection CreateConnection(
      string server,
      NetworkCredential credential,
      bool useSsl
      )
      {
      LdapConnection connect = new LdapConnection(
      new LdapDirectoryIdentifier(server),
      credential
      );

      connect.SessionOptions.ProtocolVersion = 3;

      if (useSsl)
      {
      connect.SessionOptions.SecureSocketLayer = true;
      connect.AuthType = AuthType.Basic;
      }
      else
      {
      connect.SessionOptions.Signing = true;
      connect.SessionOptions.Sealing = true;
      connect.AuthType = AuthType.Negotiate;
      }

      return connect;
      }

      public void ChangePassword(
      string userDN,
      string oldPassword,
      string newPassword
      )
      {
      DirectoryAttributeModification deleteMod =
      new DirectoryAttributeModification();

      deleteMod.Name = “unicodePwd”;
      deleteMod.Add(GetPasswordData(oldPassword));
      deleteMod.Operation = DirectoryAttributeOperation.Delete;

      DirectoryAttributeModification addMod =
      new DirectoryAttributeModification();

      addMod.Name = “unicodePwd”;
      addMod.Add(GetPasswordData(newPassword));
      addMod.Operation = DirectoryAttributeOperation.Add;

      ModifyRequest request = new ModifyRequest(
      userDN,
      deleteMod,
      addMod
      );

      DirectoryResponse response = _connection.SendRequest(request);
      }

      public void SetPassword(
      string userDN,
      string password
      )
      {
      DirectoryAttributeModification pwdMod =
      new DirectoryAttributeModification();

      pwdMod.Name = “unicodePwd”;
      pwdMod.Add(GetPasswordData(password));
      pwdMod.Operation = DirectoryAttributeOperation.Replace;

      ModifyRequest request = new ModifyRequest(
      userDN,
      pwdMod
      );

      DirectoryResponse response =
      _connection.SendRequest(request);
      }

      private byte[] GetPasswordData(string password)
      {
      string formattedPassword;
      formattedPassword = String.Format(“\”{0}\””, password);
      return (Encoding.Unicode.GetBytes(formattedPassword));
      }

      #region IDisposable Members

      public void Dispose()
      {
      if (_connection != null)
      {
      _connection.Dispose();
      }
      }

      #endregion
      }
      }

  3. ErPe says:

    Hey,

    Great article ! I have been presented with similar problem of syncing password across untrusted “realms”. what I did I compiled and customized a bit the password filter from http://sourceforge.net/projects/passwdhk/ – so it works on 2008 R2 SP1 x64

    Then I set up own webservices that do the sync with SQL DB in the backend to avoid looping

    Every time password is grabbed I call the webservices. Now problem Im seeing is the password history.

    Your way to change password with respecting password history seems to be the answer. However much more better would be somehow to check if the password has been already used 😦

    Thanks !
    R

  4. Pingback: Enforcing Active Directory password history when resetting passwords using PHP | my blog

  5. Howdy,

    Probably a dumb question, but what does your “LdapSearchHelper” class look like? Thanks!

    -kb

    • I’m afraid I cannot find the class! This was done some time ago and the VMs have long gone. I’ll have another look in an old backup and will post back if I can find it.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s