Wednesday, January 23, 2013

Enforcing Active Directory password history when resetting passwords using PHP

The code:

$ctrl1 = array(
    // LDAP_SERVER_POLICY_HINTS_OID for Windows 2012 and above
    "oid" => "1.2.840.113556.1.4.2239",
    "value" => sprintf("%c%c%c%c%c", 48, 3, 2, 1, 1));
$ctrl2 = array(
    // LDAP_SERVER_POLICY_HINTS_DEPRECATED_OID for Windows 2008 R2 SP1 and above
    "oid" => "1.2.840.113556.1.4.2066",
    "value" => sprintf("%c%c%c%c%c", 48, 3, 2, 1, 1));
if (!ldap_set_option($ds, LDAP_OPT_SERVER_CONTROLS, array($ctrl1, $ctrl2))) {
    error_log("ERROR: Failed to set server controls");
}

$result = ldap_mod_replace($ds, $dn, $entry);
...


Details:

There are a couple of ways to reset Active Directory passwords using LDAP:
  1. A delete operation on the unicodePwd attribute immediately followed by an add, which is a password change

  2. A modification of the unicodePwd attribute, which is considered an administrative password reset
When doing the latter, password history requirements may not be enforced. Apparently this is expected behavior. The solution is a server control:
  1. First, the control must be available on the server. It's present in Windows Server 2008 R2 Service Pack 1 and above. It can also be installed in 2008 R2 using this hotfix: http://support.microsoft.com/?id=2386717

  2. Next, the client must use the control to tell the AD server to enforce password history requirements.
If you happen to be doing this in PHP, the solution for the second part is the code I've posted. The tricky part was to get the BER encoding for the value correct.

I put both controls (the new one as well as the deprecated one) in my code and also didn't set the iscritical flag (which defaults to false) in order to make the code as flexible as possible.

In case you're looking to implement a solution to reset AD passwords using PHP, you may find these helpful:

14 comments:

  1. it doesn't work...
    no error while ldap_set_option but no result either

    ReplyDelete
  2. Sorry...
    It works..

    ReplyDelete
  3. Glad you were able to get it working! I didn't get a chance to respond to your first comment, but I was going to mention that the code I posted was just one small piece in the puzzle, and the links I posted should provide some help in implementing the remaining pieces. The last link in particular provides a decent working example.

    ReplyDelete
  4. I am trying the first method (User Change password).!
    Using ldap_mod_del then ldap_mod_add gives "No Such Attribute" and "Type Or Value Exists." This isn't surprising to me since unicodePwd needs to exist so del fails and it already exists so add fails. How to go abt this..?? Let me know thanks

    ReplyDelete
    Replies
    1. When doing the first method, you have to provide the old password when deleting and the new password when adding. It seems that it also may be necessary that both operations (the delete and the add) are sent as part of the same request. You can read more here: http://msdn.microsoft.com/en-us/library/cc223248.aspx

      Having said that, this isn't really the scope of this post, which deals with the second method; I've never actually implemented the first method. I did provide additional resources at the bottom of the post for implementing the second method which you may find helpful should you choose to do so.

      Hope that helps!

      Delete
  5. thank you very much, work perfectly.

    ReplyDelete
  6. This comment has been removed by the author.

    ReplyDelete
  7. Hi! It's not working for me. The func "ldap_set_option($ldap_connection, LDAP_OPT_SERVER_CONTROLS, array($ctrl1, $ctrl2))" returns "1". And then when I do "ldap_mod_replace($ldap_connection, $userDn , $userdata)", returns:

    [Wed Nov 13 14:26:11 2013] [error] [client xxx.xxx.xxx.xxx] PHP Warning: ldap_mod_replace(): Modify: Server is unwilling to perform in /opt/apache/htdocs/ad-change-pass/functions.php on line 116, referer: http://yyy.yyy.yyy.yyy/ad-change-pass/index.php

    I'm trying with an account with reset password control and then also with an admin account. But no luck. Any idea? AD is running on Windows server 2008 R2 SP1

    ReplyDelete
    Replies
    1. "Server is unwilling to perform" from my experience means the AD server refused to change the password, normally because the password doesn't meet complexity or history requirements. So in some sense, the code might be working perfectly if you get that error. If you're sure that's not the case, you may have missed something else, such as properly encoding the password. That's outside the scope of this post, but the links I posted at the end may help.

      Delete
  8. I know this is an old thread but am hoping someone is checking and can provide some help. I have built out a password reset that works using ldap_mod_replace. Also ldap_set_option returns 1 so everything appears to be setup properly. Problem is the password history is not being enforced. It is being enforced correctly at the desktop.

    For testing purposes the password policy is applied to a specific AD OU that the test account is a member of, it is not applied to the entire domain.

    Any thoughts?

    ReplyDelete
    Replies
    1. First of all, the code in this post is just a small piece of a much larger process. I do have some PHP functions for working with AD (including changing passwords) here that may help:

      https://github.com/bmaupin/junkpile/blob/master/php/misc/ad-functions.php

      Having said that, there are many things to take into account, including what version of Windows/AD you're using, whether or not the server control is installed, the permissions of the account that's being used to reset the password, etc. If the code I referenced doesn't help, I'd recommend going to stackoverflow.com. There you can ask a question and include more details, sample code, etc.

      Hope that helps!

      Delete
    2. Do you know if the account we use in AD can high too high permissions so it ignores the pwd history? No matter what we do or control we use, it still allows us to update the same password over and over.

      Delete
    3. Unfortunately I don't know.

      My previous comment (https://laviefrugale.blogspot.com/2013/01/enforcing-active-directory-password.html?showComment=1488044254018#c3725922431688325484) still sums up most of what I'd say.

      I would add one addition; while stackoverflow.com is a great resource for the programming side of things, serverfault.com is another great resource for the system administration side of this. If you get stuck, I'd recommend asking on one or both of those sites.

      Delete
  9. Thanks alot, it worked for me. I just put the first one..

    $ctrl1 = array(
    "oid" => "1.2.840.113556.1.4.2239",
    "value" => sprintf("%c%c%c%c%c", 48, 3, 2, 1, 1));

    if (!ldap_set_option($ds, LDAP_OPT_SERVER_CONTROLS, array($ctrl1))) {
    error_log("ERROR: Failed to set server controls");
    }

    $result = ldap_mod_replace($ds, $dn, $entry);

    ReplyDelete