ldap-authentication 4.2.0 → 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 +97 -0
- package/README.md +45 -0
- package/example/fetch-users.mjs +36 -0
- package/example/index.js +34 -36
- package/example/starttls.mjs +40 -0
- package/example/verify-user-exists.js +45 -0
- package/index.d.ts +32 -1
- package/index.js +44 -3
- package/package.json +5 -4
- package/scripts/test-local.sh +33 -0
- package/test/jasmine.js +7 -3
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
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
17
|
-
}
|
|
21
|
+
attributes: ['uid', 'sn', 'cn'],
|
|
22
|
+
})
|
|
23
|
+
console.log('admin mode ->', JSON.stringify(user, null, 2))
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
40
|
-
|
|
41
|
-
ldapOpts: {
|
|
42
|
-
|
|
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: '
|
|
51
|
-
groupMemberAttribute: '
|
|
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().
|
|
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<
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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.
|
|
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",
|
|
@@ -48,9 +49,9 @@
|
|
|
48
49
|
},
|
|
49
50
|
"homepage": "https://github.com/shaozi/ldap-authentication#readme",
|
|
50
51
|
"dependencies": {
|
|
51
|
-
"ldapts": "^
|
|
52
|
+
"ldapts": "^9.0.0"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
|
-
"jasmine": "^
|
|
55
|
+
"jasmine": "^7.0.0"
|
|
55
56
|
}
|
|
56
57
|
}
|
|
@@ -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
|
package/test/jasmine.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
var Jasmine = require('jasmine')
|
|
2
2
|
var jasmine = new Jasmine()
|
|
3
3
|
|
|
4
|
+
// jasmine 7: environment options (random, seed, stopSpecOnExpectationFailure)
|
|
5
|
+
// are nested under the `env` key
|
|
4
6
|
jasmine.loadConfig({
|
|
5
7
|
spec_dir: 'test',
|
|
6
8
|
spec_files: ['**/*[sS]pec.?(m)js'],
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
env: {
|
|
10
|
+
random: false,
|
|
11
|
+
seed: null,
|
|
12
|
+
stopSpecOnExpectationFailure: false,
|
|
13
|
+
},
|
|
10
14
|
})
|
|
11
15
|
|
|
12
16
|
class MyConsoleReporter {
|