ldap-authentication 4.2.1 → 4.4.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/.github/workflows/codeql-analysis.yml +12 -10
- package/AGENTS.md +99 -0
- package/README.md +52 -0
- package/docker/ldap/10-ldap-test-data.ldif +14 -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 +48 -2
- package/index.js +370 -468
- package/package.json +3 -2
- package/scripts/test-local.sh +33 -0
- package/test/authentication-result.spec.js +82 -41
- package/test/binary.spec.js +11 -10
- package/test/config.js +9 -0
- package/test/fetch-users.spec.js +23 -32
- package/test/fixtures/jpeg-photo.b64 +1 -0
- package/test/starttls.spec.js +12 -12
- package/test/test.spec.js +123 -41
- package/.github/workflows/publish.yml +0 -53
|
@@ -31,15 +31,20 @@ jobs:
|
|
|
31
31
|
language: [ 'javascript' ]
|
|
32
32
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
|
33
33
|
# Learn more:
|
|
34
|
-
# https://docs.github.com/en/
|
|
34
|
+
# https://docs.github.com/en/code-security/code-scanning/defining-your-code-scanning-policy/configuring-code-scanning-for-your-personal-account-and-repositories/changing-your-code-scanning-configuration#changing-the-languages-that-are-analyzed
|
|
35
35
|
|
|
36
36
|
steps:
|
|
37
37
|
- name: Checkout repository
|
|
38
|
-
uses: actions/checkout@
|
|
38
|
+
uses: actions/checkout@v4
|
|
39
|
+
|
|
40
|
+
- name: Use Node.js 24.x
|
|
41
|
+
uses: actions/setup-node@v4
|
|
42
|
+
with:
|
|
43
|
+
node-version: 24.x
|
|
39
44
|
|
|
40
45
|
# Initializes the CodeQL tools for scanning.
|
|
41
46
|
- name: Initialize CodeQL
|
|
42
|
-
uses: github/codeql-action/init@
|
|
47
|
+
uses: github/codeql-action/init@v4
|
|
43
48
|
with:
|
|
44
49
|
languages: ${{ matrix.language }}
|
|
45
50
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
@@ -47,13 +52,10 @@ jobs:
|
|
|
47
52
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
|
48
53
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
|
49
54
|
|
|
50
|
-
# Autobuild attempts to build any compiled languages
|
|
51
|
-
# If this step fails, then you should remove it and run the build manually (see below)
|
|
55
|
+
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
|
56
|
+
# If this step fails, then you should remove it and run the build manually (see below).
|
|
52
57
|
- name: Autobuild
|
|
53
|
-
uses: github/codeql-action/autobuild@
|
|
54
|
-
|
|
55
|
-
# ℹ️ Command-line programs to run using the OS shell.
|
|
56
|
-
# 📚 https://git.io/JvXDl
|
|
58
|
+
uses: github/codeql-action/autobuild@v4
|
|
57
59
|
|
|
58
60
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
|
59
61
|
# and modify them (or add more) to build your code if your project
|
|
@@ -64,4 +66,4 @@ jobs:
|
|
|
64
66
|
# make release
|
|
65
67
|
|
|
66
68
|
- name: Perform CodeQL Analysis
|
|
67
|
-
uses: github/codeql-action/analyze@
|
|
69
|
+
uses: github/codeql-action/analyze@v4
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
After changing the seed ldifs, rebuild the container first (`docker compose -f
|
|
33
|
+
docker-compose.yml build`) - `up -d` reuses the existing image and the seed
|
|
34
|
+
data is baked into it at build time.
|
|
35
|
+
|
|
36
|
+
Seeded data (see `docker/ldap/*.ldif`): domain `dc=example,dc=com`; users
|
|
37
|
+
`cn=gauss`, `cn=einstein` and `cn=Doe, John` (uid `doe`, used to test DNs with a
|
|
38
|
+
comma in the CN) in `ou=users` (all with password `password`); group
|
|
39
|
+
`cn=科学A部` in `ou=groups` containing gauss; admin
|
|
40
|
+
`cn=read-only-admin,dc=example,dc=com` (password `password`).
|
|
41
|
+
`test/binary.spec.js` MUTATES the directory (adds `jpegPhoto` to gauss), so spec
|
|
42
|
+
order matters - keep the deterministic order: in jasmine 7 the env options
|
|
43
|
+
(`random`, `seed`, `stopSpecOnExpectationFailure`) must be nested under the `env`
|
|
44
|
+
key in `loadConfig` (see `test/jasmine.js`).
|
|
45
|
+
|
|
46
|
+
## Code style
|
|
47
|
+
|
|
48
|
+
Prettier (`.prettierrc`): no semicolons, single quotes, 2-space indent, trailing
|
|
49
|
+
commas in `es5`. Match the existing style when editing `index.js`.
|
|
50
|
+
|
|
51
|
+
## Version control - IMPORTANT: this is a Jujutsu (jj) repo
|
|
52
|
+
|
|
53
|
+
This repo is managed with **jj** (see `.jj/`). Do NOT use `git commit` for commits;
|
|
54
|
+
use jj:
|
|
55
|
+
|
|
56
|
+
1. `jj describe -m "<message>"` - commits the current working-copy changes
|
|
57
|
+
(after `jj describe`, jj may warn that the commit became immutable and create an
|
|
58
|
+
empty working-copy commit on top - that is normal and harmless)
|
|
59
|
+
2. `jj bookmark move master` - move the local `master` bookmark to the new commit
|
|
60
|
+
(defaults to the working copy, `@`)
|
|
61
|
+
3. `jj git push --bookmark master` - push to `origin`
|
|
62
|
+
(`git@github.com:shaozi/ldap-authentication.git`). The remote prints a
|
|
63
|
+
"Bypassed rule violations ... Changes must be made through a pull request"
|
|
64
|
+
notice on master - that is expected (maintainer bypass) and not an error.
|
|
65
|
+
|
|
66
|
+
The maintainer works detached (no branch); commits go on the `master` bookmark
|
|
67
|
+
with a single descriptive message, e.g.
|
|
68
|
+
`"Add fetchUsers() to search all users via paged admin-bound lookup (#3); bump to 4.1.0"`.
|
|
69
|
+
|
|
70
|
+
## Releases
|
|
71
|
+
|
|
72
|
+
1. Bump `version` in **both** `package.json` and `package-lock.json` (the lockfile
|
|
73
|
+
has it twice: top-level and under `packages."").version`). Follow semver
|
|
74
|
+
(new feature = e.g. 4.1.0, docs/deps-only = patch).
|
|
75
|
+
2. Commit + push as above.
|
|
76
|
+
3. `jj tag set vN.N.N -r @-` (tag the just-pushed master commit), then
|
|
77
|
+
`git push origin vN.N.N` (jj has no tag push in this version).
|
|
78
|
+
4. `gh release create vN.N.N --title "vN.N.N" --notes "..."` - creating the
|
|
79
|
+
release triggers `.github/workflows/npm-publish.yml` (OIDC), which runs
|
|
80
|
+
`npm ci && npm publish`.
|
|
81
|
+
5. Verify: `gh run list` (the `Publish Package` run should succeed) and
|
|
82
|
+
`npm view ldap-authentication@N.N.N`. Note that the npm registry's packument
|
|
83
|
+
can lag the publish by a couple of minutes - check the
|
|
84
|
+
`https://registry.npmjs.org/ldap-authentication` `time`/`versions` before
|
|
85
|
+
re-publishing.
|
|
86
|
+
|
|
87
|
+
## CI
|
|
88
|
+
|
|
89
|
+
`integration-test.yml` runs on push/PR to `master`: builds the LDAP container from
|
|
90
|
+
`docker-compose.yml`, runs `npm ci` + `npm run test` with `INGITHUB=true` on a
|
|
91
|
+
Node 22.x/24.x matrix. Release events trigger `npm-publish.yml` (OIDC publish).
|
|
92
|
+
|
|
93
|
+
## When adding a new option
|
|
94
|
+
|
|
95
|
+
Update all of: the validation in `index.js` (`authenticateResult` /
|
|
96
|
+
`fetchUsers`), the JSDoc in `index.js`, `index.d.ts` (`AuthenticationOptions` and
|
|
97
|
+
/ or `FetchUsersOptions`), and the README (Parameters list, the
|
|
98
|
+
options-by-mode table, and an example if the option changes behavior). Add or
|
|
99
|
+
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,12 +266,28 @@ 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.
|
|
260
281
|
|
|
261
282
|
In version 4, a new function is added: `authenticateResult()`. It has the same call signature as `authenticate()` but returns an object `AuthenticationResult` with more details.
|
|
262
283
|
|
|
284
|
+
`authenticate()` and `fetchUsers()` throw a `LdapAuthenticationError` on failure:
|
|
285
|
+
- `authenticate()`: when the failure corresponds to a known outcome, the
|
|
286
|
+
error's `code` property holds the matching AUTH_RESULT_* constant (the same
|
|
287
|
+
value `authenticateResult()` reports).
|
|
288
|
+
- Missing required options throw a `LdapAuthenticationError` too, with all
|
|
289
|
+
missing fields listed in the message in a single error.
|
|
290
|
+
|
|
263
291
|
`fetchUsers()` returns an array of user objects, one per matched LDAP entry (each with its `dn` and the returned attributes), or an empty array if no user matches the filter.
|
|
264
292
|
|
|
265
293
|
|
|
@@ -279,6 +307,30 @@ AuthenticationResult object has the following fields:
|
|
|
279
307
|
- `message`: authentication message array, which contains server messages
|
|
280
308
|
- `client`: ldapClient instance
|
|
281
309
|
|
|
310
|
+
## Active Directory notes
|
|
311
|
+
|
|
312
|
+
- A typical admin bind DN is a service or admin account, e.g. `cn=Administrator,cn=users,dc=example,dc=com`,
|
|
313
|
+
or a dedicated LDAP sync account.
|
|
314
|
+
- Username attributes: `sAMAccountName` for logins like `jdoe`, `userPrincipalName` for `jdoe@example.com`.
|
|
315
|
+
To look a user up by either at once, use
|
|
316
|
+
`usernameFilter: '(|(sAMAccountName={{username}})(userPrincipalName={{username}}))'`.
|
|
317
|
+
- Set `userSearchBase` to the OU containing the users (e.g. `ou=users,dc=example,dc=com`):
|
|
318
|
+
the search is faster and avoids `AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS`.
|
|
319
|
+
- `fetchUsers()` uses LDAP paged results, so Active Directory's usual 1000-entry limit per search
|
|
320
|
+
is not an issue (adjust the page size with `pageSize` if needed).
|
|
321
|
+
- Binary attributes such as `thumbnailPhoto` should be requested as `thumbnailPhoto;binary`;
|
|
322
|
+
they are returned as base64-encoded strings.
|
|
323
|
+
|
|
324
|
+
## Troubleshooting
|
|
325
|
+
|
|
326
|
+
| Symptom | Likely cause / fix |
|
|
327
|
+
|---|---|
|
|
328
|
+
| `ECONNREFUSED`, `ETIMEDOUT`, or other connect errors | `ldapOpts.url` is wrong or the server is unreachable. Check the URL, the network/firewall, and `connectTimeout`. |
|
|
329
|
+
| `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`. |
|
|
330
|
+
| `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. |
|
|
331
|
+
| `identity ambiguous` (`AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS`) | The search matched multiple entries - narrow `userSearchBase` or make the filter more specific. |
|
|
332
|
+
| `Invalid credentials` (`AUTH_RESULT_FAILURE_CREDENTIAL_INVALID`) | The user was found but the password is wrong. |
|
|
333
|
+
| 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
334
|
|
|
283
335
|
## Old Stuff
|
|
284
336
|
|
|
@@ -30,6 +30,20 @@ gidNumber: 1000
|
|
|
30
30
|
homeDirectory: /home/gauss
|
|
31
31
|
userPassword: {SSHA}u/l8yXCyiWhP3iXpt0UWMyM9ch5sZGFwLXRlc3QtMjAyNg==
|
|
32
32
|
|
|
33
|
+
# Note: the DN line needs the comma in the RDN value escaped (\,), the
|
|
34
|
+
# attribute value line does not - LDIF values are raw strings, but `dn:` is
|
|
35
|
+
# parsed as LDAP DN syntax (RFC 4514)
|
|
36
|
+
dn: cn=Doe\, John,ou=users,dc=example,dc=com
|
|
37
|
+
objectClass: inetOrgPerson
|
|
38
|
+
objectClass: posixAccount
|
|
39
|
+
cn: Doe, John
|
|
40
|
+
sn: Doe
|
|
41
|
+
uid: doe
|
|
42
|
+
uidNumber: 1002
|
|
43
|
+
gidNumber: 1002
|
|
44
|
+
homeDirectory: /home/doe
|
|
45
|
+
userPassword: {SSHA}u/l8yXCyiWhP3iXpt0UWMyM9ch5sZGFwLXRlc3QtMjAyNg==
|
|
46
|
+
|
|
33
47
|
dn: cn=einstein,ou=users,dc=example,dc=com
|
|
34
48
|
objectClass: inetOrgPerson
|
|
35
49
|
objectClass: posixAccount
|
|
@@ -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,18 +81,41 @@ 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
|
+
* (the error's `code` then holds the corresponding AUTH_RESULT_* constant)
|
|
89
|
+
* or when required options are missing.
|
|
90
|
+
*/
|
|
61
91
|
export function authenticate(options: AuthenticationOptions): Promise<any>
|
|
92
|
+
/**
|
|
93
|
+
* Same options as {@link authenticate} but never throws on failure; returns an
|
|
94
|
+
* {@link AuthenticationResult}. Throws {@link LdapAuthenticationError} when
|
|
95
|
+
* required options are missing.
|
|
96
|
+
*/
|
|
62
97
|
export function authenticateResult(options: AuthenticationOptions): Promise<AuthenticationResult>
|
|
63
98
|
|
|
64
99
|
/**
|
|
65
100
|
* Bind with the admin account and search all users under `userSearchBase`.
|
|
66
101
|
* The search always uses paged results, so results are not limited by the
|
|
67
102
|
* common server-side limit of 1000 entries.
|
|
103
|
+
*
|
|
104
|
+
* Returns an empty array if no user matches the filter.
|
|
68
105
|
*/
|
|
69
|
-
export function fetchUsers(options: FetchUsersOptions): Promise<
|
|
106
|
+
export function fetchUsers(options: FetchUsersOptions): Promise<LdapUserEntry[]>
|
|
70
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Thrown by authenticate()/authenticateResult()/fetchUsers() on failure.
|
|
110
|
+
* `message` describes the failure. When the failure corresponds to a known
|
|
111
|
+
* authentication outcome, `code` holds the matching AUTH_RESULT_* constant
|
|
112
|
+
* (the same value authenticateResult() reports in `AuthenticationResult.code`).
|
|
113
|
+
* Missing required options also throw this error, with all missing fields
|
|
114
|
+
* listed in `message`.
|
|
115
|
+
*/
|
|
71
116
|
export class LdapAuthenticationError extends Error {
|
|
72
|
-
constructor(message: any)
|
|
117
|
+
constructor(message: any, code?: number)
|
|
73
118
|
name: string
|
|
119
|
+
code?: number
|
|
74
120
|
}
|
|
75
121
|
}
|