ldap-authentication 4.0.5 → 4.1.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/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
- then search for the user to be verified. If the user exists, user details will be returned (without verifying the user's password).
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
 
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
@@ -11,6 +11,7 @@ export {
11
11
  LdapAuthenticationError,
12
12
  authenticate,
13
13
  authenticateResult,
14
+ fetchUsers,
14
15
  } from './index.js'
15
16
 
16
17
  export default ldapAuthentication
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldap-authentication",
3
- "version": "4.0.5",
3
+ "version": "4.1.0",
4
4
  "description": "A simple async nodejs library for LDAP user authentication",
5
5
  "main": "index.js",
6
6
  "types": "./index.d.ts",
@@ -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
+ })