I'm using a Vagrant VM with Ubuntu 24.04 Noble to develop a project. I'm using Node 20.18.0 and NestJs 10.4.5.
In one of the routes, I´m trying to access a secure ldap server using NodeJs's ldapts library. The objective of connecting securely is to write the unicodePwd field in the AD.
Microsoft has this documentation relative to writing unicodePwd attribute. Basically the password has to be enclosed in double quotes and it must be UTF-16 encoded. Then the attribute should be written in a secure connection.
Inititally I tried connection this way:
const client = new Client({
url: 'ldaps://<ldapip>',
timeout: 0,
connectTimeout: 0,
});
const bindDN = 'CN=Administrator,CN=Users,DC=myproject,DC=local';
const password = 'mypdw';
try {
console.log('D1');
let cert = fs.readFileSync('/vagrant/backend/certs/mycert.crt');
console.log(cert.toString('base64'));
await client.startTLS({
ca: [cert],
});
console.log('D2');
await client.bind(bindDN, password);
console.log('D3');
(...)
} catch (error) {
return { status: 500, msg: 'LDAP fail', error }
} finally {
await client.unbind();
}
When I run this code, D0, D1 and the certificate are printed correctly, but it doesn't reach point D2. It was failing with the following error:
{"code":"UNABLE_TO_VERIFY_LEAF_SIGNATURE"}}
I know for sure that the certificate is emmited by the AD's CA. I'm using Chromium and the CA is correctly imported to it. I also tried to create a file with both the user certificate and the CA's certificate in it and it didn't work also.
Then I've done some changes to the code and now it connects, but I'm not sure I have a secure connection in place because I can't do what I need to do with it (writing unicodePwd attribute). Is there a way to check if the secure connection is in place ? or just connecting and binding to ldaps://myserver:636 is enough to guarantee a secure connection ?
This is the current version of the code:
// I tried the line below both with ca and client certs with equal results
let cert = fs.readFileSync('/vagrant/backend/certs/client-cert.crt');
let opts = {
ca: [cert],
host: 'myurl.myproject.local',
rejectUnauthorized: false,
secure: true
};
const client = new Client({
url: 'ldaps://myurl.myproject.local:636',
timeout: 0,
tlsOptions: {
...opts,
minVersion: 'TLSv1.1'
},
connectTimeout: 0,
});
const bindDN = 'CN=Administrator,CN=Users,DC=myproject,DC=local';
const password = 'mypdw';
try {
console.log('D1');
console.log(cert.toString('base64'));
console.log('D2');
await client.bind(bindDN, password);
console.log('D3', client.isConnected);
let passwdUtf16 = this.encodePassword("mypwd");
var newUser = {
sn: 'teste',
objectClass: ["organizationalPerson", "person", "user"],
unicodePwd: passwdUtf16
}
await client.add('cn=test,ou=MyCompany,ou=Organizations,dc=myproject,dc=local', newUser);
console.log('D4');
I also tried to add the user without the unicodePwd attribute (it works) and then do this:
let change = new Change({ operation: 'replace', modification: new Attribute({ type: 'unicodePwd;binary', values: [passwdUtf16] }) });
await client.modify('cn=test,ou=MyCompany,ou=Organizations,dc=myproject,dc=local', change);
In both cases I'm getting the UnwillingToPerformError leading me to think I don't have a secure connection in place.
This is the encodePassword function:
encodePassword(str) {
const text = '"' + str + '"';
let byteArray = new Uint8Array(text.length * 2);
for (let i = 0; i < text.length; i++) {
byteArray[i * 2] = text.charCodeAt(i); // & 0xff;
byteArray[i * 2 + 1] = text.charCodeAt(i) >> 8; // & 0xff;
}
return String.fromCharCode.apply(String, byteArray);
}
Now I have to questions:
How can I assert this is a secure connection in order to write unicodePwd attribute correctly ?
Am I doing anything wrong writing the password ?
I was finally able to write AD's password in NodeJs/NestJs. The answer was simpler than I thought. To answer my own questions:
There's no need to use startTLS function. Just binding to a client directed to ldaps:// is enough. You can check if the connection is using a secure TLS encoding using Wireshark. Just point to the network interface and it will clearly state that it's using TLS.
At least on our test lab AD, there's no need to set dsHeuristics field as stated by @ErkinD39. You can just use a replace
change in the modify function for unicodePwd field. Just make sure of 3 things:
In our case the only thing blocking the password write and generating UnwillingToPerform error was that the password we were using for tests didn't follow password rules. As soon as we tried a password that included alphas, numbers and a special char it worked. We used user@123 as password.
Here's the complete working code:
let certCA = fs.readFileSync('/vagrant/backend/certs/cacert.crt');
const client = new Client({
url: 'ldaps://myurl.myproject.local',
tlsOptions: {
ca: [certCA],
}
});
const bindDN = 'CN=Administrator,CN=Users,DC=myproject,DC=local';
const password = 'adm@123';
try {
await client.bind(bindDN, password);
let pwd = "user@123";
let passwdUtf16 = Buffer.from(`"${pwd}"`, 'utf16le');
let newUser = {
sn: 'test',
objectClass: ["organizationalPerson", "person", "user"],
}
await client.add('cn=test,ou=Company,ou=Organizations,dc=myproject,dc=local', newUser);
let changeReplPwd = new Change({ operation: 'replace', modification: new Attribute({ type: 'unicodePwd', values: [passwdUtf16] })}) ;
await client.modify('cn=test,ou=Company,ou=Organizations,dc=myproject,dc=local', [changeReplPwd]);
} catch (error) {
return { status: 500, msg: 'LDAP fail', error }
} finally {
await client.unbind();
}
you can also include the user with password directly this way:
let certCA = fs.readFileSync('/vagrant/backend/certs/cacert.crt');
const client = new Client({
url: 'ldaps://myurl.myproject.local',
tlsOptions: {
ca: [certCA]
}
});
const bindDN = 'CN=Administrator,CN=Users,DC=myproject,DC=local';
const password = 'adm@123';
try {
await client.bind(bindDN, password);
let pwd = "user@123";
// please note the line below is different from the example above
let passwdUtf16 = String.fromCharCode.apply(String, Buffer.from(`"${pwd}"`, 'utf16le'));
var newUser = {
sn: 'test',
objectClass: ["organizationalPerson", "person", "user"],
unicodePwd: passwdUtf16
}
await client.add('cn=test,ou=Company,ou=Organizations,dc=myproject,dc=local', newUser);
} catch (error) {
return { status: 500, msg: 'LDAP fail', error }
} finally {
await client.unbind();
}