ldap-authentication 4.2.1 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,97 @@
1
+ # AGENTS.md - guidance for AI coding agents working in this repo
2
+
3
+ ## What this repo is
4
+
5
+ `ldap-authentication` - a small Node.js library that authenticates users against an
6
+ LDAP/AD server. It is a thin wrapper around `ldapts` (its only runtime dependency).
7
+
8
+ There is **no build step**. The package is published as-is:
9
+
10
+ - `index.js` - CJS implementation (all runtime logic lives here)
11
+ - `index.mjs` - ESM re-export entry point
12
+ - `index.d.ts` - hand-written TypeScript types (keep in sync with `index.js` and the README)
13
+ - `test/` - jasmine integration specs (require a running LDAP server)
14
+ - `example/` - runnable usage examples (require a running LDAP server)
15
+ - `docker/` + `docker-compose.yml` - the seeded OpenLDAP test server
16
+ - `dist/` - **stale legacy build (gitignored, not referenced by package.json, not
17
+ published). Ignore it; do not edit or "fix" it.**
18
+
19
+ ## Running the tests
20
+
21
+ The specs are integration tests against a seeded OpenLDAP container:
22
+
23
+ 1. `docker compose -f docker-compose.yml up -d` - LDAP on `ldap://localhost:1389`,
24
+ LDAPS on `ldaps://localhost:1636`
25
+ 2. Wait until an admin bind succeeds (server is ready when this works)
26
+ 3. `INGITHUB=true npm test` - the `INGITHUB` env var switches the specs from the
27
+ docker-internal URL (`ldap://ldap:1389`) to `ldap://localhost:1389`
28
+ 4. `docker compose -f docker-compose.yml down`
29
+
30
+ Or all of the above in one shot: `npm run test:local` (`scripts/test-local.sh`).
31
+
32
+ Seeded data (see `docker/ldap/*.ldif`): domain `dc=example,dc=com`; users
33
+ `cn=gauss` and `cn=einstein` in `ou=users` (both with password `password`); group
34
+ `cn=科学A部` in `ou=groups` containing gauss; admin
35
+ `cn=read-only-admin,dc=example,dc=com` (password `password`).
36
+ `test/binary.spec.js` MUTATES the directory (adds `jpegPhoto` to gauss), so spec
37
+ order matters - keep the deterministic order: in jasmine 7 the env options
38
+ (`random`, `seed`, `stopSpecOnExpectationFailure`) must be nested under the `env`
39
+ key in `loadConfig` (see `test/jasmine.js`).
40
+
41
+ ## Code style
42
+
43
+ Prettier (`.prettierrc`): no semicolons, single quotes, 2-space indent, trailing
44
+ commas in `es5`. Match the existing style when editing `index.js`.
45
+
46
+ ## Version control - IMPORTANT: this is a Jujutsu (jj) repo
47
+
48
+ This repo is managed with **jj** (see `.jj/`). Do NOT use `git commit` for commits;
49
+ use jj:
50
+
51
+ 1. `jj describe -m "<message>"` - commits the current working-copy changes
52
+ (after `jj describe`, jj may warn that the commit became immutable and create an
53
+ empty working-copy commit on top - that is normal and harmless)
54
+ 2. `jj bookmark move master` - move the local `master` bookmark to the new commit
55
+ (defaults to the working copy, `@`)
56
+ 3. `jj git push --bookmark master` - push to `origin`
57
+ (`git@github.com:shaozi/ldap-authentication.git`). The remote prints a
58
+ "Bypassed rule violations ... Changes must be made through a pull request"
59
+ notice on master - that is expected (maintainer bypass) and not an error.
60
+
61
+ The maintainer works detached (no branch); commits go on the `master` bookmark
62
+ with a single descriptive message, e.g.
63
+ `"Add fetchUsers() to search all users via paged admin-bound lookup (#3); bump to 4.1.0"`.
64
+
65
+ ## Releases
66
+
67
+ 1. Bump `version` in **both** `package.json` and `package-lock.json` (the lockfile
68
+ has it twice: top-level and under `packages."").version`). Follow semver
69
+ (new feature = e.g. 4.1.0, docs/deps-only = patch).
70
+ 2. Commit + push as above.
71
+ 3. `jj tag set vN.N.N -r @-` (tag the just-pushed master commit), then
72
+ `git push origin vN.N.N` (jj has no tag push in this version).
73
+ 4. `gh release create vN.N.N --title "vN.N.N" --notes "..."` - creating the
74
+ release triggers `.github/workflows/npm-publish.yml` (OIDC), which runs
75
+ `npm ci && npm publish`.
76
+ 5. Verify: `gh run list` (the `Publish Package` run should succeed) and
77
+ `npm view ldap-authentication@N.N.N`. Note that the npm registry's packument
78
+ can lag the publish by a couple of minutes - check the
79
+ `https://registry.npmjs.org/ldap-authentication` `time`/`versions` before
80
+ re-publishing. The legacy `publish.yml` workflow also triggers on release and
81
+ its `npm publish` step fails with E404 because it duplicates the other
82
+ workflow - that failure is expected/harmless; `publish.yml` is a candidate
83
+ for deletion.
84
+
85
+ ## CI
86
+
87
+ `integration-test.yml` runs on push/PR to `master`: builds the LDAP container from
88
+ `docker-compose.yml`, runs `npm ci` + `npm run test` with `INGITHUB=true` on a
89
+ Node 22.x/24.x matrix. Release events additionally trigger the publish workflows.
90
+
91
+ ## When adding a new option
92
+
93
+ Update all of: the validation in `index.js` (`authenticateResult` /
94
+ `fetchUsers`), the JSDoc in `index.js`, `index.d.ts` (`AuthenticationOptions` and
95
+ / or `FetchUsersOptions`), and the README (Parameters list, the
96
+ options-by-mode table, and an example if the option changes behavior). Add or
97
+ extend the specs in `test/` (integration specs run against the seeded container).
package/README.md CHANGED
@@ -212,6 +212,18 @@ auth()
212
212
  - For `ldaps://` URLs, omit `starttls` and the connection will use TLS from the start
213
213
  - TLS options like `rejectUnauthorized`, `minVersion`, and `servername` can be specified in `ldapOpts.tlsOptions`
214
214
 
215
+ #### Runnable examples
216
+
217
+ The [example/](example/) directory contains complete, runnable scripts: admin auth, self auth, group lookup,
218
+ `fetchUsers`, `verifyUserExists`, and StartTLS. They run against the bundled seeded test server
219
+ (start it via `docker compose up -d`, or point `LDAP_URL` at your own server):
220
+
221
+ ```sh
222
+ docker compose up -d # seeded OpenLDAP on localhost:1389 / 1636
223
+ node example/fetch-users.mjs # or any other script in example/
224
+ docker compose down
225
+ ```
226
+
215
227
  ## Parameters
216
228
 
217
229
  - `ldapOpts`: This is passed to `ldapts` client directly
@@ -254,6 +266,15 @@ auth()
254
266
  - `groupMemberAttribute`: if specified with groupClass and groupsSearchBase, will be used as member name (if not specified this defaults to `member`) in search filter for authenticated user groups
255
267
  - `groupMemberUserAttribute`: if specified with groupClass and groupsSearchBase, will be used as the attribute on the user object (if not specified this defaults to `dn`) in search filter for authenticated user groups
256
268
 
269
+ ### Which options for which mode?
270
+
271
+ | Mode (call) | Required | Commonly used in addition |
272
+ |---|---|---|
273
+ | Admin authenticate (`authenticate`) | `ldapOpts`, `adminDn`, `adminPassword`, `userPassword`, `userSearchBase`, `usernameAttribute` or `usernameFilter`, `username` | `attributes`, `groupsSearchBase`, `groupClass`, `starttls` |
274
+ | Self authenticate (`authenticate`) | `ldapOpts`, `userDn`, `userPassword` | `userSearchBase`, `usernameAttribute`, `attributes`, `groupsSearchBase`, `starttls` |
275
+ | Verify user exists (`authenticate` with `verifyUserExists: true`) | `ldapOpts`, `adminDn`, `adminPassword`, `userSearchBase`, `usernameAttribute` or `usernameFilter`, `username` | `attributes`, `groupsSearchBase`, `starttls` |
276
+ | Fetch all users (`fetchUsers`) | `ldapOpts`, `adminDn`, `adminPassword`, `userSearchBase` | `userFilter`, `attributes`, `pageSize`, `starttls` |
277
+
257
278
  ## Returns
258
279
 
259
280
  The user object if `authenticate()` is success.
@@ -279,6 +300,30 @@ AuthenticationResult object has the following fields:
279
300
  - `message`: authentication message array, which contains server messages
280
301
  - `client`: ldapClient instance
281
302
 
303
+ ## Active Directory notes
304
+
305
+ - A typical admin bind DN is a service or admin account, e.g. `cn=Administrator,cn=users,dc=example,dc=com`,
306
+ or a dedicated LDAP sync account.
307
+ - Username attributes: `sAMAccountName` for logins like `jdoe`, `userPrincipalName` for `jdoe@example.com`.
308
+ To look a user up by either at once, use
309
+ `usernameFilter: '(|(sAMAccountName={{username}})(userPrincipalName={{username}}))'`.
310
+ - Set `userSearchBase` to the OU containing the users (e.g. `ou=users,dc=example,dc=com`):
311
+ the search is faster and avoids `AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS`.
312
+ - `fetchUsers()` uses LDAP paged results, so Active Directory's usual 1000-entry limit per search
313
+ is not an issue (adjust the page size with `pageSize` if needed).
314
+ - Binary attributes such as `thumbnailPhoto` should be requested as `thumbnailPhoto;binary`;
315
+ they are returned as base64-encoded strings.
316
+
317
+ ## Troubleshooting
318
+
319
+ | Symptom | Likely cause / fix |
320
+ |---|---|
321
+ | `ECONNREFUSED`, `ETIMEDOUT`, or other connect errors | `ldapOpts.url` is wrong or the server is unreachable. Check the URL, the network/firewall, and `connectTimeout`. |
322
+ | `LdapAuthenticationError` with `admin bind failed` / `user bind failed` | Wrong `adminDn`/`adminPassword`, or `userDn`/`userPassword` in self mode. Verify the bind manually, e.g. `ldapsearch -b dc=example,dc=com -D <dn> -w <password> dn`. |
323
+ | `identity not found` (`AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND`) | The user does not exist under `userSearchBase`, or `usernameAttribute`/`username`/`usernameFilter` does not match the attribute(s) stored on the server. |
324
+ | `identity ambiguous` (`AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS`) | The search matched multiple entries - narrow `userSearchBase` or make the filter more specific. |
325
+ | `Invalid credentials` (`AUTH_RESULT_FAILURE_CREDENTIAL_INVALID`) | The user was found but the password is wrong. |
326
+ | TLS certificate errors | For self-signed certificates use `tlsOptions: { rejectUnauthorized: false }`; add `servername` for SNI. Use `ldaps://` (without `starttls`) or `ldap://` with `starttls: true`. |
282
327
 
283
328
  ## Old Stuff
284
329
 
@@ -0,0 +1,36 @@
1
+ // Fetch all users with the admin account - the search is paged, so more
2
+ // than 1000 entries can be returned.
3
+ // Requires the bundled seeded test server: `docker compose up -d`
4
+ // (or set LDAP_URL to point at your own server).
5
+
6
+ import { fetchUsers } from '../index.mjs'
7
+
8
+ const ldapOpts = {
9
+ url: process.env.LDAP_URL || 'ldap://localhost:1389',
10
+ }
11
+
12
+ const baseOptions = {
13
+ ldapOpts,
14
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
15
+ adminPassword: 'password',
16
+ userSearchBase: 'dc=example,dc=com',
17
+ }
18
+
19
+ // 1. All users (default filter: entries with a uid or sAMAccountName), all attributes
20
+ let users = await fetchUsers(baseOptions)
21
+ console.log('all users ->', users.map((user) => user.uid ?? user.cn))
22
+
23
+ // 2. Custom filter + attribute selection
24
+ users = await fetchUsers({
25
+ ...baseOptions,
26
+ userFilter: '(uid=gauss)',
27
+ attributes: ['uid', 'sn'],
28
+ })
29
+ console.log('filtered ->', users)
30
+
31
+ // 3. Small page size (paged results are always on)
32
+ users = await fetchUsers({
33
+ ...baseOptions,
34
+ pageSize: 1,
35
+ })
36
+ console.log('paged ->', users.length, 'entries with pageSize 1')
package/example/index.js CHANGED
@@ -1,58 +1,56 @@
1
+ // Admin and self authentication, with group lookup.
2
+ // Requires the bundled seeded test server: `docker compose up -d`
3
+ // (or set LDAP_URL to point at your own server).
4
+
1
5
  const { authenticate } = require('../index')
2
6
 
7
+ const url = process.env.LDAP_URL || 'ldap://localhost:1389'
8
+
3
9
  async function auth() {
4
- // auth with admin
5
- let options = {
6
- ldapOpts: {
7
- url: 'ldap://localhost:1389',
8
- // tlsOptions: { rejectUnauthorized: false }
9
- },
10
+ // 1. Admin mode: bind as admin, find the user, then bind as the user.
11
+ // Restrict `attributes` so the server does not return everything (including
12
+ // userPassword).
13
+ let user = await authenticate({
14
+ ldapOpts: { url },
10
15
  adminDn: 'cn=read-only-admin,dc=example,dc=com',
11
16
  adminPassword: 'password',
12
17
  userPassword: 'password',
13
18
  userSearchBase: 'dc=example,dc=com',
14
19
  usernameAttribute: 'uid',
15
20
  username: 'gauss',
16
- // starttls: false
17
- }
21
+ attributes: ['uid', 'sn', 'cn'],
22
+ })
23
+ console.log('admin mode ->', JSON.stringify(user, null, 2))
18
24
 
19
- let user = await authenticate(options)
20
- console.log(`user = ${JSON.stringify(user, null, 2)}`)
21
-
22
- // auth with regular user
23
- options = {
24
- ldapOpts: {
25
- url: 'ldap://ldap.forumsys.com',
26
- // tlsOptions: { rejectUnauthorized: false }
27
- },
28
- userDn: 'uid=einstein,dc=example,dc=com',
25
+ // 2. Self mode: the user binds with its own DN and gets its details
26
+ user = await authenticate({
27
+ ldapOpts: { url },
28
+ userDn: 'cn=einstein,ou=users,dc=example,dc=com',
29
29
  userPassword: 'password',
30
30
  userSearchBase: 'dc=example,dc=com',
31
31
  usernameAttribute: 'uid',
32
32
  username: 'einstein',
33
- // starttls: false
34
- }
35
-
36
- user = await authenticate(options)
37
- console.log(`user = ${JSON.stringify(user, null, 2)}`)
33
+ attributes: ['uid', 'sn'],
34
+ })
35
+ console.log('self mode ->', { uid: user.uid, sn: user.sn })
38
36
 
39
- // Getting user group info
40
- options = {
41
- ldapOpts: {
42
- url: 'ldap://ldap.forumsys.com',
43
- },
44
- userDn: 'uid=gauss,dc=example,dc=com',
37
+ // 3. Admin mode with group lookup
38
+ user = await authenticate({
39
+ ldapOpts: { url },
40
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
41
+ adminPassword: 'password',
45
42
  userPassword: 'password',
46
43
  userSearchBase: 'dc=example,dc=com',
47
44
  usernameAttribute: 'uid',
48
45
  username: 'gauss',
49
46
  groupsSearchBase: 'dc=example,dc=com',
50
- groupClass: 'groupOfUniqueNames',
51
- groupMemberAttribute: 'uniqueMember',
52
- }
53
-
54
- user = await authenticate(options)
55
- console.log(`user = ${JSON.stringify(user, null, 2)}`)
47
+ groupClass: 'groupOfNames',
48
+ groupMemberAttribute: 'member',
49
+ })
50
+ console.log('with groups ->', user.groups.map((group) => group.cn))
56
51
  }
57
52
 
58
- auth().then()
53
+ auth().catch((error) => {
54
+ console.error(error)
55
+ process.exit(1)
56
+ })
@@ -0,0 +1,40 @@
1
+ // Authenticate over STARTTLS (ldap:// URL upgraded to TLS).
2
+ // Requires an LDAP server that supports TLS certificates.
3
+ // (The bundled test container generates TLS config but does not enable it,
4
+ // so this script prints a notice instead of failing in that case.)
5
+
6
+ import { authenticate } from '../index.mjs'
7
+
8
+ const isTlsNotSupported = (error) =>
9
+ error instanceof Error &&
10
+ /secure TLS connection was established/i.test(error.message)
11
+
12
+ // The bundled container uses a self-signed certificate, so the test
13
+ // disables certificate verification. Do NOT do this in production;
14
+ // provide your CA in tlsOptions.ca instead.
15
+ try {
16
+ const user = await authenticate({
17
+ ldapOpts: {
18
+ url: process.env.LDAP_URL || 'ldap://localhost:1389',
19
+ tlsOptions: {
20
+ rejectUnauthorized: false, // self-signed certificate (test only)
21
+ },
22
+ },
23
+ starttls: true, // upgrade the ldap:// connection to TLS
24
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
25
+ adminPassword: 'password',
26
+ userPassword: 'password',
27
+ userSearchBase: 'dc=example,dc=com',
28
+ usernameAttribute: 'uid',
29
+ username: 'gauss',
30
+ })
31
+ console.log('starttls auth ->', user.uid)
32
+ } catch (error) {
33
+ if (isTlsNotSupported(error)) {
34
+ console.log('This LDAP server does not support TLS - STARTTLS could not be established.')
35
+ console.log('Run this example against a server with TLS enabled (or use an ldaps:// URL).')
36
+ } else {
37
+ console.error(error)
38
+ process.exit(1)
39
+ }
40
+ }
@@ -0,0 +1,45 @@
1
+ // Verify that a user exists (without checking the password).
2
+ // Requires the bundled seeded test server: `docker compose up -d`
3
+ // (or set LDAP_URL to point at your own server).
4
+
5
+ const { authenticate, LdapAuthenticationError } = require('../index')
6
+
7
+ const url = process.env.LDAP_URL || 'ldap://localhost:1389'
8
+
9
+ async function verify() {
10
+ // Existing user
11
+ let user = await authenticate({
12
+ ldapOpts: { url },
13
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
14
+ adminPassword: 'password',
15
+ verifyUserExists: true,
16
+ userSearchBase: 'dc=example,dc=com',
17
+ usernameAttribute: 'uid',
18
+ username: 'gauss',
19
+ })
20
+ console.log('gauss exists ->', user.uid)
21
+
22
+ // Non-existing user throws LdapAuthenticationError
23
+ try {
24
+ await authenticate({
25
+ ldapOpts: { url },
26
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
27
+ adminPassword: 'password',
28
+ verifyUserExists: true,
29
+ userSearchBase: 'dc=example,dc=com',
30
+ usernameAttribute: 'uid',
31
+ username: 'does-not-exist',
32
+ })
33
+ } catch (error) {
34
+ if (error instanceof LdapAuthenticationError) {
35
+ console.log('does-not-exist ->', error.message)
36
+ } else {
37
+ throw error
38
+ }
39
+ }
40
+ }
41
+
42
+ verify().catch((error) => {
43
+ console.error(error)
44
+ process.exit(1)
45
+ })
package/index.d.ts CHANGED
@@ -37,6 +37,29 @@ declare module 'ldap-authentication' {
37
37
  readonly client: any
38
38
  }
39
39
 
40
+ /**
41
+ * A single group entry returned on `user.groups` when group lookup is
42
+ * enabled (`groupsSearchBase` + `groupClass`). `objectName` mirrors `dn`
43
+ * for backward compatibility with the old ldapjs-based API.
44
+ */
45
+ export interface LdapGroupEntry {
46
+ dn: string
47
+ objectName?: string
48
+ [attr: string]: any
49
+ }
50
+
51
+ /**
52
+ * A single user object returned by `authenticate()` / `fetchUsers()`.
53
+ * Always contains the entry's `dn`; other attribute values are
54
+ * `string`/`string[]` (or a base64 string for `;binary` attributes), and
55
+ * `groups` is present when group lookup is enabled.
56
+ */
57
+ export interface LdapUserEntry {
58
+ dn: string
59
+ groups?: LdapGroupEntry[]
60
+ [attr: string]: any
61
+ }
62
+
40
63
  export interface FetchUsersOptions {
41
64
  ldapOpts: ClientOptions
42
65
  adminDn: string
@@ -58,15 +81,23 @@ declare module 'ldap-authentication' {
58
81
  starttls?: boolean
59
82
  }
60
83
 
84
+ /**
85
+ * Authenticate a user against the LDAP server. Kept as `Promise<any>` for
86
+ * backward compatibility; the resolved value has the shape of
87
+ * {@link LdapUserEntry}. Throws {@link LdapAuthenticationError} on failure.
88
+ */
61
89
  export function authenticate(options: AuthenticationOptions): Promise<any>
90
+ /** Same options as {@link authenticate} but never throws on failure; returns an {@link AuthenticationResult}. */
62
91
  export function authenticateResult(options: AuthenticationOptions): Promise<AuthenticationResult>
63
92
 
64
93
  /**
65
94
  * Bind with the admin account and search all users under `userSearchBase`.
66
95
  * The search always uses paged results, so results are not limited by the
67
96
  * common server-side limit of 1000 entries.
97
+ *
98
+ * Returns an empty array if no user matches the filter.
68
99
  */
69
- export function fetchUsers(options: FetchUsersOptions): Promise<any[]>
100
+ export function fetchUsers(options: FetchUsersOptions): Promise<LdapUserEntry[]>
70
101
 
71
102
  export class LdapAuthenticationError extends Error {
72
103
  constructor(message: any)
package/index.js CHANGED
@@ -48,6 +48,10 @@ const AUTH_RESULT_FAILURE_UNCATEGORIZED = -4
48
48
  const DEFAULT_FETCH_USERS_FILTER = '(|(uid=*)(sAMAccountName=*))'
49
49
  const DEFAULT_FETCH_USERS_PAGE_SIZE = 1000
50
50
 
51
+ /**
52
+ * Result object returned by {@link authenticateResult}. Inspect `code` (one
53
+ * of the AUTH_RESULT_* constants) and `messages` to classify failures.
54
+ */
51
55
  class AuthenticationResult {
52
56
  #authCode = AUTH_RESULT_FAILURE_UNCATEGORIZED
53
57
  #identity
@@ -613,9 +617,20 @@ async function verifyUserExists(
613
617
  )
614
618
  }
615
619
 
616
- // fetch all users under the search base, using the admin account to search.
617
- // the search always uses paged results so the common server-side limit of
618
- // 1000 entries does not apply.
620
+ /**
621
+ * Fetch all users under `userSearchBase`, using the admin account to search.
622
+ * No individual username or password is required. The search always uses
623
+ * LDAP paged results, so the common server-side limit of 1000 entries does
624
+ * not apply. Returns an empty array if no user matches.
625
+ *
626
+ * @param {FetchUsersOptions} options - required: `ldapOpts` (with `url`),
627
+ * `adminDn`, `adminPassword`, `userSearchBase`; optional: `userFilter`,
628
+ * `attributes`, `explicitBufferAttributes`, `pageSize`, `starttls`.
629
+ * See the types in index.d.ts and the README for details.
630
+ * @returns {Promise<LdapUserEntry[]>} one entry per matched user, each with
631
+ * its `dn` and the returned attributes.
632
+ * @throws {LdapAuthenticationError} if the admin bind or the search fails.
633
+ */
619
634
  async function fetchUsers(options) {
620
635
  assert(
621
636
  options.ldapOpts && options.ldapOpts.url,
@@ -658,6 +673,22 @@ async function fetchUsers(options) {
658
673
  }
659
674
  }
660
675
 
676
+ /**
677
+ * Authenticate a user against the LDAP server.
678
+ *
679
+ * Modes (see the README for a full option reference):
680
+ * - Admin mode: `adminDn` + `adminPassword` + `userSearchBase` +
681
+ * `usernameAttribute` (or `usernameFilter`) + `username` + `userPassword`.
682
+ * The library binds as admin, finds the user's DN, then binds as the user.
683
+ * - Self mode: `userDn` + `userPassword`. Optionally `userSearchBase` and
684
+ * `usernameAttribute` to also return the user's details.
685
+ * - Verify mode: `verifyUserExists: true` with admin credentials; verifies
686
+ * that the user exists without checking the password.
687
+ *
688
+ * @param {AuthenticationOptions} options
689
+ * @returns {Promise<any>} the user object if authentication succeeded.
690
+ * @throws {LdapAuthenticationError} if authentication failed.
691
+ */
661
692
  async function authenticate(options) {
662
693
  const result = await authenticateResult(options)
663
694
 
@@ -670,6 +701,15 @@ async function authenticate(options) {
670
701
  return result.user
671
702
  }
672
703
 
704
+ /**
705
+ * Same options and behavior as {@link authenticate}, but never throws on
706
+ * authentication failure - it returns an {@link AuthenticationResult} whose
707
+ * `code` identifies the outcome (useful for custom error handling).
708
+ *
709
+ * @param {AuthenticationOptions} options
710
+ * @returns {Promise<AuthenticationResult>}
711
+ * @throws {LdapAuthenticationError|Error} only on invalid options or network errors.
712
+ */
673
713
  async function authenticateResult(options) {
674
714
  if (!options.userDn) {
675
715
  assert(options.adminDn, 'Admin mode adminDn must be provided')
@@ -756,6 +796,7 @@ async function authenticateResult(options) {
756
796
  )
757
797
  }
758
798
 
799
+ /** Thrown by authenticate()/authenticateResult()/fetchUsers() on failure; `message` describes the failure. */
759
800
  class LdapAuthenticationError extends Error {
760
801
  constructor(message) {
761
802
  super(message)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldap-authentication",
3
- "version": "4.2.1",
3
+ "version": "4.3.0",
4
4
  "description": "A simple async nodejs library for LDAP user authentication",
5
5
  "main": "index.js",
6
6
  "types": "./index.d.ts",
@@ -17,7 +17,8 @@
17
17
  "node": ">=22.0.0"
18
18
  },
19
19
  "scripts": {
20
- "test": "node test/jasmine.js"
20
+ "test": "node test/jasmine.js",
21
+ "test:local": "bash scripts/test-local.sh"
21
22
  },
22
23
  "repository": {
23
24
  "type": "git",
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env bash
2
+ # One-shot local integration test: start the seeded LDAP test container
3
+ # (docker-compose.yml), wait until it accepts an admin bind, run `npm test`,
4
+ # then stop the container again.
5
+ set -uo pipefail
6
+ cd "$(dirname "$0")/.."
7
+
8
+ cleanup() {
9
+ docker compose -f docker-compose.yml down
10
+ }
11
+ trap cleanup EXIT
12
+
13
+ docker compose -f docker-compose.yml up -d
14
+
15
+ for i in $(seq 1 30); do
16
+ if node -e "
17
+ const ldapts = require('ldapts')
18
+ const c = new ldapts.Client({ url: 'ldap://localhost:1389', connectTimeout: 2000 })
19
+ c.bind('cn=read-only-admin,dc=example,dc=com', 'password')
20
+ .then(() => { c.unbind(); process.exit(0) })
21
+ .catch(() => process.exit(1))
22
+ "
23
+ then
24
+ break
25
+ fi
26
+ if [ "$i" -eq 30 ]; then
27
+ echo 'LDAP server did not become ready in time' >&2
28
+ exit 1
29
+ fi
30
+ sleep 2
31
+ done
32
+
33
+ INGITHUB=true npm test