ldap-authentication 4.0.5 → 4.2.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/integration-test.yml +1 -1
- package/README.md +30 -2
- package/index.d.ts +28 -0
- package/index.js +98 -0
- package/index.mjs +1 -0
- package/package.json +2 -2
- package/test/fetch-users.spec.js +136 -0
package/README.md
CHANGED
|
@@ -25,7 +25,11 @@ This library use `ldapts` as the underneath library. It has three modes of authe
|
|
|
25
25
|
then does a search on the user and return the user's details.
|
|
26
26
|
|
|
27
27
|
3. **Verify user exists**. If an `verifyUserExists : true` is provided, the library will login (ldap bind) with the admin user,
|
|
28
|
-
|
|
28
|
+
then search for the user to be verified. If the user exists, user details will be returned (without verifying the user's password).
|
|
29
|
+
|
|
30
|
+
In addition, the `fetchUsers()` function can be used to fetch all users under a search base, using the admin
|
|
31
|
+
account to search (without any username or password of the individual users). The search always uses
|
|
32
|
+
LDAP paged results, so the common server-side limit of 1000 entries per search does not apply.
|
|
29
33
|
|
|
30
34
|
## Features
|
|
31
35
|
|
|
@@ -102,6 +106,22 @@ let authenticated = await authenticate({
|
|
|
102
106
|
})
|
|
103
107
|
```
|
|
104
108
|
|
|
109
|
+
#### Fetch all users (admin search, without user passwords)
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
const { fetchUsers } = require('ldap-authentication')
|
|
113
|
+
|
|
114
|
+
let users = await fetchUsers({
|
|
115
|
+
ldapOpts: { url: 'ldap://ldap.forumsys.com' },
|
|
116
|
+
adminDn: 'cn=read-only-admin,dc=example,dc=com',
|
|
117
|
+
adminPassword: 'password',
|
|
118
|
+
userSearchBase: 'dc=example,dc=com',
|
|
119
|
+
// userFilter: '(objectClass=person)', // default: (|(uid=*)(sAMAccountName=*))
|
|
120
|
+
// attributes: ['uid', 'sn', 'mail'], // omitted = all attributes
|
|
121
|
+
// pageSize: 500, // default: 1000
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
105
125
|
#### Complete example
|
|
106
126
|
|
|
107
127
|
The library works with both CommonJS and ES modules:
|
|
@@ -218,6 +238,12 @@ auth()
|
|
|
218
238
|
- `username`: The username to authenticate with. It is used together with the name in `usernameAttribute`
|
|
219
239
|
to construct an ldap filter as `({attribute}={username})`
|
|
220
240
|
to find the user and get user details in LDAP. Example: `some user input`
|
|
241
|
+
- `userFilter`: (used by `fetchUsers()`) The ldap search filter to select the users to return.
|
|
242
|
+
By default it is `(|(uid=*)(sAMAccountName=*))`, which matches both POSIX (`uid`)
|
|
243
|
+
and Active Directory (`sAMAccountName`) users. Example: `'(objectClass=person)'`,
|
|
244
|
+
or `'(objectClass=*)'` to match everything
|
|
245
|
+
- `pageSize`: (used by `fetchUsers()`) The number of entries to fetch per page for the paged
|
|
246
|
+
search. Default: `1000`
|
|
221
247
|
- `attributes`: A list of attributes of a user details to be returned from the LDAP server.
|
|
222
248
|
If is set to `[]` or ommited, all details will be returned. Example: `['sn', 'cn']`
|
|
223
249
|
- `starttls`: Boolean. Use `STARTTLS` or not. When `true`, the connection will be upgraded to TLS
|
|
@@ -234,6 +260,8 @@ The user object if `authenticate()` is success.
|
|
|
234
260
|
|
|
235
261
|
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.
|
|
236
262
|
|
|
263
|
+
`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
|
+
|
|
237
265
|
|
|
238
266
|
### AuthenticationResult Object
|
|
239
267
|
|
|
@@ -295,4 +323,4 @@ Version 2 supports Node version 12, 14, 15, 16, 17 and 18.
|
|
|
295
323
|
|
|
296
324
|
Version 3 supports Node version 16, 17, 18, 20 and 22.
|
|
297
325
|
|
|
298
|
-
Version 4 supports Node version
|
|
326
|
+
Version 4 supports Node version 22 and above.
|
package/index.d.ts
CHANGED
|
@@ -37,9 +37,37 @@ declare module 'ldap-authentication' {
|
|
|
37
37
|
readonly client: any
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export interface FetchUsersOptions {
|
|
41
|
+
ldapOpts: ClientOptions
|
|
42
|
+
adminDn: string
|
|
43
|
+
adminPassword: string
|
|
44
|
+
userSearchBase: string
|
|
45
|
+
/**
|
|
46
|
+
* LDAP search filter used to select the users to return.
|
|
47
|
+
* Defaults to `(|(uid=*)(sAMAccountName=*))`, which matches both
|
|
48
|
+
* POSIX (`uid`) and Active Directory (`sAMAccountName`) users.
|
|
49
|
+
* Example: `'(objectClass=person)'`, or `'(objectClass=*)'` to match everything.
|
|
50
|
+
*/
|
|
51
|
+
userFilter?: string
|
|
52
|
+
/** A list of attributes of the users to be returned from the LDAP server. If omitted, all details will be returned. */
|
|
53
|
+
attributes?: string[]
|
|
54
|
+
/** A list of attributes to be returned as base64-encoded strings. */
|
|
55
|
+
explicitBufferAttributes?: string[]
|
|
56
|
+
/** Number of entries to fetch per page for the paged search. Defaults to 1000. */
|
|
57
|
+
pageSize?: number
|
|
58
|
+
starttls?: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
40
61
|
export function authenticate(options: AuthenticationOptions): Promise<any>
|
|
41
62
|
export function authenticateResult(options: AuthenticationOptions): Promise<AuthenticationResult>
|
|
42
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Bind with the admin account and search all users under `userSearchBase`.
|
|
66
|
+
* The search always uses paged results, so results are not limited by the
|
|
67
|
+
* common server-side limit of 1000 entries.
|
|
68
|
+
*/
|
|
69
|
+
export function fetchUsers(options: FetchUsersOptions): Promise<any[]>
|
|
70
|
+
|
|
43
71
|
export class LdapAuthenticationError extends Error {
|
|
44
72
|
constructor(message: any)
|
|
45
73
|
name: string
|
package/index.js
CHANGED
|
@@ -45,6 +45,9 @@ const AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS = -2
|
|
|
45
45
|
const AUTH_RESULT_FAILURE_CREDENTIAL_INVALID = -3
|
|
46
46
|
const AUTH_RESULT_FAILURE_UNCATEGORIZED = -4
|
|
47
47
|
|
|
48
|
+
const DEFAULT_FETCH_USERS_FILTER = '(|(uid=*)(sAMAccountName=*))'
|
|
49
|
+
const DEFAULT_FETCH_USERS_PAGE_SIZE = 1000
|
|
50
|
+
|
|
48
51
|
class AuthenticationResult {
|
|
49
52
|
#authCode = AUTH_RESULT_FAILURE_UNCATEGORIZED
|
|
50
53
|
#identity
|
|
@@ -273,6 +276,55 @@ async function _searchUserGroups(
|
|
|
273
276
|
return groups
|
|
274
277
|
}
|
|
275
278
|
|
|
279
|
+
// search all users under the search base and return the list of user objects
|
|
280
|
+
async function _fetchAllUsers(
|
|
281
|
+
ldapClient,
|
|
282
|
+
searchBase,
|
|
283
|
+
userFilter,
|
|
284
|
+
attributes = null,
|
|
285
|
+
explicitBufferAttributes = null,
|
|
286
|
+
pageSize = DEFAULT_FETCH_USERS_PAGE_SIZE
|
|
287
|
+
) {
|
|
288
|
+
let filter = userFilter || DEFAULT_FETCH_USERS_FILTER
|
|
289
|
+
|
|
290
|
+
let searchOptions = {
|
|
291
|
+
filter: filter,
|
|
292
|
+
scope: 'sub',
|
|
293
|
+
// always use paged results, so more than the usual server-side limit
|
|
294
|
+
// (usually 1000 entries per page) can be returned
|
|
295
|
+
paged: { pageSize: pageSize },
|
|
296
|
+
}
|
|
297
|
+
if (attributes) {
|
|
298
|
+
searchOptions.attributes = attributes
|
|
299
|
+
}
|
|
300
|
+
if (explicitBufferAttributes) {
|
|
301
|
+
searchOptions.explicitBufferAttributes = explicitBufferAttributes
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const { searchEntries } = await ldapClient.search(searchBase, searchOptions)
|
|
305
|
+
|
|
306
|
+
let users = searchEntries || []
|
|
307
|
+
// when attribute endwith ;binary, ldapts returns Buffer, we convert them into base64 string
|
|
308
|
+
for (let user of users) {
|
|
309
|
+
if (user != null && attributes != null) {
|
|
310
|
+
for (let attr of attributes) {
|
|
311
|
+
if (attr.endsWith(';binary') && Buffer.isBuffer(user[attr])) {
|
|
312
|
+
user[attr] = user[attr].toString('base64')
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// when attribute is one of the explicitBufferAttributes, should convert to base64 string
|
|
317
|
+
if (user != null && explicitBufferAttributes != null) {
|
|
318
|
+
for (let attr of explicitBufferAttributes) {
|
|
319
|
+
if (Buffer.isBuffer(user[attr])) {
|
|
320
|
+
user[attr] = user[attr].toString('base64')
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return users
|
|
326
|
+
}
|
|
327
|
+
|
|
276
328
|
async function authenticateWithAdmin(
|
|
277
329
|
adminDn,
|
|
278
330
|
adminPassword,
|
|
@@ -561,6 +613,51 @@ async function verifyUserExists(
|
|
|
561
613
|
)
|
|
562
614
|
}
|
|
563
615
|
|
|
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.
|
|
619
|
+
async function fetchUsers(options) {
|
|
620
|
+
assert(
|
|
621
|
+
options.ldapOpts && options.ldapOpts.url,
|
|
622
|
+
'fetchUsers: ldapOpts.url must be provided'
|
|
623
|
+
)
|
|
624
|
+
assert(options.adminDn, 'fetchUsers: adminDn must be provided')
|
|
625
|
+
assert(options.adminPassword, 'fetchUsers: adminPassword must be provided')
|
|
626
|
+
assert(options.userSearchBase, 'fetchUsers: userSearchBase must be provided')
|
|
627
|
+
|
|
628
|
+
let ldapAdminClient
|
|
629
|
+
try {
|
|
630
|
+
ldapAdminClient = await _ldapBind(
|
|
631
|
+
options.adminDn,
|
|
632
|
+
options.adminPassword,
|
|
633
|
+
options.starttls,
|
|
634
|
+
options.ldapOpts
|
|
635
|
+
)
|
|
636
|
+
} catch (error) {
|
|
637
|
+
if (ldapAdminClient && ldapAdminClient.isConnected) {
|
|
638
|
+
await ldapAdminClient.unbind()
|
|
639
|
+
}
|
|
640
|
+
throw new LdapAuthenticationError(error.message || 'admin bind failed')
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
try {
|
|
644
|
+
return await _fetchAllUsers(
|
|
645
|
+
ldapAdminClient,
|
|
646
|
+
options.userSearchBase,
|
|
647
|
+
options.userFilter,
|
|
648
|
+
options.attributes,
|
|
649
|
+
options.explicitBufferAttributes,
|
|
650
|
+
options.pageSize || DEFAULT_FETCH_USERS_PAGE_SIZE
|
|
651
|
+
)
|
|
652
|
+
} catch (error) {
|
|
653
|
+
throw new LdapAuthenticationError(error.message || 'user search failed')
|
|
654
|
+
} finally {
|
|
655
|
+
if (ldapAdminClient && ldapAdminClient.isConnected) {
|
|
656
|
+
await ldapAdminClient.unbind()
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
564
661
|
async function authenticate(options) {
|
|
565
662
|
const result = await authenticateResult(options)
|
|
566
663
|
|
|
@@ -686,6 +783,7 @@ module.exports.AuthenticationResult = AuthenticationResult
|
|
|
686
783
|
|
|
687
784
|
module.exports.authenticate = authenticate
|
|
688
785
|
module.exports.authenticateResult = authenticateResult
|
|
786
|
+
module.exports.fetchUsers = fetchUsers
|
|
689
787
|
module.exports.LdapAuthenticationError = LdapAuthenticationError
|
|
690
788
|
|
|
691
789
|
module.exports.exportForTesting = {
|
package/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ldap-authentication",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "A simple async nodejs library for LDAP user authentication",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"./package.json": "./package.json"
|
|
15
15
|
},
|
|
16
16
|
"engines": {
|
|
17
|
-
"node": ">=
|
|
17
|
+
"node": ">=22.0.0"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"test": "node test/jasmine.js"
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const { fetchUsers, LdapAuthenticationError } = require('../index.js')
|
|
2
|
+
|
|
3
|
+
const url = process.env.INGITHUB ? 'ldap://localhost:1389' : 'ldap://ldap:1389'
|
|
4
|
+
|
|
5
|
+
describe('ldap-authentication fetchUsers test', () => {
|
|
6
|
+
const baseOptions = {
|
|
7
|
+
ldapOpts: {
|
|
8
|
+
url: url,
|
|
9
|
+
},
|
|
10
|
+
adminDn: 'cn=read-only-admin,dc=example,dc=com',
|
|
11
|
+
adminPassword: 'password',
|
|
12
|
+
userSearchBase: 'dc=example,dc=com',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
it('Should return all users with the default filter', async () => {
|
|
16
|
+
let users = await fetchUsers(baseOptions)
|
|
17
|
+
|
|
18
|
+
expect(Array.isArray(users)).toBe(true)
|
|
19
|
+
expect(users.length).toBe(2)
|
|
20
|
+
let uids = users.map((user) => user.uid)
|
|
21
|
+
expect(uids).toContain('gauss')
|
|
22
|
+
expect(uids).toContain('einstein')
|
|
23
|
+
for (let user of users) {
|
|
24
|
+
expect(user.dn).toBeTruthy()
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('Should return only the requested attributes', async () => {
|
|
29
|
+
let users = await fetchUsers({
|
|
30
|
+
...baseOptions,
|
|
31
|
+
attributes: ['uid', 'sn'],
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
expect(users.length).toBe(2)
|
|
35
|
+
let gauss = users.find((user) => user.uid === 'gauss')
|
|
36
|
+
expect(gauss).toBeTruthy()
|
|
37
|
+
expect(gauss.sn).toEqual('Bar1')
|
|
38
|
+
expect(gauss.cn).toBeUndefined()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('Should return only matching users with a custom userFilter', async () => {
|
|
42
|
+
let users = await fetchUsers({
|
|
43
|
+
...baseOptions,
|
|
44
|
+
userFilter: '(uid=gauss)',
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
expect(users.length).toBe(1)
|
|
48
|
+
expect(users[0].uid).toEqual('gauss')
|
|
49
|
+
expect(users[0].sn).toEqual('Bar1')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('Should return all entries including non-users when userFilter matches everything', async () => {
|
|
53
|
+
let users = await fetchUsers({
|
|
54
|
+
...baseOptions,
|
|
55
|
+
userFilter: '(objectClass=*)',
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
expect(users.length).toBeGreaterThan(2)
|
|
59
|
+
let dns = users.map((user) => user.dn)
|
|
60
|
+
expect(dns).toContain('cn=gauss,ou=users,dc=example,dc=com')
|
|
61
|
+
expect(dns).toContain('cn=科学A部,ou=groups,dc=example,dc=com')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('Should return empty list when userFilter matches nothing', async () => {
|
|
65
|
+
let users = await fetchUsers({
|
|
66
|
+
...baseOptions,
|
|
67
|
+
userFilter: '(uid=does-not-exist)',
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
expect(users).toEqual([])
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('ldap-authentication fetchUsers negative test', () => {
|
|
75
|
+
const baseOptions = {
|
|
76
|
+
ldapOpts: {
|
|
77
|
+
url: url,
|
|
78
|
+
},
|
|
79
|
+
adminDn: 'cn=read-only-admin,dc=example,dc=com',
|
|
80
|
+
adminPassword: 'password',
|
|
81
|
+
userSearchBase: 'dc=example,dc=com',
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
it('wrong admin password should throw LdapAuthenticationError', async () => {
|
|
85
|
+
let options = {
|
|
86
|
+
...baseOptions,
|
|
87
|
+
adminPassword: 'wrongpassword',
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let e = null
|
|
91
|
+
try {
|
|
92
|
+
await fetchUsers(options)
|
|
93
|
+
} catch (error) {
|
|
94
|
+
e = error
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
expect(e).toBeTruthy()
|
|
98
|
+
expect(e).toBeInstanceOf(LdapAuthenticationError)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('wrong admin dn should throw LdapAuthenticationError', async () => {
|
|
102
|
+
let options = {
|
|
103
|
+
...baseOptions,
|
|
104
|
+
adminDn: 'cn=not-exist,dc=example,dc=com',
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let e = null
|
|
108
|
+
try {
|
|
109
|
+
await fetchUsers(options)
|
|
110
|
+
} catch (error) {
|
|
111
|
+
e = error
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
expect(e).toBeTruthy()
|
|
115
|
+
expect(e).toBeInstanceOf(LdapAuthenticationError)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('missing userSearchBase should throw', async () => {
|
|
119
|
+
let options = {
|
|
120
|
+
ldapOpts: {
|
|
121
|
+
url: url,
|
|
122
|
+
},
|
|
123
|
+
adminDn: 'cn=read-only-admin,dc=example,dc=com',
|
|
124
|
+
adminPassword: 'password',
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let e = null
|
|
128
|
+
try {
|
|
129
|
+
await fetchUsers(options)
|
|
130
|
+
} catch (error) {
|
|
131
|
+
e = error
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
expect(e).toBeTruthy()
|
|
135
|
+
})
|
|
136
|
+
})
|