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!
