skip to Main Content

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:

  1. How can I assert this is a secure connection in order to write unicodePwd attribute correctly ?

  2. Am I doing anything wrong writing the password ?

2

Answers


  1. Chosen as BEST ANSWER

    I was finally able to write AD's password in NodeJs/NestJs. To answer my own questions:

    1. How can I assert this is a secure connection in order to write unicodePwd attribute correctly ?

    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 transitting using TLS.

    1. Am I doing anything wrong writing the password ?

    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:

    • the password string should be surrounded by double quotes
    • Should be utf-16 encoded and
    • there's no password rules policy in place.

    In our case the only thing blocking the password write that was 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 company@123 as password.

    Here's the complete working code:

    let certCA = fs.readFileSync('/vagrant/backend/certs/cacert.crt');
    let certUser = fs.readFileSync('/vagrant/backend/certs/user.crt');
    const client = new Client({
        url: 'ldaps://myurl.myproject.local',
        timeout: 3 * 1000,
        connectTimeout: 2 * 1000,
        strictDN: true,
        tlsOptions: {
            ca: [certCA],
            host: 'myurl.myproject.local',
            rejectUnauthorized: false, 
            minVersion: 'TLSv1.2',
            cert: certUser, 
        }
    });
    const bindDN = 'CN=Administrator,CN=Users,DC=myproject,DC=local';
    const password = 'company@123';
    
    try {
        await client.bind(bindDN, password);
    
        let passwdUtf16 =  Buffer.from(`"${pwd}"`, 'utf16le'); 
        
        var newUser = {
            sn: 'test',
            distinguishedName: "CN=test,OU=Company,OU=Organizations,DC=myproject,DC=local",
            objectClass: ["organizationalPerson", "person", "user"],
            sAMAccountName: "test",
            displayName: "test",
        }
    
        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');
    let certUser = fs.readFileSync('/vagrant/backend/certs/user.crt');
    const client = new Client({
        url: 'ldaps://myurl.myproject.local',
        timeout: 3 * 1000,
        connectTimeout: 2 * 1000,
        strictDN: true,
        tlsOptions: {
            ca: [certCA],
            host: 'myurl.myproject.local',
            rejectUnauthorized: false, 
            minVersion: 'TLSv1.2',
            cert: certUser, 
        }
    });
    const bindDN = 'CN=Administrator,CN=Users,DC=myproject,DC=local';
    const password = 'company@123';
    
    try {
        await client.bind(bindDN, password);
        
        // 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',
            distinguishedName: "CN=test,OU=Company,OU=Organizations,DC=myproject,DC=local",
            objectClass: ["organizationalPerson", "person", "user"],
            sAMAccountName: "test",
            displayName: "test",
            unicodePwd: passwdUtf16,
            userAccountControl: '512'
        }
    
        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();
    }
    

    1. As per the following statement SASL-layer encryption is also accepted instead of SSL/TLS.

    "On Windows Server 2003 operating system and later, the DC also permits modification of the unicodePwd attribute on a connection protected by 128-bit (or better) Simple Authentication and Security Layer (SASL)-layer encryption instead of SSL/TLS."
    Ref: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/6e803168-f140-4d23-b2d3-c3a8ab5917d2

    I think special to this attribute the document says:

    "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, and that Vdel MUST be the current password on the object."

    You may increase LDAP logging level following this link https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/configure-ad-and-lds-event-logging. Diagnostics subkey referenced in the article may be chosen as 16 or 27 to check LDAP interface events or PDC password update notifications respectively. Logging level may be chosen as 4 (verbose).

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search