Active Directory on Server 2012 – Proper set up to change passwords via programming

active-directory

This Question was previously on StackOverflow, as I thought it was a programming issue. It seems it is more of an issue with setting up Active Directory properly, so I moved it here.

Ok, I've been at this for some time now, but have not found my solution. I routinely connect to OpenLDAP with the server I am on, and everything works fine with SSL and LDAPs. However, now I need to switch to Active Directory, and am having trouble.

To set up the system, I did the following:
1) Exported my certificate from Server 2012:
http://pic.dhe.ibm.com/infocenter/rdirserv/v5r1m0/index.jsp?topic=%2Fcom.ibm.rational.rds.administering.doc%2Ftopics%2Ft_Exporting_certificate_Active_Directory_server.html

2) Converted my certificate from DER to PEM:
http://www.novell.com/coolsolutions/tip/5838.html

3) Made sure that TLS_REQCERT was set to "never"

4) Verified that my ldap.conf file was pointing to the correct location of my cert.

Boom! I was able to connect via ldap_connect() and was able to bind with my user. However, then when I went to modify a password, I received the ever so helpful error message of "Server Unwilling to Perform."

Some people suggest that this is a problem with SSL. So, I did some testing with the command:

*ldapsearch -H "ldaps://my.domain.tld" -b "" -s base -Omaxssf=0 -d7*

The results I receive are:

ldap_send_initial_request
ldap_new_connection 1 1 0
ldap_int_open_connection
ldap_connect_to_host: TCP my.domain.tld:636
ldap_new_socket: 3
ldap_prepare_socket: 3
ldap_connect_to_host: Trying ip.add.re.ss:636
ldap_pvt_connect: fd: 3 tm: -1 async: 0

TLS: loaded CA certificate file /etc/openldap/certs/cert-name.pem
... certificate data ...
TLS: certificate [CN=my.domain.tld] **is valid**
... more certificate data ...
* host: my.domain.tld  port: 636  (default)
  refcnt: 2  status: Connected
  last used: Fri Feb 21 11:50:13 2014

So it appears I'm connected, and that my certificate is valid.

Trying a search from the command line:

ldapsearch -H "ldaps://my.domain.tld:636" -D "user@my.domain.tld" -W -x -b "CN=Person I. M. Lookingfor,CN=Users,DC=my,DC=domain,DC=tld"

Returns all the information I expect to retrieve.

At this point, I don't think it's a problem with the code, so much as I believe there is some setting I a still missing in AD. I am not a Windows person, so I do not know what else I could be missing. Any additional thoughts would be appreciated.

CODE TO GENERATE LDIF

function encodePwd($new_password) {
        $newpass = '';
        $new_password = "\"".$new_password."\"";
        $len = strlen($new_password);
        for ($i = 0; $i < $len; $i++) {
                $newpass .= "{$new_password{$i}}\000";
        }
        $newpass = base64_encode($newpass);
        return $newpass;
}

Resulting LDIF:

dn: CN=Tester B. Testerton,CN=Users,DC=my,DC=domain,DC=tld
changetype: modify
replace: unicodePwd
unicodePwd:: IgBiAHUAQAAoADUAJABpAF4APQA5AGwAawBDACIA

Then the code attempts:

$mods["unicodePwd"] = $new_pass64;
if(ldap_modify($ldap_conx,$ldap_user_dn,$mods) === false)
{
    $e = ldap_error($ldap_conx);
    $e_no = ldap_errno($ldap_conx);
    echo "Error attempting to modify password in LDAP for user: $uname\n";
} else {
    echo "Success! Changed password for user: $uname\n";
}

I changed the "Domain Users" group to be "Managed By" my admin user account. Then, I ran the ldifde command in the PowerShell as Administrator and the it worked where it hadn't before becoming the "manager":

Logging in as current user using SSPI
Importing directory from file ".\tester.ldif"
Loading entries..
1 entry modified successfully.

The command has completed successfully

However, I still am not able to modify the password via PHP; something is still missing. I routinely use this Linux server to update both OpenLDAP and Novell eDirectory servers, so I know my LDAP settings for PHP are correct.

My problem it seems, is more with setting up the Active Directory Server for use with programmatic updates. I've tried the password update with Python, but the results are the same.

Update
I looked at the link provided by Ryan (http://technet.microsoft.com/en-us/magazine/ff848710.aspx), and confirmed that my attempt to convert the password is in fact converting to the proper encoding (i.e. "car" converts to IgBjAGEAcgAiAA== as in the article). It appears that now it's down to figuring out how to add extended rights to my LDAP manager.

Best Answer

As you already know, updating the unicodePwd attribute of a user account remotely requires a 128-bit or better SSL/TLS connection to the domain controller, by default. Or SASL if 2008R2 or 2012. It looks like you're already covered there since you've got a certificate and you're utilizing LDAPS.

Also, just for public information, the unicodePwd attribute is write-only. It's never returned in the result of an LDAP search.

I also see that you're enclosing the plain-text password with quotation marks, then base64-encoding the entire thing, which are both correct. You're so close!

But what I don't see in your code is where you are ensuring that the encoding is UTF-16. It has to be UTF-16. UTF-16 little endian, then base64-encoded, to be exact.

"... the DC requires that the password value be specified in a UTF-16 encoded Unicode string containing the password surrounded by quotation marks, which has been BER-encoded as an octet string per the Object(Replica-Link) syntax."

Also, just so that we don't miss the obvious, ensure correct permissions:

For the password change operation to succeed, the server enforces the requirement that the user or inetOrgPerson object whose password is being changed must possess the "User-Change-Password" control access right on itself,

(i.e. the account doesn't have the "user cannot change password" flag set,)

and that Vdel must be the current password on the object. For the password reset to succeed, the server enforces the requirement that the client possess the "User-Force-Change-Password" control access right on the user or inetOrgPerson object whose password is to be reset.

And again, just so we don't overlook the obvious, make sure the new value you're trying to set meets the domain's password policy.

I don't speak PHP or Perl so well, but here's some Powershell that gets the password in the correctly encoded format that you need:

PS C:\> [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes("`"SweetNewPwd123!`""))
IgBTAHcAZQBlAHQATgBlAHcAUAB3AGQAMQAyADMAIQAiAA==

Let us ruminate over the documentation of the ever-mysterious unicodePwd attribute:

http://msdn.microsoft.com/en-us/library/cc223248.aspx

Also as supplemental reading, check this post:

http://technet.microsoft.com/en-us/magazine/ff848710.aspx