Code Monkey home page Code Monkey logo

ldapts's Introduction

LDAPts

NPM version node version Known Vulnerabilities

Providing an API to access LDAP directory servers from Node.js programs.

Table of Contents

API details

Create a client

The code to create a new client looks like:

import { Client } from 'ldapts';

const client = new Client({
  url: 'ldaps://ldap.jumpcloud.com',
  timeout: 0,
  connectTimeout: 0,
  tlsOptions: {
    minVersion: 'TLSv1.2',
  },
  strictDN: true,
});

You can use ldap:// or ldaps://; the latter would connect over SSL (note that this will not use the LDAP TLS extended operation, but literally an SSL connection to port 636, as in LDAP v2). The full set of options to create a client is:

Attribute Description
url A valid LDAP URL (proto/host/port only)
timeout Milliseconds client should let operations live for before timing out (Default: Infinity)
connectTimeout Milliseconds client should wait before timing out on TCP connections (Default: OS default)
tlsOptions TLS connect() options
strictDN Force strict DN parsing for client methods (Default is true)

Specifying Controls

Single or an array of Control objects can be added to various operations like the following:

import { Control } from 'ldapts/controls';

const { searchEntries, searchReferences } = await client.search(
  searchDN,
  {
    filter: '([email protected])',
  },
  new Control('1.2.840.113556.1.4.417'),
);

You can also subclass Control for finer control over how data is parsed and written. Look at PagedResultsControl for an example.

bind

bind(dnOrSaslMechanism, [password], [controls])

Performs a bind operation against the LDAP server.

Arguments:

Argument Description
dnOrSaslMechanism (string) The name (DN) of the directory object that the client wishes to bind as or the SASL mechanism (PLAIN, EXTERNAL)
[password] (string) Password for the target bind DN. For SASL this is instead an optional set of encoded SASL credentials.
[controls] (Control|Control[]) Optional Control object or array of Control objects

Simple Example:

await client.bind('cn=root', 'secret');

SASL Example:

// No credentials
await client.bind('EXTERNAL');

// With credentials
const credentials = '...foo...';
await client.bind('PLAIN', credentials);

startTLS

startTLS(options, [controls])

Performs a StartTLS extended operation against the LDAP server to initiate a TLS-secured communication channel over an otherwise clear-text connection.

Arguments:

Argument Description
options (object) TLS connect() options
[controls] (Control|Control[]) Optional Control object or array of Control objects

Example:

await client.startTLS({
  ca: [fs.readFileSync('mycacert.pem')],
});

add

add(dn, entry, [controls])

Performs an add operation against the LDAP server.

Allows you to add an entry (as a js object or array of Attributes), and as always, controls are optional.

Arguments:

Argument Description
dn (string) The DN of the entry to add
entry (object|Attribute[]) The set of attributes to include in that entry
[controls] (Control|Control[]) Optional Control object or array of Control objects

Example:

var entry = {
  cn: 'foo',
  sn: 'bar',
  email: ['[email protected]', '[email protected]'],
  objectclass: 'fooPerson',
};
await client.add('cn=foo, o=example', entry);

compare

compare(dn, attribute, value, [controls])

Performs an LDAP compare operation with the given attribute and value against the entry referenced by dn.

Arguments:

Argument Description
dn (string) The DN of the entry in which the comparison is to be made
attribute (string) The Name of the attribute in which the comparison is to be made
value (string) The Attribute Value Assertion to try to find in the specified attribute
[controls] (Control|Control[]) Optional Control object or array of Control objects

Returns: (boolean): Returns true if the target entry exists and does contain the specified attribute value; otherwise false

Example:

const hasValue = await client.compare('cn=foo, o=example', 'sn', 'bar');

del

del(dn, [controls])

Deletes an entry from the LDAP server.

Arguments:

Argument Description
dn (string) The DN of the entry to delete
[controls] (Control|Control[]) Optional Control object or array of Control objects

Example:

await client.del('cn=foo, o=example');

exop

exop(oid, [value], [controls])

Performs an LDAP extended operation against an LDAP server.

Arguments:

Argument Description
oid (string) Object identifier representing the type of request
[value] (string) Optional value - based on the type of operation
[controls] (Control|Control[]) Optional Control object or array of Control objects

Example (performs an LDAP 'whoami' extended op):

const { value } = await client.exop('1.3.6.1.4.1.4203.1.11.3');

modify

modify(name, changes, [controls])

Performs an LDAP modify operation against the LDAP server. This API requires you to pass in a Change object, which is described below. Note that you can pass in a single Change or an array of Change objects.

Arguments:

Argument Description
dn (string) The DN of the entry to modify
changes (Change|Change[]) The set of changes to make to the entry
[controls] (Control|Control[]) Optional Control object or array of Control objects

Example (update multiple attributes):

import { Attribute, Change } from 'ldapts';

await client.modify('cn=foo, o=example', [
  new Change({ operation: 'replace', modification: new Attribute({ type: 'title', values: ['web tester'] }) }),
  new Change({ operation: 'replace', modification: new Attribute({ type: 'displayName', values: ['John W Doe'] }) }),
]);

Example (update binary attribute):

import { Attribute, Change } from 'ldapts';

const thumbnailPhotoBuffer = await fs.readFile(path.join(__dirname, './groot_100.jpg'));

var change = new Change({
  operation: 'replace',
  modification: new Attribute({
    type: 'thumbnailPhoto;binary',
    values: [thumbnailPhotoBuffer],
  }),
});

await client.modify('cn=foo, o=example', change);

Change

Change({ operation, modification })

A Change object maps to the LDAP protocol of a modify change, and requires you to set the operation and modification.

Arguments:

Argument Description
operation (replace|add|delete) See table below
modification (Attribute) Attribute details to add, remove, or update

Operations:

Value Description
replace Replaces the attribute referenced in modification. If the modification has no values, it is equivalent to a delete.
add Adds the attribute value(s) referenced in modification. The attribute may or may not already exist.
delete Deletes the attribute (and all values) referenced in modification.

modifyDN

modifyDN(dn, newDN, [controls])

Performs an LDAP modifyDN (rename) operation against an entry in the LDAP server. A couple points with this client API:

  • There is no ability to set "keep old dn." It's always going to flag the old dn to be purged.
  • The client code will automatically figure out if the request is a "new superior" request ("new superior" means move to a different part of the tree, as opposed to just renaming the leaf).

Arguments:

Argument Description
dn (string) The DN of the entry to rename
newDN (string) The new RDN to use assign to the entry. It may be the same as the current RDN if you only intend to move the entry beneath a new parent. If the new RDN includes any attribute values that aren’t already in the entry, the entry will be updated to include them.
[controls] (Control|Control[]) Optional Control object or array of Control objects

Example:

await client.modifyDN('cn=foo, o=example', 'cn=bar');

search

search(baseDN, options, [controls])

Performs a search operation against the LDAP server.

The search operation is more complex than the other operations, so this one takes an options object for all the parameters.

Arguments:

Argument Description
baseDN (string) The base of the subtree in which the search is to be constrained
options (object) See table below
[controls] (Control|Control[]) Optional Control object or array of Control objects

Options:

Attribute Description
[scope=sub] (string)
  • base - Indicates that only the entry specified as the search base should be considered. None of its subordinates will be considered.
  • one - Indicates that only the immediate children of the entry specified as the search base should be considered. The base entry itself should not be considered, nor any descendants of the immediate children of the base entry.
  • sub - Indicates that the entry specified as the search base, and all of its subordinates to any depth, should be considered.
  • children - Indicates that the entry specified by the search base should not be considered, but all of its subordinates to any depth should be considered.
[filter=(objectclass=*)] (string|Filter) The filter of the search request. It must conform to the LDAP filter syntax specified in RFC4515
[derefAliases=never] (string)
  • never - Never dereferences entries, returns alias objects instead. The alias contains the reference to the real entry.
  • always - Always returns the referenced entries, not the alias object.
  • search - While searching subordinates of the base object, dereferences any alias within the search scope. Dereferenced objects become the bases of further search scopes where the Search operation is also applied by the server. The server should eliminate duplicate entries that arise due to alias dereferencing while searching.
  • find - Dereferences aliases in locating the base object of the search, but not when searching subordinates of the base object.
[returnAttributeValues=true] (boolean) If true, attribute values should be included in the entries that are returned; otherwise entries that match the search criteria should be returned containing only the attribute descriptions for the attributes contained in that entry but should not include the values for those attributes.
[sizeLimit=0] (number) The maximum number of entries that should be returned from the search. A value of zero indicates no limit. Note that the server may also impose a size limit for the search operation, and in that case the smaller of the client-requested and server-imposed size limits will be enforced.
[timeLimit=10] (number) The maximum length of time, in seconds, that the server should spend processing the search. A value of zero indicates no limit. Note that the server may also impose a time limit for the search operation, and in that case the smaller of the client-requested and server-imposed time limits will be enforced.
[paged=false] (boolean|SearchPageOptions) Used to allow paging and specify the page size
[attributes=] (string[]) A set of attributes to request for inclusion in entries that match the search criteria and are returned to the client. If a specific set of attribute descriptions are listed, then only those attributes should be included in matching entries. The special value “” indicates that all user attributes should be included in matching entries. The special value “+” indicates that all operational attributes should be included in matching entries. The special value “1.1” indicates that no attributes should be included in matching entries. Some servers may also support the ability to use the “@” symbol followed by an object class name (e.g., “@inetOrgPerson”) to request all attributes associated with that object class. If the set of attributes to request is empty, then the server should behave as if the value “” was specified to request that all user attributes be included in entries that are returned.
[explicitBufferAttributes=] (string[]) List of explicit attribute names to return as Buffer objects

Example:

const { searchEntries, searchReferences } = await client.search(searchDN, {
  filter: '([email protected])',
});

Please see Client tests for more search examples

Filter Strings

The easiest way to write search filters is to write them compliant with RFC2254, which is "The string representation of LDAP search filters."

Assuming you don't really want to read the RFC, search filters in LDAP are basically are a "tree" of attribute/value assertions, with the tree specified in prefix notation. For example, let's start simple, and build up a complicated filter. The most basic filter is equality, so let's assume you want to search for an attribute email with a value of [email protected]. The syntax would be:

const filter = `([email protected])`;

ldapts requires all filters to be surrounded by '()' blocks. Ok, that was easy. Let's now assume that you want to find all records where the email is actually just anything in the "@bar.com" domain and the location attribute is set to Seattle:

const filter = `(&(email=*@bar.com)(l=Seattle))`;

Now our filter is actually three LDAP filters. We have an and filter (single amp &), an equality filter (the l=Seattle), and a substring filter. Substrings are wildcard filters. They use * as the wildcard. You can put more than one wildcard for a given string. For example you could do (email=*@*bar.com) to match any email of @bar.com or its subdomains like "[email protected]".

Now, let's say we also want to set our filter to include a specification that either the employeeType not be a manager nor a secretary:

const filter = `(&(email=*@bar.com)(l=Seattle)(!(|(employeeType=manager)(employeeType=secretary))))`;

The not character is represented as a !, the or as a single pipe |. It gets a little bit complicated, but it's actually quite powerful, and lets you find almost anything you're looking for.

Return buffer for specific attribute

Sometimes you may want to get a buffer back instead of a string for an attribute value. Depending on the server software, you may be able to append ;binary (the binary attribute subtype) to the attribute name, to have the value returned as a Buffer.

const searchResults = await ldapClient.search('ou=Users,o=5be4c382c583e54de6a3ff52,dc=jumpcloud,dc=com', {
  filter: '([email protected])',
  attributes: ['jpegPhoto;binary'],
});

However, some servers are very strict when it comes to the binary attribute subtype and will only acknowledge it if there is an associated AN.1 type or valid BER encoding. In those cases, you can tell ldapts to explicitly return a Buffer for an attribute:

const searchResult = await client.search('ou=Users,o=5be4c382c583e54de6a3ff52,dc=jumpcloud,dc=com', {
  filter: '([email protected])',
  explicitBufferAttributes: ['jpegPhoto'],
});

unbind

unbind()

Used to indicate that the client wants to close the connection to the directory server.

Example:

await client.unbind();

Usage Examples

Authenticate example

const { Client } = require('ldapts');

const url = 'ldap://ldap.forumsys.com:389';
const bindDN = 'cn=read-only-admin,dc=example,dc=com';
const password = 'password';

const client = new Client({
  url,
});

let isAuthenticated;
try {
  await client.bind(bindDN, password);
  isAuthenticated = true;
} catch (ex) {
  isAuthenticated = false;
} finally {
  await client.unbind();
}

Search example

const { Client } = require('ldapts');

const url = 'ldaps://ldap.jumpcloud.com';
const bindDN = 'uid=tony.stark,ou=Users,o=5be4c382c583e54de6a3ff52,dc=jumpcloud,dc=com';
const password = 'MyRedSuitKeepsMeWarm';
const searchDN = 'ou=Users,o=5be4c382c583e54de6a3ff52,dc=jumpcloud,dc=com';

const client = new Client({
  url,
  tlsOptions: {
    rejectUnauthorized: args.rejectUnauthorized,
  },
});

try {
  await client.bind(bindDN, password);

  const { searchEntries, searchReferences } = await client.search(searchDN, {
    scope: 'sub',
    filter: '([email protected])',
  });
} catch (ex) {
  throw ex;
} finally {
  await client.unbind();
}

Delete Active Directory entry example

const { Client } = require('ldapts');

const url = 'ldap://127.0.0.1:1389';
const bindDN = 'uid=foo,dc=example,dc=com';
const password = 'bar';
const dnToDelete = 'uid=foobar,dc=example,dc=com';

const client = new Client({
  url,
});

try {
  await client.bind(bindDN, password);

  await client.del(dnToDelete);
} catch (ex) {
  if (ex instanceof InvalidCredentialsError) {
    // Handle authentication specifically
  }

  throw ex;
} finally {
  await client.unbind();
}

Development

Start test OpenLDAP server

docker-compose up -d

Close test OpenLDAP server

docker-compose down

ldapts's People

Contributors

adrianplavka avatar dependabot-preview[bot] avatar hasegawa-jun avatar hikaru7719 avatar israelfrid avatar jgeurts avatar liudonghua123 avatar stevenhair avatar stiller-leser avatar templum avatar thernstig avatar timohocker avatar tsaarni avatar wattry avatar willmcenaney avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

ldapts's Issues

Determine if a client is still connected?

Is there a simple way to check prior to running a search operation whether or not an ldapts client is still connected and usable? Specifically, I'm using ldapts in conjunction with a connection pool (node generic-pool) and I'm trying to find an efficient way to validate bound clients to see if they are still usable (e.g., to make sure the connection hasn't timed out due to being idle) after the connection is checked out from the pool.

Thank you!

Validate login of user.

I am trying to use this project to log users in. After the system forms a bind, it needs to take the username and password provided by the user and validate it.

Add Buffer as second parameter for extended operation

Can we change:
public async exop(oid: string, value?: string, controls?: Control|Control[])
to:
public async exop(oid: string, value?: string | Buffer, controls?: Control|Control[])?

In ExtendedRequest.ts Buffer as value is allowed, as per definition it should also be allowed in Client.ts for the calling method?

Search filter with brackets yields no result

Hey @jgeurts ,

first off thank you very much for the work.

I am experiencing an issue with the following query:

const searchResult: any = await client.search(settings.ldapGroupSearchDN, {
      filter: "(&(objectCategory=group)(displayName=My group (something)",
})

After replacing the brackets with \28 and \29 this call yields no result. However running: ldapsearch -H ldaps://.... -x -w 'verySecret' -D myUser -b 'ldapGroupSearchDN' "(&(objectCategory=group)(displayName=My group \28something\29))" does.

Any pointer why this is?

Also:

Is there a reason why https://github.com/ldapts/ldapts/blob/master/src/filters/Filter.ts#L36 is protected? Would be cool, if I'd be able to use it. Currently I need to fallback to https://github.com/tcort/ldap-escape before executing the call above (or manually use .replace()). Otherwise I see UnhandledPromiseRejectionWarning: Error: Illegal unescaped character: ( in value: My group (something at Function._unescapeHexValues (ldapts/dist/FilterParser.js:272:27)

Best regards,
stiller-leser

Error: Processing on the associated request Timeout limit specified by either the client request or the server administration limits has been exceeded and has been terminated because it took too long to complete. Code: 0x3

I want to make a filter operation, about 10 seconds passed, then it failed with the following error.

D:\code\node\ldap-simple-tool>node bin\ldap-simple-tool.js filter uid=xxx
try to filter {"expression":"uid=xxx"}!
filter operation error!
Error: Processing on the associated request Timeout limit specified by either the client request or the server administration limits has been exceeded and has been terminated because it took too long to complete. Code: 0x3
filter failed, please check the filter expression

D:\code\node\ldap-simple-tool>

The code I write is the following.

const filter = async ({ addr, baseDn, bindDn, bindPass, authFilter, tls, startTLS }, [_, expression]) => {
  const client = new Client({
    url: `${tls ? 'ldaps' : 'ldap'}://${addr}`,
    timeout: 0,
    connectTimeout: 0,
  });
  try {
    startTLS && client.startTLS();
    await client.bind(bindDn, bindPass);
    const filterResults = await _filter({ baseDn }, client, expression);
    return filterResults;
  } catch (error) {
    console.error(chalk.yellow.italic(`filter operation error!\n${error}`));
  } finally {
    await client.unbind();
  }
  return null;
};

It seems the timeout configs did not work as expected. I have also tried to remove timeout and connectTimeout or set a very large number.

However I can make filter operation in some client tool like LDAP Admin.

DN has embedded parentheses

I search a group to retrieve the paths for each user in the group, then query each using that path as the distinguished name. Unfortunately, some of the DN entries contain embedded parentheses like this:
CN=Joe Cool (contractor),CN=Users,DC=myco,DC=mycompany,DC=com
These parentheses choke the filter parser in ldapts. I've tried various schemes for escaping them, but still get rejections from ldapts. These are legal characters for LDAP, how can I get these past the filter parser in ldapts?

Error: Connection closed before message response was received.

I tried to take this for a spin with a publicly available LDAP server. Older code with node-ldapauth-fork is running fine. With this package, I am getting an error. You can check the code and run it at https://repl.it/@purplemana/Ldapts. It's a copy-paste of the search example from the package's docs with the server details changed. You can get the ldap details here - https://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/.

Here's the code for reference:

const { Client } = require('ldapts');

const url = 'ldap://ldap.forumsys.com:389';
const bindDN = 'cn=read-only-admin,dc=example,dc=com';
const password = 'password';
const searchDN = 'ou=mathematicians,dc=example,dc=com';

async function search() {
  const client = new Client({
    url
  });

  try {
    await client.bind(bindDN, password);

    const {
      searchEntries,
      searchReferences,
    } = await client.search(searchDN, {
      scope: 'sub',
      filter: 'cn=riemann',
    });

    console.log(searchEntries)

    return searchEntries;
  } catch (ex) {
    throw ex;
  } finally {
    await client.unbind();
  }
}


search()
.then(console.log);

Concurrent searches ('ldaps' protocol) with node > v12

Expected Behavior

When performing more than two search operations simultanously, every single one should resolve correctly.

Actual Behavior

Only the first two search operations are resolved (Even though the ldap server answers correctly).

This does only occur with node.js >= v12.

It does not occur with only two concurrent search operations.

From our ldap server logs, we can see, that the server sends everything correctly.

The same problem occurs with ldapjs (v2.0.0) as well.

Error: SearchRequest: Operation timed out (Client.ts:826:25)

Steps to Reproduce

With this script you can reproduce the issue. We perform the same base search operation 3 times in parallel with Promise.all.

It doesn't matter if we use 3 different search operations but for simplicities sake we use a base search with our bindDN as base to be sure the entry exists.

If you run this script with i.e. node v10, everything works as expected, with node v12 or higher you will see the error message after the operation has timed out.

import { Client } from 'ldapts'
import assert from 'assert'

async function main() {
  const url = 'ldaps://...'
  const bindDN = 'cn=...'
  const bindCredentials = 'secret'

  const client = new Client({
    url,
    // since the operation times out, we use a low timeout
    timeout: 5000,
    connectTimeout: 5000,
  })

  try {
    await client.bind(bindDN, bindCredentials)

    const results = await Promise.all([
      findByDN(client, bindDN),
      findByDN(client, bindDN),
      findByDN(client, bindDN),
    ])

    assert.deepEqual(
      results.map((result) => result.dn),
      [bindDN, bindDN, bindDN],
    )

    console.log('ldapts is compatible with the selected Node version.')
  } catch (err) {
    console.log('ldapts is not compatible with the selected Node version.')
    console.error(err)
  } finally {
    await client.unbind()

    process.exit()
  }
}

async function findByDN(client, dn) {
  const { searchEntries } = await client.search(dn, {
    scope: 'base',
  })

  assert.equal(searchEntries.length, 1)

  console.log(`found ${searchEntries[0].dn}`)

  return searchEntries[0]
}

main()

Context (Environment)

  • NetIQ eDirectory version: 9.2.2
  • node.js version: v12 or higher
  • ldapts version: v2.6.1

'Socket connection not established' when server closes connection

I'm encountering the 'Socket connection not established' error located at the top of the _send function in Client.js. This will occur when this.connected == true, but this.socket == undefined. We land here because (for example) the search function will only call _connect if this.connected == false, not if this.socket is undefined.

To reproduce:

  1. Start server
  2. Have client make a query. This sets this.connected = true and defines the socket
  3. Have the server gracefully close the connection.
  4. Have client make another query.

I believe root cause occurs in the socketClose function and the connectTimer timeout, where the socket is deleted, but this.connected is not set to false

startTLS error

I am not sure if I am doing this correctly, I was not able to find any example code to do this.

I would like to use the startTLS extended operation to establish a secure connection to my ldap server and then bind the uname/pwd to authenticate a user.

I get the following error
{ Error: Client network socket disconnected before secure TLS connection was established at TLSSocket.onConnectEnd (_tls_wrap.js:1095:19) at Object.onceWrapper (events.js:286:20) at TLSSocket.emit (events.js:203:15) at endReadableNT (_stream_readable.js:1145:12) at process._tickCallback (internal/process/next_tick.js:63:19) code: 'ECONNRESET', path: undefined,
Here is my code (the variables are declared in the file, not adding here)

const getLdap = async () => {
  const client = new Client({
    url: url,
    connectTimeout: 5000,
    tlsOptions: {
      rejectUnauthorized: tlsOpts.rejectUnauthorized,
      ca: [fs.readFileSync(tlsOpts.cert)],
    },
  });

  await client.startTLS(tlsOpts, (err) => {
    try {
      client.bind(searchDN, password);
      const { searchEntries } = client.search(searchDN, searchOpts);
      console.log(searchEntries[0].memberOf);
      return searchEntries[0].memberOf;
    } catch (err) {
      throw err.message;
    } finally {
      client.unbind();
    }
  });
};

More DOCs please

It would be real nice if there were documentation for all functionality.

The current docs do not discuss features like add, change, etc. And I just have no clue how to use them.

Compare does not work with read only user.

When bind with a read-only user and try to use compare, I get this message:

InsufficientAccessError: The caller does not have sufficient rights to perform the requested operation. Code: 0x32
    at Function.parse (/home/william/dev/wmantly/ldap-node-test/node_modules/ldapts/StatusCodeParser.js:53:24)
    at Client.compare (/home/william/dev/wmantly/ldap-node-test/node_modules/ldapts/Client.js:202:59)
    at processTicksAndRejections (internal/process/task_queues.js:97:5)
    at async /home/william/dev/wmantly/ldap-node-test/app.js:26:16 {
  code: 50,
  message: 'The caller does not have sufficient rights to perform the requested operation. Code: 0x32'
}

Error: Could not find declaration file for module 'asn1'

I am making a program in TS and while building this error pops up. Any idea why?

node_modules/ldapts/dist/filters/Filter.d.ts:2:38 - error TS7016: Could not find a declaration file for module 'asn1'. '.../node_modules/asn1/lib/index.js' implicitly has an 'any' type.
  Try `npm install @types/asn1` if it exists or add a new declaration (.d.ts) file containing `declare module 'asn1';`

2 import { BerReader, BerWriter } from 'asn1';

Same error also occurs for other .d.ts files like Attribute.d.ts, Change.d.ts, Control.d.ts

Btw there is no types for asn1

@jgeurts @adrianplavka

Any plans to port over server functionality?

I just stumbled across this project and it looks promising. The ldapjs codebase has been dead for quite a while now and it's great to see some movement again.

I see that this only uses the client functionality from ldapjs, is there any plans to bring over the server functionality from it?

My team runs an internal ldap proxy and we use ldapjs. We currently have an internal fork of it to fix some server bugs that have/will never been fixed in ldapjs. It would be great to switch over to this library (we also use TypeScript) and to potentially push some of our fixes upstream as well.

Allow retrieval of raw attribute value in search results without ;binary attribute option

As mentioned in my comment on #11, OpenLDAP doesn't return attribute data when requesting attributes with the ;binary attribute option in searches unless those attributes are explicitly encodable with BER. Unfortunately, attributes like jpegPhoto do not meet this requirement, and it seems to be impossible to coax OpenLDAP into just dealing with it. This means that the trick this library employs to return Buffers for binary attributes in searches fails, as specifying option attributes: ['jpegPhoto'] returns a corrupt UTF-16 string, but attributes: ['jpegPhoto;binary'] returns no attribute data whatsoever.

It would be nice to have, say, a .raw property on attribute values in search entries which would hold their value as the pre-toStringed Buffer holding the attribute data.

Related: #72 (sort of a duplicate?)

how to modify ?

hello
please to help me. when i call modify function required Change but when i import it and console log to check is undefined
when i new Change , Attribute
-> TypeError: _ldapts.Change is not a constructor
-> TypeError: _ldapts.Attribute is not a constructor

--> my code <--
import { Client, Change, Attribute } from 'ldapts'
...
const mod = new Attribute({
type: 'info;string',
values: [ 'zzz']
})

--> and error return <--
UnhandledPromiseRejectionWarning: TypeError: _ldapts.Attribute is not a constructor

can you help me resolve this issue ,

Thank you very much

Error types are not exported

There is not an easy way to import the Error classes for checking in exception handling, having to do import { InvalidCredentialsError } from 'ldapts/dist/errors/resultCodeErrors/InvalidCredentialsError'.

Would it be reasonable to export these from the index.d.ts?

Implement StartTLS

This would be a huge benefit for us, as only simple auth is supported at the moment we will have to require an ldaps connection. I just tested out using starttls with the ldapjs library and it seems that it would fix our issue and allow plain ldap connections once again. I would love to continue using ldapts in our project but for now I may have to just wrap the ldapjs stuff :(

Binary attribute value gets corrupted due to wrong encoding

Tried accessing a Samba-based PDC via LDAP including its objectSid attribute which comes binary-encoded. Just like with ldapjs I was realizing that binary content exposed this way gets corrupted due to assuming UTF-8 encoding when converting attribute values from Buffer to string, even though the resulting string is full of Unicode escape sequences, afterwards. In my case a 25-bytes buffer value becomes a 24 characters string of unicode escapes.

In ldapjs there is an opportunity to get the raw set of buffers instead of transcoded strings per search result entry.

How do you handle / hook referrals?

I have a multi domain tree and different domains require different credentials so i need to intercept the referral and rebind using the appropriate credentials.

Filter disabled users on AD

I have just migrated from ldapjs and appreciate changes and corrections made on the code. Nevertheless, a filter using previously with ldapjs not work. It's normaly should remove desactivate users on AD (see last part of filter below).

const searchOptions = {
            scope: 'sub',
            sizeLimit: 50,
            attributes: ["sAMAccountName", "name", "mail", "telephoneNumber", "sn", "givenName"],
            filter: '(&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))'
        };

Without userAccountControl Filter, I've got all users but with I've an empty result.

Modify Multiple Attributes at once

is there a way to modify multiple attributes in a single change e.g. below
await client.modify(SEARCHCRITERIA'', new Change({ operation: 'replace', modification: new Attribute( { type: 'title', values: ['web tester'] }, { type: 'displayName', values: ['John W Doe'] } ), }));

or

await client.modify(SEARCHCRITERIA'', new Change({ operation: 'replace', modification: new Attribute( { type:[ 'title','displayName'] values: ['web tester','John W Doe'], } ), }));

Return empty attributes

It would be nice if all attributes requested that exist on the server were returned, instead of needing to check for keys to know if they are empty. Currently ldapts will remove attributes returned by the server that are falsy. (empty strings) Requesting a binary version still returns no keys if the value is falsy.

For example if you want a users phone number, but the telephoneNumber attribute was never filled in it will be empty and ldapts drops the attribute key.

client.search('dc=domain,dc=local', {
        scope: 'sub',
        filter: `([email protected])`,
        attributes: ['telephoneNumber']
      });

// Result [ { dn: 'CN=Test User,OU=Users,DC=domain,DC=local' } ]

For consistency it'd be nice to have:

client.search('dc=domain,dc=local', {
        scope: 'sub',
        filter: `([email protected])`,
        attributes: ['telephoneNumber']
      });

// Result [ { dn: 'CN=Test User,OU=Users,DC=domain,DC=local', telephoneNumber: ''} ]

It looks like this is happening on line 45 of SearchEntry.ts, however I didn't make any PR since I didn't know if this behavior was intentional.

Unhandled promise rejection - Error: AddRequest: Operation timed out

Package Version: 2.7.0 (but also tested with 1.9.0 and the bug is still present)
Node: 14
AD protocol: ldaps

If for some reason we attempt to add a user with a null password, we get an unhandled promise rejection even if we catch the "add" error. I tried listening on the client.socket's events, but that did not help in any way. Although this can be prevented higher up in our code, it seems like ldapts should not fail in this way ...

Also, I tried to unbind the client in the try-finally, but that just resulted in a completely different error (Error: Connection closed before message response was received. Message type: AddRequest (0x68)) which I think might be pretty much related.

Here is the minimal snippet I used to reproduce this bug :

const { Client } = require('ldapts');

const client = new Client({
	url: 'ldaps://openldap:636',
	timeout: 5000,
	connectTimeout: 5000,
});

let entry = {
	cn: 'firstname  lastname',
	objectclass: ['organizationalPerson', 'person', 'inetOrgPerson', 'top'],
	pwdReset: 'TRUE',
	givenName : 'firstname',
	sn : 'lastname',
	mail : '[email protected]',
	uid : '[email protected]',
	userPassword : null, // This is the problem, leaving this undefined works as expected
};

let dn = `uid=${entry.uid},ou=Users,dc=test,dc=com`;

const doStuff = async() => {
	try {
		await client.bind(process.env.LDAP_DN, process.env.LDAP_PASSWORD);
		await client.add(dn, entry);
		console.log('I added my entry');
	} finally {
		//await client.unbind(); // This just fails differently...
	}
}

doStuff().then().catch(console.error);

Search & Modify with Buffer object

Hi,
Superb library!

Is it possible to add a ignore parsing array option on client.modify and client.search to receive the raw buffer instead of the parsed response, ( in SearchEntry.js ) as it converts everything to a string.

{ scope: 'sub', filter: '(&(sAMAccountName=someAccount))', attributes: ['thumbnailPhoto', 'dn', 'name', 'something...'], noparsing: ['thumbnailPhoto'], }

essentially im trying to get the thumbnailPhoto which is usually a binary photo
and then save the photo to file, but as it is already converted to string, some characters gets mangled by the encoding and Im unable to get the correct output.

ldapjs lib has the same issue where object.entries are strings but since it pushes the full object back, the data was still accessable at object.attributes[0].buffers[0];

Buffer.from(object.attributes[0].buffers[0], 'hex').toString('binary');

not totally sure how to send a "modify" replace in binary form, clues?

TLS Connection to LDAPS

Package Version: 2.6.1
Node: 10
AD protocol: ldaps

I am trying to establish a TLS connection between the AD and my node service however I am getting an error
unable to get local issuer certificate

My implementation of the code looks something like this, where I am getting the certs from the AWS SSM.

const createConnection = async () => {
  const connection = config && config.connection
  if (process.env.REJECT_UNAUTHORISED === 'true') {
    const rootCA = await getParam(process.env.CA_ROOT_CERT)
    const bufferedCACert = Buffer.from(rootCA, 'base64').toString('utf8')
    const subCA = await getParam(process.env.CA_SUB_CERT)
    const bufferedSubCACert = Buffer.from(subCA, 'base64').toString('utf8')
    connection.tlsOptions.ca = [bufferedCACert, bufferedSubCACert]
  }

  const client = new Client(connection)
  return client
}

The AD is using certificates issued by the global CA.

I tried using the startTls function but saw an error message indicating that the Tls connection was already established and when reviewing the code source, I can verify that it is the case.

Also a second point, why do I need to add ca in the config?
https://nodejs.org/docs/latest-v10.x/api/tls.html. The documentation indicates that I need to add only if it is a self signed?

Can anyone help me out please?

SASL external with TLS client certs

I added a similar request/question to ldapjs but thought I'd ask here as well, since this seems better maintained. I've read the issues and docs and am pretty sure this is not supported, but wanted to check anyway.

I need to use LDAP (as a client) over ldaps:// where the tlsOptions contain a key and cert for TLS client authentication. I.e. the LDAP server (the peer) will verify my LDAP client via TLS client authentication.

But I want this TLS client auth to be used with the "SASL EXTERNAL mechanism" (page 29 in https://docs.ldap.com/specs/rfc4422.txt).
As far as I can understand, this is something I can currently achieve with ldapsearch by supplying the -Y EXTERNAL.

So with all that in mind, since ldapts already supports TLS using client cert/key, is it possible for it to use -Y EXTERNAL so that they are also used as the SASL mechanism "EXTERNAL"?

Change class not exposed in exports

The change class is inaccessible to make modifications when importing the module.

Suggest the following line in index.ts would work
export * from './Change';

This would allow it on import as either:
const { Client, Change } = require('ldapts');
or
const Change = require('ldapts').Change;

Please let your errors include the original error message too.

In your bind function, and most others, you send the LDAP command and then, if the response was not 0 (Success) then you call StatusCodeParser.parse(result.status);

But this throws away the original message that was part of the results and replaces it with your error messages.

Honestly I really don't want your error messages. I want the original message.

At a minimum can you include both your message and the original message??

Like this: StatusCodeParser.parse(result.status, result.errorMessage);

noisy log exposes secrets

When using your library it seems to log quite noisily whatever is going on. Basically, this might be a good idea. But when it comes to logging bind operations it is covering used passwords in cleartext which seems to be unnecessary not to say probable source for collecting a wide range of user credentials.

Reconnection policy

As it goes with external services, if the service goes temporarily unavailable (by closing the socket), the client will not try to reconnect again.

With the current API, it is not possible, to define custom event handlers for implementing a reconnection policy.

My proposed suggestion would be (similar to ldapjs package), to have a new object setting in ldap.Client's constructor named reconnection, which would define, if the reconnection should be enabled & intervals for automatic reconnection.

The next solution (which could, by the way, exist with the previous solution) would be to extend client as an EventEmitter object. That way, users of this library could extend event behavior to their liking.

Error on connection

Unhandled Exception TypeError: Cannot read property 'removeAllListeners' of undefined
     at Client._onConnect (/usr/src/app/node_modules/ldapts/Client.js:456:21)
     at Socket.socket.once (/usr/src/app/node_modules/ldapts/Client.js:431:26)
     at Object.onceWrapper (events.js:273:13)
     at Socket.emit (events.js:187:15)
     at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1149:10) undefined

Getting this error on trying to connect. Let me know if you guys need some specifics.

Is there other way to get error instead exceptions?

This module solve an error that happens with ldapjs. I'm excited to use ldapts, but I see a big problem because the errors do not handle with return codes but exceptions.
try-catch expression can affect to performance in app. Is there other way to handle errors and do not use try-catch?

how to limit connection time with LDAP server?

The module works well! There is a question. If the LDAP server is available, then everything works, but if the LDAP server is not available (fell, does not respond to ping), then client.bind (...) waits a very long time. It would be nice to have the ability to set the maximum wait time for server availability

Client EXOP fails with TypeError: Cannot read property 'toString' of null

Using Active Directory this function fails in the method ldapClient.exop(....)

export async function authenticate(username, password) {

    // Declare ldap client connection once and enable reconnect
    // and listen for connect events (see below)
    const ldapClient = new Client({
        url: `ldap://${config.ldapHost}`,
        reconnect: {
            initialDelay: 100,
            maxDelay: 1000,
            failAfter: 10
        },
    });
    await ldapClient.exop('1.2.840.113556.1.4.1781'); // throws error 'toString' of null
    await ldapClient.bind(username ,password);
    await ldapClient.unbind();
}

I get this stack trace

TypeError: Cannot read property 'toString' of null
    at new ResultCodeError (/Users/slaursen/dev10/deploy/ep-auth-server/node_modules/ldapts/errors/resultCodeErrors/ResultCodeError.js:7:51)
    at new UnknownStatusCodeError (/Users/slaursen/dev10/deploy/ep-auth-server/node_modules/ldapts/errors/resultCodeErrors/UnknownStatusCodeError.js:6:9)
    at Function.parse (/Users/slaursen/dev10/deploy/ep-auth-server/node_modules/ldapts/StatusCodeParser.js:80:24)
    at Client.exop (/Users/slaursen/dev10/deploy/ep-auth-server/node_modules/ldapts/Client.js:218:58)
    at process._tickCallback (internal/process/next_tick.js:68:7)

[bug] filter Missing paren

Hi,

I am getting Missing paren error for filter bellow:

"(&(userPrincipalName=sostad*)(&(objectClass=user))(|(objectClass=person))(!(objectClass=computer)(objectClass=group)))"

It looks like my filter is correct and error is a bug!
could you please take a look at it and let me know what's wrong in my filter?

error:

Missing paren: (&(userPrincipalName=sostad*)(&(objectClass=user))(|(objectClass=person))(!(objectClass=computer)(objectClass=group))). Full string: (&(userPrincipalName=sostad*)(&(objectClass=user))(|(objectClass=person))(!(objectClass=computer)(objectClass=group)))

Escaping filters & distinguished names

First of all, I want to thank you for this repository. We switched our project's LDAP implementation from ldapjs to yours and it seems to be working just fine. Being a TypeScript project, it's just a cherry on top.

However, quickly glancing over the code, I couldn't find escaping mechanism of certain characters to prevent LDAP injections.

Is there any way of incorporating this into the project? Right now, it seems that I am forced to use ldap-escape library.

The `sizeLimit` option is limiting result to N-1 items

It looks like when sizeLimit option is defined, the library is returning N-1 items only (one item is missing). Example: When I selected sizeLimit=2, one item (user) were been returned only.

Tested on: LDAPTS version: 1.4.2, LDAP server: Microsoft Active LDAP


Note: When sizeLimit is not defined, the real limit looks like 100 items. The default value is not documented in source code. But I am not sure if this limit is caused by lib, or it is default value somewhere in AD LDAP server - so adding this info as minor information only.

LDAP_MATCHING_RULE_IN_CHAIN yields no result

Hi @jgeurts,

it's me - again ;)

I am currently testing the following ldap query (&(objectClass=user)(displayName=SomeIdentifier)(memberOf:1.2.840.113556.1.4.1941:=CN=A Parent Group)) to search for a user in a group that contains many children groups. According to https://docs.microsoft.com/en-us/windows/desktop/adsi/search-filter-syntax 1.2.840.113556.1.4.1941 translates into a LDAP_MATCHING_RULE_IN_CHAIN.

The query gets correctly translated into a big AndFilter with three filters by ldap ts:

AndFilter {
  type: 160,
  filters: [
    EqualityFilter {
      type: 163,
      attribute: 'objectClass',
      value: 'user'
    },
    EqualityFilter {
      type: 163,
      attribute: 'displayName',
      value: 'SomeIdentifier'
    },
    ExtensibleFilter {
      type: 169,
      matchType: 'memberOf',
      rule: '1.2.840.113556.1.4.1941',
      dnAttributes: false,
      value: 'CN=A Parent Group'
    }
  ]
}

Checking the buffer in the SearchRequest it seems that it gets at least written into the stream correctly (even though one can't see much):

�b�
   objectClassuser�
                   displayNameSomeIdentifier�3�1.2.840.113556.1.4.1941memberOf�CN=A Parent Group0LLL

Using the following ldapsearch I get a result:

ldapsearch -H ldaps://myldapserver.com -x -w 'verySecret' -D 'myUser' -b 'DC=global,DC=corp,...' "(&(objectClass=user)(displayName=SomeIdentifier)(memberOf:1.2.840.113556.1.4.1941:=CN=A Parent Group))"

Would be awesome if you could look into this. As always: More than happy to help!

Regards,
stiller-leser

Example (or even elaboration) for creating a Control

I'm need to perform an LDAP query using an extended control. (Specifically, Active Directory using 1.2.840.113556.1.4.417).

The documentation doesn't have any examples for creating a Control to pass to the search client. I've tried a few things but can't figure it out on my own.

I found this example for ldapjs but nothing similar works with ldapts.

Can you give any pointers for creating a Control object?

TypeError: Invalid data, chunk must be a string or buffer, not object

This is not a bug report per se, I just wanted to share something that got me stuck with my current setup and maybe open a discussion about a PR.

I was getting this error when trying to bind to the ldap server given in the bind example:

TypeError: Invalid data, chunk must be a string or buffer, not object
at Socket.write (net.js:704:11)
at Client._send (app/server.js:22724:21)
at Client._sendSearch (app/server.js:22549:35)
at Client.search (app/server.js:22531:20)

After some investigation I identified the asn1 lib used to create the buffer from the message as being the problem. Internet told me it is barely maintained anymore and offered asn1-ber as a dropin replacement. I made an alias in my webpack config to not modify anything and it started working straight away:

resolve: {
        alias: {
            'asn1': 'asn1-ber'
        },
}

I'm not sure if there is any downside to using the alternative lib but the author says there isn't. Seems to be working for me and my particular setup (Meteor.js). But if other people are having the same problem it might justify a change directly in ldapts rather than the webpack workaround.

Great lib in any case, works like a dream, thank you so much.

binding with empty bindDN and password

const { Client } = require('ldapts');

const url = 'ldap://ldap.forumsys.com:389';
const bindDN = '';
const password = '';

async function authenticate(bindDN, password) {
  const client = new Client({
    url,
  });

  let isAuthenticated;
    try {
      await client.bind(bindDN, password);
      isAuthenticated = true;
    } catch (ex) {
      isAuthenticated = false;
    } finally {
      await client.unbind();
    }
  return isAuthenticated;
}

I created a function with a given example. When bindDN and password are empty string, isAuthenticated returns true. I find that isAuthenticated in Try block gets assigned 'true'. In my understanding, await delays until client.bind function returns, right? If empty bindDN and password gets an error in binding, it should jumps into catch block. Any clue?

FR: expose errors as part of API

I'd like to detect, whether some bind request is failing due to technical issues (like connection timeouts) or because of provided user DN/password being wrong. Currently I have to import the error classes from ldapts/errors for that. Is it possible to include the errors with the base API available from ldapts itself?

Can't do a search query if connection got closed by LDAP server

I switched to this library from ldapjs to try to fix a bug when connection get closed by LDAP server.
By searching some issues, thought this problem is fixed, but that is not the case.

I get disconnected from LDAP server after 15 minutes of inactivity.
When I do new request after more than 15 minutes, I got error like this
Selection_165

It would be nice if you can handle this case and reconnect client again if possible.

My current workaround is in catch block, to check error.message does it contain this specific code 000004DC, and if that is the case, bind client again, and do request again

But I saw in your library that on every search you check is client connected. Don't know is there a way to listen to event when server close connection, or to check something else also and to call connect method again

TS4090: (TS) Conflicting definitions for 'node'

I'm getting a TS4090 error now in my project since updating ldapts. My work around is to delete this typings file from ldapts. The node typings at my project level is apparently newer and typescript doesn't know what to do.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.