ldap-authentication 3.3.4 → 4.0.3

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.
@@ -8,7 +8,7 @@ services:
8
8
  command: sleep infinity
9
9
 
10
10
  ldap:
11
- image: bitnami/openldap
11
+ image: symas/openldap:2.6
12
12
  ports:
13
13
  - '1389:1389'
14
14
  - '1636:1636'
@@ -16,7 +16,7 @@ jobs:
16
16
  runs-on: ubuntu-latest
17
17
  strategy:
18
18
  matrix:
19
- node-version: [16.x, 17.x, 18.x, 20.x, 22.x]
19
+ node-version: [20.x, 22.x, 24.x]
20
20
 
21
21
  name: 'Integration Node v${{ matrix.node-version }}'
22
22
 
@@ -46,7 +46,7 @@ jobs:
46
46
  uses: actions/setup-node@v3
47
47
  with:
48
48
  node-version: ${{ matrix.node-version }}
49
- - run: npm install
49
+ - run: npm ci
50
50
  - run: npm run test
51
51
 
52
52
  - name: Stop containers
@@ -1,33 +1,22 @@
1
- # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
- # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3
-
4
- name: Publish to NPM
1
+ name: Publish Package
5
2
 
6
3
  on:
7
4
  release:
8
5
  types: [created]
9
6
 
7
+ permissions:
8
+ id-token: write # Required for OIDC
9
+ contents: read
10
+
10
11
  jobs:
11
- build:
12
+ publish:
12
13
  runs-on: ubuntu-latest
13
14
  steps:
14
15
  - uses: actions/checkout@v4
15
- - uses: actions/setup-node@v4
16
- with:
17
- node-version: 20
18
- - run: npm ci
19
16
 
20
-
21
- publish-npm:
22
- needs: build
23
- runs-on: ubuntu-latest
24
- steps:
25
- - uses: actions/checkout@v4
26
17
  - uses: actions/setup-node@v4
27
18
  with:
28
- node-version: 20
29
- registry-url: https://registry.npmjs.org/
19
+ node-version: '24'
20
+ registry-url: 'https://registry.npmjs.org'
30
21
  - run: npm ci
31
22
  - run: npm publish
32
- env:
33
- NODE_AUTH_TOKEN: ${{secrets.npm_token}}
package/README.md CHANGED
@@ -147,6 +147,43 @@ async function auth() {
147
147
  auth()
148
148
  ```
149
149
 
150
+ ### Example with StartTLS
151
+
152
+ ```javascript
153
+ import { authenticate } from 'ldap-authentication'
154
+
155
+ async function auth() {
156
+ // auth with admin
157
+ let options = {
158
+ ldapOpts: {
159
+ url: 'ldap://ldap.example.com',
160
+ tlsOptions: {
161
+ rejectUnauthorized: false, // For self-signed certificates
162
+ minVersion: 'TLSv1.2',
163
+ servername: 'ldap.example.com' // For SNI (Server Name Indication)
164
+ }
165
+ },
166
+ starttls: true, // Enable StartTLS
167
+ adminDn: 'cn=admin,dc=example,dc=com',
168
+ adminPassword: 'password',
169
+ userPassword: 'password',
170
+ userSearchBase: 'dc=example,dc=com',
171
+ usernameAttribute: 'uid',
172
+ username: 'testuser'
173
+ }
174
+
175
+ let user = await authenticate(options)
176
+ console.log(user)
177
+ }
178
+
179
+ auth()
180
+ ```
181
+
182
+ **Important Notes for StartTLS:**
183
+ - Use `ldap://` URLs with `starttls: true` (not `ldaps://`)
184
+ - For `ldaps://` URLs, omit `starttls` and the connection will use TLS from the start
185
+ - TLS options like `rejectUnauthorized`, `minVersion`, and `servername` can be specified in `ldapOpts.tlsOptions`
186
+
150
187
  ## Parameters
151
188
 
152
189
  - `ldapOpts`: This is passed to `ldapts` client directly
@@ -172,7 +209,9 @@ auth()
172
209
  to find the user and get user details in LDAP. Example: `some user input`
173
210
  - `attributes`: A list of attributes of a user details to be returned from the LDAP server.
174
211
  If is set to `[]` or ommited, all details will be returned. Example: `['sn', 'cn']`
175
- - `starttls`: Boolean. Use `STARTTLS` or not
212
+ - `starttls`: Boolean. Use `STARTTLS` or not. When `true`, the connection will be upgraded to TLS
213
+ using the STARTTLS extended operation. TLS options can be specified in `ldapOpts.tlsOptions`.
214
+ Note: Use `starttls: true` with `ldap://` URLs, not `ldaps://` URLs
176
215
  - `groupsSearchBase`: if specified with groupClass, will serve as search base for authenticated user groups
177
216
  - `groupClass`: if specified with groupsSearchBase, will be used as objectClass in search filter for authenticated user groups
178
217
  - `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
@@ -182,6 +221,28 @@ auth()
182
221
 
183
222
  The user object if `authenticate()` is success.
184
223
 
224
+ 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.
225
+
226
+
227
+ ### AuthenticationResult Object
228
+
229
+ AuthenticationResult object has the following fields:
230
+
231
+ - `code`: number. constants:
232
+ - `AUTH_RESULT_FAILURE` = 0
233
+ - `AUTH_RESULT_SUCCESS` = 1
234
+ - `AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND` = -1
235
+ - `AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS` = -2
236
+ - `AUTH_RESULT_FAILURE_CREDENTIAL_INVALID` = -3
237
+ - `AUTH_RESULT_FAILURE_UNCATEGORIZED` = -4
238
+ - `identity`: identity supplied as string
239
+ - `user`: user object if authentication is successful, otherwise null
240
+ - `message`: authentication message array, which contains server messages
241
+ - `client`: ldapClient instance
242
+
243
+
244
+ ## Old Stuff
245
+
185
246
  In version 2, The user object has a `raw` field that has the raw data from the LDAP/AD server. It can be used to access buffer objects (profile pics for example).
186
247
 
187
248
  Buffer data can now be accessed by `user.raw.profilePhoto`, etc, instead of `user.profilePhoto`.
@@ -221,4 +282,6 @@ export async function verifyLogin(email: string, password: string) {
221
282
 
222
283
  Version 2 supports Node version 12, 14, 15, 16, 17 and 18.
223
284
 
224
- Version 3 supports Node version 16, 17, 18, 20 and 22,
285
+ Version 3 supports Node version 16, 17, 18, 20 and 22.
286
+
287
+ Version 4 supports Node version 20 and above.
@@ -1,6 +1,6 @@
1
1
  services:
2
2
  ldap:
3
- image: bitnami/openldap:2.6.3
3
+ image: symas/openldap:2.6
4
4
  ports:
5
5
  - '1389:1389'
6
6
  - '1636:1636'
package/index.d.ts CHANGED
@@ -17,6 +17,7 @@ declare module 'ldap-authentication' {
17
17
  groupMemberUserAttribute?: string
18
18
  userPassword?: string
19
19
  attributes?: string[]
20
+ explicitBufferAttributes?: string[]
20
21
  }
21
22
 
22
23
  export function authenticate(options: AuthenticationOptions): Promise<any>
package/index.js CHANGED
@@ -38,12 +38,82 @@ function _ldapEscapeDN(s) {
38
38
  return ret
39
39
  }
40
40
 
41
+ const AUTH_RESULT_FAILURE = 0
42
+ const AUTH_RESULT_SUCCESS = 1
43
+ const AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND = -1
44
+ const AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS = -2
45
+ const AUTH_RESULT_FAILURE_CREDENTIAL_INVALID = -3
46
+ const AUTH_RESULT_FAILURE_UNCATEGORIZED = -4
47
+
48
+ class AuthenticationResult {
49
+ #authCode = AUTH_RESULT_FAILURE_UNCATEGORIZED
50
+ #identity
51
+ #user
52
+ #messages = []
53
+ #client
54
+
55
+ constructor(authCode, identity, user, messages, client) {
56
+ this.#authCode = authCode // one of the above constants
57
+ this.#identity = identity // identity supplied as string
58
+ this.#user = user // user object found on ldap server OR null
59
+ this.#messages = messages // authentication messages array, which contains server messages
60
+ this.#client = client // ldapClient instance
61
+ }
62
+
63
+ get code() {
64
+ return this.#authCode
65
+ }
66
+
67
+ get identity() {
68
+ return this.#identity
69
+ }
70
+
71
+ get messages() {
72
+ return this.#messages
73
+ }
74
+
75
+ get client() {
76
+ return this.#client
77
+ }
78
+
79
+ get user() {
80
+ return this.#user
81
+ }
82
+ }
83
+
84
+ const authenticationMessages = {
85
+ AUTH_RESULT_FAILURE: 'Authentication failed',
86
+ AUTH_RESULT_SUCCESS: 'Authentication successful',
87
+ AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND: 'Authentication identity not found',
88
+ AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS: 'Authentication identity ambiguous',
89
+ AUTH_RESULT_FAILURE_CREDENTIAL_INVALID: 'Invalid credentials',
90
+ AUTH_RESULT_FAILURE_UNCATEGORIZED: 'Uncategorized authentication failure',
91
+ }
92
+
41
93
  // bind and return the ldap client
42
94
  async function _ldapBind(dn, password, starttls, ldapOpts) {
43
95
  // TODO: check if ldapts expects escaped dn or not (possible double escaping problems?)
44
96
  dn = _ldapEscapeDN(dn)
45
97
  ldapOpts.connectTimeout = ldapOpts.connectTimeout || 5000
46
- let client = new ldapts.Client(ldapOpts)
98
+
99
+ // When using StartTLS, we need to exclude tlsOptions from the Client constructor
100
+ // and only pass them to the startTLS() method to avoid connection conflicts.
101
+ // According to ldapts documentation:
102
+ // - For LDAPS (ldaps://): pass tlsOptions to Client constructor
103
+ // - For StartTLS (ldap://): do NOT pass tlsOptions to Client constructor, only to startTLS()
104
+ // - For plain LDAP (ldap://): do NOT pass tlsOptions to Client constructor
105
+ let clientOpts = ldapOpts
106
+ const isLdaps = ldapOpts.url && ldapOpts.url.startsWith('ldaps://')
107
+
108
+ // Only pass tlsOptions to Client constructor if using ldaps:// protocol
109
+ // For ldap:// protocol (plain or StartTLS), exclude tlsOptions from constructor
110
+ if (!isLdaps && ldapOpts.tlsOptions) {
111
+ // Create a shallow copy of ldapOpts without tlsOptions for the Client constructor
112
+ const { tlsOptions, ...optsWithoutTls } = ldapOpts
113
+ clientOpts = optsWithoutTls
114
+ }
115
+
116
+ let client = new ldapts.Client(clientOpts)
47
117
 
48
118
  if (starttls) {
49
119
  await client.startTLS(ldapOpts.tlsOptions)
@@ -60,7 +130,8 @@ async function _searchUser(
60
130
  searchBase,
61
131
  usernameAttribute,
62
132
  username,
63
- attributes = null
133
+ attributes = null,
134
+ explicitBufferAttributes = null
64
135
  ) {
65
136
  let filter = new ldapts.EqualityFilter({
66
137
  attribute: usernameAttribute,
@@ -74,6 +145,9 @@ async function _searchUser(
74
145
  if (attributes) {
75
146
  searchOptions.attributes = attributes
76
147
  }
148
+ if(explicitBufferAttributes) {
149
+ searchOptions.explicitBufferAttributes = explicitBufferAttributes
150
+ }
77
151
 
78
152
  // TODO: we don't support reference yet
79
153
  // If the server was able to locate the entry referred to by the baseObject
@@ -93,8 +167,24 @@ async function _searchUser(
93
167
  !searchEntries[0] ||
94
168
  !searchEntries[0].dn
95
169
  ) {
96
- user = null
170
+ return new AuthenticationResult(
171
+ AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND,
172
+ username,
173
+ null,
174
+ [authenticationMessages.AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND],
175
+ ldapClient
176
+ )
97
177
  } else {
178
+ if (searchEntries.length > 1) {
179
+ return new AuthenticationResult(
180
+ AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS,
181
+ username,
182
+ null,
183
+ [authenticationMessages.AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS],
184
+ ldapClient
185
+ )
186
+ }
187
+
98
188
  user = searchEntries[0]
99
189
  }
100
190
 
@@ -106,7 +196,22 @@ async function _searchUser(
106
196
  }
107
197
  }
108
198
  }
109
- return user
199
+ // when attribute is one of the explicitBufferAttributes, should convert to base64 string
200
+ if (user != null && explicitBufferAttributes != null) {
201
+ for (let attr of explicitBufferAttributes) {
202
+ if (Buffer.isBuffer(user[attr])) {
203
+ user[attr] = user[attr].toString('base64')
204
+ }
205
+ }
206
+ }
207
+
208
+ return new AuthenticationResult(
209
+ AUTH_RESULT_SUCCESS,
210
+ username,
211
+ user,
212
+ [authenticationMessages.AUTH_RESULT_SUCCESS],
213
+ ldapClient
214
+ )
110
215
  }
111
216
 
112
217
  // search a groups which user is member
@@ -170,7 +275,8 @@ async function authenticateWithAdmin(
170
275
  groupClass,
171
276
  groupMemberAttribute = 'member',
172
277
  groupMemberUserAttribute = 'dn',
173
- attributes = null
278
+ attributes = null,
279
+ explicitBufferAttributes = null
174
280
  ) {
175
281
  let ldapAdminClient
176
282
  try {
@@ -184,23 +290,39 @@ async function authenticateWithAdmin(
184
290
  if (ldapAdminClient && ldapAdminClient.isConnected) {
185
291
  await ldapAdminClient.unbind()
186
292
  }
187
- throw new LdapAuthenticationError(error)
293
+
294
+ return new AuthenticationResult(
295
+ AUTH_RESULT_FAILURE,
296
+ username,
297
+ null,
298
+ [authenticationMessages.AUTH_RESULT_FAILURE, error.message || 'admin bind failed'],
299
+ ldapAdminClient
300
+ )
188
301
  }
189
- let user = await _searchUser(
302
+
303
+ let searchResult = await _searchUser(
190
304
  ldapAdminClient,
191
305
  userSearchBase,
192
306
  usernameAttribute,
193
307
  username,
194
- attributes
308
+ attributes,
309
+ explicitBufferAttributes
195
310
  )
311
+
312
+ let user = searchResult.user
313
+
196
314
  if (!user || !user.dn) {
197
315
  ldapOpts.log &&
198
316
  ldapOpts.log.trace(
199
317
  `admin did not find user! (${usernameAttribute}=${username})`
200
318
  )
201
319
  await ldapAdminClient.unbind()
202
- throw new LdapAuthenticationError(
203
- 'user not found or usernameAttribute is wrong'
320
+ return new AuthenticationResult(
321
+ AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND,
322
+ username,
323
+ null,
324
+ [authenticationMessages.AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND],
325
+ ldapAdminClient
204
326
  )
205
327
  }
206
328
  let userDn = user.dn
@@ -211,7 +333,14 @@ async function authenticateWithAdmin(
211
333
  if (ldapUserClient && ldapUserClient.isConnected) {
212
334
  await ldapUserClient.unbind()
213
335
  }
214
- throw new LdapAuthenticationError(error)
336
+
337
+ return new AuthenticationResult(
338
+ AUTH_RESULT_FAILURE_CREDENTIAL_INVALID,
339
+ username,
340
+ null,
341
+ [authenticationMessages.AUTH_RESULT_FAILURE_CREDENTIAL_INVALID, error.message || 'invalid credentials'],
342
+ ldapAdminClient
343
+ )
215
344
  }
216
345
  if (groupsSearchBase && groupClass && groupMemberAttribute) {
217
346
  let groups = await _searchUserGroups(
@@ -226,7 +355,14 @@ async function authenticateWithAdmin(
226
355
  }
227
356
  await ldapAdminClient.unbind()
228
357
  await ldapUserClient.unbind()
229
- return user
358
+
359
+ return new AuthenticationResult(
360
+ AUTH_RESULT_SUCCESS,
361
+ username,
362
+ user,
363
+ [authenticationMessages.AUTH_RESULT_SUCCESS],
364
+ ldapAdminClient
365
+ )
230
366
  }
231
367
 
232
368
  async function authenticateWithUser(
@@ -241,7 +377,8 @@ async function authenticateWithUser(
241
377
  groupClass,
242
378
  groupMemberAttribute = 'member',
243
379
  groupMemberUserAttribute = 'dn',
244
- attributes = null
380
+ attributes = null,
381
+ explicitBufferAttributes = null
245
382
  ) {
246
383
  let ldapUserClient
247
384
  try {
@@ -250,28 +387,54 @@ async function authenticateWithUser(
250
387
  if (ldapUserClient && ldapUserClient.isConnected) {
251
388
  await ldapUserClient.unbind()
252
389
  }
253
- throw new LdapAuthenticationError(error)
390
+
391
+ return new AuthenticationResult(
392
+ AUTH_RESULT_FAILURE,
393
+ username,
394
+ null,
395
+ [authenticationMessages.AUTH_RESULT_FAILURE, error.message || 'user bind failed'],
396
+ ldapUserClient
397
+ )
254
398
  }
255
399
  if (!usernameAttribute || !userSearchBase) {
256
400
  // if usernameAttribute is not provided, no user detail is needed.
257
401
  await ldapUserClient.unbind()
258
- return true
402
+ return new AuthenticationResult(
403
+ AUTH_RESULT_SUCCESS,
404
+ username,
405
+ {},
406
+ [authenticationMessages.AUTH_RESULT_SUCCESS],
407
+ ldapUserClient
408
+ )
259
409
  }
260
- let user = await _searchUser(
410
+
411
+ let searchResult = await _searchUser(
261
412
  ldapUserClient,
262
413
  userSearchBase,
263
414
  usernameAttribute,
264
415
  username,
265
- attributes
416
+ attributes,
417
+ explicitBufferAttributes
266
418
  )
419
+
420
+ let user = searchResult.user
421
+
267
422
  if (!user || !user.dn) {
268
423
  ldapOpts.log &&
269
424
  ldapOpts.log.trace(
270
425
  `user logged in, but user details could not be found. (${usernameAttribute}=${username}). Probabaly wrong attribute or searchBase?`
271
426
  )
272
427
  await ldapUserClient.unbind()
273
- throw new LdapAuthenticationError(
274
- 'user logged in, but user details could not be found. Probabaly usernameAttribute or userSearchBase is wrong?'
428
+
429
+ return new AuthenticationResult(
430
+ AUTH_RESULT_FAILURE,
431
+ username,
432
+ null,
433
+ [
434
+ authenticationMessages.AUTH_RESULT_FAILURE,
435
+ 'user logged in, but user details could not be found. Probabaly usernameAttribute or userSearchBase is wrong?',
436
+ ],
437
+ ldapUserClient
275
438
  )
276
439
  }
277
440
  if (groupsSearchBase && groupClass && groupMemberAttribute) {
@@ -286,7 +449,14 @@ async function authenticateWithUser(
286
449
  user.groups = groups
287
450
  }
288
451
  await ldapUserClient.unbind()
289
- return user
452
+
453
+ return new AuthenticationResult(
454
+ AUTH_RESULT_SUCCESS,
455
+ username,
456
+ user,
457
+ [authenticationMessages.AUTH_RESULT_SUCCESS],
458
+ ldapUserClient
459
+ )
290
460
  }
291
461
 
292
462
  async function verifyUserExists(
@@ -301,7 +471,8 @@ async function verifyUserExists(
301
471
  groupClass,
302
472
  groupMemberAttribute = 'member',
303
473
  groupMemberUserAttribute = 'dn',
304
- attributes = null
474
+ attributes = null,
475
+ explicitBufferAttributes = null
305
476
  ) {
306
477
  let ldapAdminClient
307
478
  try {
@@ -315,23 +486,41 @@ async function verifyUserExists(
315
486
  if (ldapAdminClient && ldapAdminClient.isConnected) {
316
487
  await ldapAdminClient.unbind()
317
488
  }
318
- throw new LdapAuthenticationError(error)
489
+ return new AuthenticationResult(
490
+ AUTH_RESULT_FAILURE,
491
+ username,
492
+ null,
493
+ [authenticationMessages.AUTH_RESULT_FAILURE, error.message || 'admin bind failed'],
494
+ ldapAdminClient
495
+ )
319
496
  }
320
- let user = await _searchUser(
497
+
498
+ let searchResult = await _searchUser(
321
499
  ldapAdminClient,
322
500
  userSearchBase,
323
501
  usernameAttribute,
324
502
  username,
325
- attributes
503
+ attributes,
504
+ explicitBufferAttributes
326
505
  )
506
+
507
+ let user = searchResult.user
508
+
327
509
  if (!user || !user.dn) {
328
510
  ldapOpts.log &&
329
511
  ldapOpts.log.trace(
330
512
  `admin did not find user! (${usernameAttribute}=${username})`
331
513
  )
332
514
  await ldapAdminClient.unbind()
333
- throw new LdapAuthenticationError(
334
- 'user not found or usernameAttribute is wrong'
515
+ return new AuthenticationResult(
516
+ AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND,
517
+ username,
518
+ null,
519
+ [
520
+ authenticationMessages.AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND,
521
+ 'user not found or usernameAttribute is wrong'
522
+ ],
523
+ ldapAdminClient
335
524
  )
336
525
  }
337
526
  if (groupsSearchBase && groupClass && groupMemberAttribute) {
@@ -346,10 +535,28 @@ async function verifyUserExists(
346
535
  user.groups = groups
347
536
  }
348
537
  await ldapAdminClient.unbind()
349
- return user
538
+ return new AuthenticationResult(
539
+ AUTH_RESULT_SUCCESS,
540
+ username,
541
+ user,
542
+ [authenticationMessages.AUTH_RESULT_SUCCESS],
543
+ ldapAdminClient
544
+ )
350
545
  }
351
546
 
352
547
  async function authenticate(options) {
548
+ const result = await authenticateResult(options)
549
+
550
+ if (result.code !== AUTH_RESULT_SUCCESS) {
551
+ throw new LdapAuthenticationError(
552
+ result.messages[result.messages.length - 1]
553
+ )
554
+ }
555
+
556
+ return result.user
557
+ }
558
+
559
+ async function authenticateResult(options) {
353
560
  if (!options.userDn) {
354
561
  assert(options.adminDn, 'Admin mode adminDn must be provided')
355
562
  assert(options.adminPassword, 'Admin mode adminPassword must be provided')
@@ -366,6 +573,7 @@ async function authenticate(options) {
366
573
  options.ldapOpts && options.ldapOpts.url,
367
574
  'ldapOpts.url must be provided'
368
575
  )
576
+
369
577
  if (options.verifyUserExists) {
370
578
  assert(options.adminDn, 'Admin mode adminDn must be provided')
371
579
  assert(
@@ -384,9 +592,11 @@ async function authenticate(options) {
384
592
  options.groupClass,
385
593
  options.groupMemberAttribute,
386
594
  options.groupMemberUserAttribute,
387
- options.attributes
595
+ options.attributes,
596
+ options.explicitBufferAttributes
388
597
  )
389
598
  }
599
+
390
600
  assert(options.userPassword, 'userPassword must be provided')
391
601
  if (options.adminDn) {
392
602
  assert(
@@ -406,9 +616,11 @@ async function authenticate(options) {
406
616
  options.groupClass,
407
617
  options.groupMemberAttribute,
408
618
  options.groupMemberUserAttribute,
409
- options.attributes
619
+ options.attributes,
620
+ options.explicitBufferAttributes
410
621
  )
411
622
  }
623
+
412
624
  assert(options.userDn, 'adminDn/adminPassword OR userDn must be provided')
413
625
  return await authenticateWithUser(
414
626
  options.userDn,
@@ -422,7 +634,8 @@ async function authenticate(options) {
422
634
  options.groupClass,
423
635
  options.groupMemberAttribute,
424
636
  options.groupMemberUserAttribute,
425
- options.attributes
637
+ options.attributes,
638
+ options.explicitBufferAttributes
426
639
  )
427
640
  }
428
641
 
@@ -438,7 +651,21 @@ class LdapAuthenticationError extends Error {
438
651
  }
439
652
  }
440
653
 
654
+ module.exports.AUTH_RESULT_FAILURE = AUTH_RESULT_FAILURE
655
+ module.exports.AUTH_RESULT_SUCCESS = AUTH_RESULT_SUCCESS
656
+ module.exports.AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND =
657
+ AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND
658
+ module.exports.AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS =
659
+ AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS
660
+ module.exports.AUTH_RESULT_FAILURE_CREDENTIAL_INVALID =
661
+ AUTH_RESULT_FAILURE_CREDENTIAL_INVALID
662
+ module.exports.AUTH_RESULT_FAILURE_UNCATEGORIZED =
663
+ AUTH_RESULT_FAILURE_UNCATEGORIZED
664
+
665
+ module.exports.AuthenticationResult = AuthenticationResult
666
+
441
667
  module.exports.authenticate = authenticate
668
+ module.exports.authenticateResult = authenticateResult
442
669
  module.exports.LdapAuthenticationError = LdapAuthenticationError
443
670
 
444
671
  module.exports.exportForTesting = {
package/package.json CHANGED
@@ -1,15 +1,18 @@
1
1
  {
2
2
  "name": "ldap-authentication",
3
- "version": "3.3.4",
3
+ "version": "4.0.3",
4
4
  "description": "A simple async nodejs library for LDAP user authentication",
5
5
  "main": "index.js",
6
6
  "types": "./index.d.ts",
7
+ "engines": {
8
+ "node": ">=20.0.0"
9
+ },
7
10
  "scripts": {
8
11
  "test": "node test/jasmine.js"
9
12
  },
10
13
  "repository": {
11
14
  "type": "git",
12
- "url": "https://github.com/shaozi/ldap-authentication.git"
15
+ "url": "git+https://github.com/shaozi/ldap-authentication.git"
13
16
  },
14
17
  "keywords": [
15
18
  "ldap",
@@ -36,9 +39,9 @@
36
39
  },
37
40
  "homepage": "https://github.com/shaozi/ldap-authentication#readme",
38
41
  "dependencies": {
39
- "ldapts": "^7.3.1"
42
+ "ldapts": "^8.1.3"
40
43
  },
41
44
  "devDependencies": {
42
- "jasmine": "^5.6.0"
45
+ "jasmine": "^6.0.0"
43
46
  }
44
47
  }
@@ -0,0 +1,425 @@
1
+ const {
2
+ authenticateResult,
3
+ AuthenticationResult,
4
+ AUTH_RESULT_SUCCESS,
5
+ AUTH_RESULT_FAILURE,
6
+ AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND,
7
+ AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS,
8
+ AUTH_RESULT_FAILURE_CREDENTIAL_INVALID,
9
+ AUTH_RESULT_FAILURE_UNCATEGORIZED
10
+ } = require('../index.js')
11
+
12
+ const url = process.env.INGITHUB ? 'ldap://localhost:1389' : 'ldap://ldap:1389'
13
+
14
+ describe('ldap-authentication test return AuthenticationResult', () => {
15
+ it('Use an admin user to check if user exists', async () => {
16
+ let options = {
17
+ ldapOpts: {
18
+ url: url,
19
+ },
20
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
21
+ adminPassword: 'password',
22
+ verifyUserExists: true,
23
+ userSearchBase: 'dc=example,dc=com',
24
+ usernameAttribute: 'uid',
25
+ username: 'gauss',
26
+ }
27
+
28
+ let result = await authenticateResult(options)
29
+ expect(result).toBeInstanceOf(AuthenticationResult)
30
+ expect(result.code).toEqual(AUTH_RESULT_SUCCESS)
31
+ expect(result.user).toBeTruthy()
32
+ expect(result.user.uid).toEqual('gauss')
33
+ })
34
+ it('Use an admin user to check if user exists and return attributes', async () => {
35
+ let options = {
36
+ ldapOpts: {
37
+ url: url,
38
+ },
39
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
40
+ adminPassword: 'password',
41
+ verifyUserExists: true,
42
+ userSearchBase: 'dc=example,dc=com',
43
+ usernameAttribute: 'uid',
44
+ username: 'gauss',
45
+ attributes: ['uid', 'sn'],
46
+ }
47
+
48
+ let result = await authenticateResult(options)
49
+ expect(result).toBeInstanceOf(AuthenticationResult)
50
+ expect(result.code).toEqual(AUTH_RESULT_SUCCESS)
51
+ expect(result.user).toBeTruthy()
52
+ expect(result.user.uid).toEqual('gauss')
53
+ expect(result.user.sn).toEqual('Bar1')
54
+ expect(result.user.cn).toBeUndefined()
55
+ })
56
+ it('Use an admin user to authenticate a regular user', async () => {
57
+ let options = {
58
+ ldapOpts: {
59
+ url: url,
60
+ },
61
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
62
+ adminPassword: 'password',
63
+ userPassword: 'password',
64
+ userSearchBase: 'dc=example,dc=com',
65
+ usernameAttribute: 'uid',
66
+ username: 'gauss',
67
+ }
68
+
69
+ let result = await authenticateResult(options)
70
+ expect(result).toBeInstanceOf(AuthenticationResult)
71
+ expect(result.code).toEqual(AUTH_RESULT_SUCCESS)
72
+ expect(result.user).toBeTruthy()
73
+ expect(result.user.uid).toEqual('gauss')
74
+ })
75
+ it('Use an admin user to authenticate a regular user and return attributes', async () => {
76
+ let options = {
77
+ ldapOpts: {
78
+ url: url,
79
+ },
80
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
81
+ adminPassword: 'password',
82
+ userPassword: 'password',
83
+ userSearchBase: 'dc=example,dc=com',
84
+ usernameAttribute: 'uid',
85
+ username: 'gauss',
86
+ attributes: ['uid', 'sn'],
87
+ }
88
+
89
+ let result = await authenticateResult(options)
90
+ expect(result).toBeInstanceOf(AuthenticationResult)
91
+ expect(result.code).toEqual(AUTH_RESULT_SUCCESS)
92
+ expect(result.user).toBeTruthy()
93
+ expect(result.user.uid).toEqual('gauss')
94
+ expect(result.user.sn).toEqual('Bar1')
95
+ expect(result.user.cn).toBeUndefined()
96
+ })
97
+ it('Use an regular user to authenticate iteself', async () => {
98
+ let options = {
99
+ ldapOpts: {
100
+ url: url,
101
+ },
102
+ userDn: 'cn=einstein,ou=users,dc=example,dc=com',
103
+ userPassword: 'password',
104
+ userSearchBase: 'dc=example,dc=com',
105
+ usernameAttribute: 'uid',
106
+ username: 'einstein',
107
+ }
108
+
109
+ let result = await authenticateResult(options)
110
+ expect(result).toBeInstanceOf(AuthenticationResult)
111
+ expect(result.code).toEqual(AUTH_RESULT_SUCCESS)
112
+ expect(result.user).toBeTruthy()
113
+ expect(result.user.uid).toEqual('einstein')
114
+ })
115
+ it('Use an regular user to authenticate iteself and return attributes', async () => {
116
+ let options = {
117
+ ldapOpts: {
118
+ url: url,
119
+ },
120
+ userDn: 'cn=einstein,ou=users,dc=example,dc=com',
121
+ userPassword: 'password',
122
+ userSearchBase: 'dc=example,dc=com',
123
+ usernameAttribute: 'uid',
124
+ username: 'einstein',
125
+ attributes: ['uid', 'sn'],
126
+ }
127
+
128
+ let result = await authenticateResult(options)
129
+ expect(result).toBeInstanceOf(AuthenticationResult)
130
+ expect(result.code).toEqual(AUTH_RESULT_SUCCESS)
131
+ expect(result.user).toBeTruthy()
132
+ expect(result.user.uid).toEqual('einstein')
133
+ expect(result.user.sn).toEqual('Bar2')
134
+ expect(result.user.cn).toBeUndefined()
135
+ })
136
+ it('Use an regular user to authenticate iteself without search', async () => {
137
+ let options = {
138
+ ldapOpts: {
139
+ url: url,
140
+ },
141
+ userDn: 'cn=einstein,ou=users,dc=example,dc=com',
142
+ userPassword: 'password',
143
+ }
144
+
145
+ let result = await authenticateResult(options)
146
+ expect(result.code).toEqual(AUTH_RESULT_SUCCESS)
147
+ expect(result.user).toBeTruthy()
148
+ })
149
+ it('Use an admin user to authenticate a regular user and fetch user group information', async () => {
150
+ let options = {
151
+ ldapOpts: {
152
+ url: url,
153
+ },
154
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
155
+ adminPassword: 'password',
156
+ userPassword: 'password',
157
+ userSearchBase: 'dc=example,dc=com',
158
+ usernameAttribute: 'uid',
159
+ username: 'gauss',
160
+ groupsSearchBase: 'dc=example,dc=com',
161
+ groupClass: 'groupOfNames',
162
+ groupMemberAttribute: 'member',
163
+ groupMemberUserAttribute: 'dn',
164
+ }
165
+
166
+ let result = await authenticateResult(options)
167
+ expect(result).toBeInstanceOf(AuthenticationResult)
168
+ expect(result.code).toEqual(AUTH_RESULT_SUCCESS)
169
+ expect(result.user).toBeTruthy()
170
+ expect(result.user.groups.length).toBeGreaterThan(0)
171
+ expect(result.user.groups[0].dn).toEqual('cn=科学A部,ou=groups,dc=example,dc=com')
172
+ })
173
+ it('Use regular user to authenticate and fetch user group information', async () => {
174
+ let options = {
175
+ ldapOpts: {
176
+ url: url,
177
+ },
178
+ userDn: 'cn=gauss,ou=users,dc=example,dc=com',
179
+ userPassword: 'password',
180
+ userSearchBase: 'dc=example,dc=com',
181
+ usernameAttribute: 'uid',
182
+ username: 'gauss',
183
+ groupsSearchBase: 'dc=example,dc=com',
184
+ groupClass: 'groupOfNames',
185
+ groupMemberAttribute: 'member',
186
+ groupMemberUserAttribute: 'dn',
187
+ }
188
+
189
+ let result = await authenticateResult(options)
190
+ expect(result).toBeInstanceOf(AuthenticationResult)
191
+ expect(result.code).toEqual(AUTH_RESULT_SUCCESS)
192
+ expect(result.user).toBeTruthy()
193
+ expect(result.user.groups.length).toBeGreaterThan(0)
194
+ expect(result.user.groups[0].dn).toEqual('cn=科学A部,ou=groups,dc=example,dc=com')
195
+ // backward compatible with 3.2
196
+ expect(result.user.groups[0].objectName).toEqual(
197
+ 'cn=科学A部,ou=groups,dc=example,dc=com'
198
+ )
199
+ })
200
+ it('Not specifying groupMemberAttribute or groupMemberUserAttribute should not cause an error and fallback to default values', async () => {
201
+ let options = {
202
+ ldapOpts: {
203
+ url: url,
204
+ },
205
+ userDn: 'cn=gauss,ou=users,dc=example,dc=com',
206
+ userPassword: 'password',
207
+ userSearchBase: 'dc=example,dc=com',
208
+ usernameAttribute: 'uid',
209
+ username: 'gauss',
210
+ groupsSearchBase: 'dc=example,dc=com',
211
+ groupClass: 'groupOfUniqueNames',
212
+ }
213
+
214
+ let result = await authenticateResult(options)
215
+ expect(result).toBeInstanceOf(AuthenticationResult)
216
+ expect(result.code).toEqual(AUTH_RESULT_SUCCESS)
217
+ expect(result.user).toBeTruthy()
218
+ expect(result.user.groups.length).toBeLessThan(1)
219
+ })
220
+ })
221
+
222
+ describe('ldap-authentication negative test returns AuthenticationResult', () => {
223
+ it('wrong admin user should fail', async () => {
224
+ let options = {
225
+ ldapOpts: {
226
+ url: url,
227
+ },
228
+ adminDn: 'cn=not-exist,dc=example,dc=com',
229
+ adminPassword: 'password',
230
+ userPassword: 'password',
231
+ userSearchBase: 'dc=example,dc=com',
232
+ usernameAttribute: 'uid',
233
+ username: 'gauss',
234
+ }
235
+
236
+ let result = await authenticateResult(options)
237
+ expect(result).toBeInstanceOf(AuthenticationResult)
238
+ expect(result.code).toEqual(AUTH_RESULT_FAILURE)
239
+ })
240
+ it('wrong admin password should fail', async () => {
241
+ let options = {
242
+ ldapOpts: {
243
+ url: url,
244
+ },
245
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
246
+ adminPassword: 'wrongpassword',
247
+ userPassword: 'password',
248
+ userSearchBase: 'dc=example,dc=com',
249
+ usernameAttribute: 'uid',
250
+ username: 'gauss',
251
+ }
252
+
253
+ let result = await authenticateResult(options)
254
+ expect(result).toBeInstanceOf(AuthenticationResult)
255
+ expect(result.code).toEqual(AUTH_RESULT_FAILURE)
256
+ })
257
+ it('admin auth wrong username should fail', async () => {
258
+ let options = {
259
+ ldapOpts: {
260
+ url: url,
261
+ },
262
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
263
+ adminPassword: 'password',
264
+ userPassword: 'password',
265
+ userSearchBase: 'dc=example,dc=com',
266
+ usernameAttribute: 'uid',
267
+ username: 'wrong',
268
+ }
269
+
270
+ let result = await authenticateResult(options)
271
+ expect(result).toBeInstanceOf(AuthenticationResult)
272
+ expect(result.code).toEqual(AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND)
273
+ })
274
+ it('admin auth wrong user password should fail', async () => {
275
+ let options = {
276
+ ldapOpts: {
277
+ url: url,
278
+ },
279
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
280
+ adminPassword: 'password',
281
+ userPassword: 'wrongpassword',
282
+ userSearchBase: 'dc=example,dc=com',
283
+ usernameAttribute: 'uid',
284
+ username: 'gauss',
285
+ }
286
+
287
+ let result = await authenticateResult(options)
288
+ expect(result).toBeInstanceOf(AuthenticationResult)
289
+ expect(result.code).toEqual(AUTH_RESULT_FAILURE_CREDENTIAL_INVALID)
290
+ })
291
+ it('user auth wrong username should fail', async () => {
292
+ let options = {
293
+ ldapOpts: {
294
+ url: url,
295
+ },
296
+ userDn: 'cn=not-exist,dc=example,dc=com',
297
+ userPassword: 'password',
298
+ userSearchBase: 'dc=example,dc=com',
299
+ usernameAttribute: 'uid',
300
+ username: 'gauss',
301
+ }
302
+
303
+ let result = await authenticateResult(options)
304
+ expect(result).toBeInstanceOf(AuthenticationResult)
305
+ expect(result.code).toEqual(AUTH_RESULT_FAILURE)
306
+ })
307
+ it('user auth wrong user password should fail', async () => {
308
+ let options = {
309
+ ldapOpts: {
310
+ url: url,
311
+ },
312
+ userDn: 'cn=gauss,dc=example,dc=com',
313
+ userPassword: 'wrongpassword',
314
+ userSearchBase: 'dc=example,dc=com',
315
+ usernameAttribute: 'uid',
316
+ username: 'gauss',
317
+ }
318
+
319
+ let result = await authenticateResult(options)
320
+ expect(result).toBeInstanceOf(AuthenticationResult)
321
+ expect(result.code).toEqual(AUTH_RESULT_FAILURE)
322
+ })
323
+ it('Use an regular user to authenticate iteself without search with wrong password should fail', async () => {
324
+ let options = {
325
+ ldapOpts: {
326
+ url: url,
327
+ },
328
+ userDn: 'uid=einstein,dc=example,dc=com',
329
+ userPassword: 'wrongpassword',
330
+ }
331
+
332
+ let result = await authenticateResult(options)
333
+ expect(result).toBeInstanceOf(AuthenticationResult)
334
+ expect(result.code).toEqual(AUTH_RESULT_FAILURE)
335
+ })
336
+ it('Wrong attributes give failure', async () => {
337
+ let options = {
338
+ ldapOpts: {
339
+ url: url,
340
+ },
341
+ userDn: 'cn=einstein,ou=users,dc=example,dc=com',
342
+ userPassword: 'password',
343
+ usernameAttribute: 'wrongattribute',
344
+ userSearchBase: 'dc=example,dc=com',
345
+ username: 'einstein',
346
+ }
347
+
348
+ let result = await authenticateResult(options)
349
+ expect(result).toBeInstanceOf(AuthenticationResult)
350
+ expect(result.code).toEqual(AUTH_RESULT_FAILURE)
351
+ })
352
+ it('Unreachable ldap server should return failure', async () => {
353
+ let options = {
354
+ ldapOpts: {
355
+ url: 'ldap://x.forumsys.com',
356
+ connectTimeout: 2000,
357
+ },
358
+ userDn: 'uid=einstein,dc=example,dc=com',
359
+ userPassword: 'password',
360
+ usernameAttribute: 'cn',
361
+ userSearchBase: 'dc=example,dc=com',
362
+ username: 'einstein',
363
+ }
364
+
365
+ let result = await authenticateResult(options)
366
+ expect(result).toBeInstanceOf(AuthenticationResult)
367
+ expect(result.code).toEqual(AUTH_RESULT_FAILURE)
368
+ })
369
+ it('Unreachable ldap server should return failure (with starttls=true)', async () => {
370
+ let options = {
371
+ ldapOpts: {
372
+ url: 'ldap://x.forumsys.com',
373
+ connectTimeout: 2000,
374
+ },
375
+ userDn: 'uid=einstein,dc=example,dc=com',
376
+ userPassword: 'password',
377
+ usernameAttribute: 'cn',
378
+ userSearchBase: 'dc=example,dc=com',
379
+ username: 'einstein',
380
+ starttls: true,
381
+ }
382
+
383
+ let result = await authenticateResult(options)
384
+ expect(result).toBeInstanceOf(AuthenticationResult)
385
+ expect(result.code).toEqual(AUTH_RESULT_FAILURE)
386
+ })
387
+ it('Unmatched supplied groupMemberUserAttribute should return empty group list', async () => {
388
+ let options = {
389
+ ldapOpts: {
390
+ url: url,
391
+ },
392
+ userDn: 'cn=gauss,ou=users,dc=example,dc=com',
393
+ userPassword: 'password',
394
+ userSearchBase: 'dc=example,dc=com',
395
+ usernameAttribute: 'uid',
396
+ username: 'gauss',
397
+ groupsSearchBase: 'dc=example,dc=com',
398
+ groupClass: 'groupOfNames',
399
+ groupMemberAttribute: 'member',
400
+ groupMemberUserAttribute: 'dnWRONG',
401
+ }
402
+
403
+ let result = await authenticateResult(options)
404
+ expect(result).toBeInstanceOf(AuthenticationResult)
405
+ expect(result.user).toBeTruthy()
406
+ expect(result.user.groups.length).toBeLessThan(1)
407
+ })
408
+ })
409
+
410
+ describe('AuthenticationResult', () => {
411
+ it('AuthenticationResult creation and getters', async () => {
412
+ const result = new AuthenticationResult(
413
+ AUTH_RESULT_SUCCESS,
414
+ 'testuser',
415
+ { cn: 'Test User' },
416
+ ['Authentication successful'],
417
+ {}
418
+ )
419
+
420
+ expect(result.code).toBe(AUTH_RESULT_SUCCESS)
421
+ expect(result.identity).toBe('testuser')
422
+ expect(result.user).toEqual({ cn: 'Test User' })
423
+ expect(result.messages).toEqual(['Authentication successful'])
424
+ })
425
+ })
@@ -0,0 +1,115 @@
1
+ const { Change } = require('ldapts')
2
+ const { authenticate, LdapAuthenticationError } = require('../index.js')
3
+ const ldapts = require('ldapts')
4
+ const { Attribute } = require('ldapts')
5
+
6
+ const url = process.env.INGITHUB ? 'ldap://localhost:1389' : 'ldap://ldap:1389'
7
+
8
+ describe('ldap-authentication binary attributes test', () => {
9
+ const jpegPhotoBase64 =
10
+ '/9j/4AAQSkZJRgABAQEASABIAAD/2wBDACgcHiMeGSgjISMtKygwPGRBPDc3PHtYXUlkkYCZlo+AjIqgtObDoKrarYqMyP/L2u71////m8H////6/+b9//j/2wBDASstLTw1PHZBQXb4pYyl+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj/wAARCACBAGQDAREAAhEBAxEB/8QAGQAAAwEBAQAAAAAAAAAAAAAAAAECAwQF/8QALRAAAgEDAwMCBQQDAAAAAAAAAAECAxEhBBIxMkFRInEFE2GBsRU0QlJikaH/xAAXAQEBAQEAAAAAAAAAAAAAAAAAAQID/8QAGxEBAQEAAwEBAAAAAAAAAAAAAAERAhIhMUH/2gAMAwEAAhEDEQA/APMIAAKAgChp2AdwAAACBlAQUkmu5RkAAACuAAADsAZQDWQKRA7AL7gAEFCbAQDAYAAXAdwBYAogAAAAgokBgADAEr8Ab09JWqZcdq8yJpjb9Pa5qr/ROy4l6Ga6ZxfvgdoYiemqw6o49yoyAAACCiQKa4AAN9PpnWy3aP5Jbiya9ClRhBJRjby/Ji1qRqRQBMkuwMZSk2trdiys2PPqR2zaNspAAIKE0A+yA201H51TPSuSW4smvTilFJJWSOdbi0RTZQghPgK56nNwzXLXtvx4NxhiUAEABQ0nJpJcgejpkoR2rtyznWo3uRpE5S4i8hfRCrPKm0wLlUUUrgZSruXTD/oROZPISuat1s3GGRQgIKADp0kFNy8pYM2rJrshBQ4M1qNErojTOdFyjZTcfqhKWaqFPObuyAdeKlBfRhcZrT01JzXdWsXWc/U7XF5IVlqWm1waYczKhXKIKEB0aOW2uvDwZvxZ9ejbBlq+CLsZaWrMAckvSgpSW6DQVNKW6ObXRUTVd5BmuKp1M05s2USBmaABpS60Qeg41rqUXeMcszjW61auroliyi7sZaFk1YKirVnSVopyXkqajTNtSv3YouXU/oWM8nHLLZWENFCsBiaAB1aCj86tnpjlko9javBlUVY29SWO4VizNaiVBJcu/m5FJwvyFTGmqct+Como3Juz5EZrnZplLKhWAw5NDZxisW47gen8NpqOncsepmarrIBZwwOWunSf+L4ZmxqVnvI1puoU1nObs347BLS7FZLZ8x27sIwqQcJuMuUaRAGdKN5X8ZKHJu+Qrq02venpbNilnGbDB3UNXHUJ7dqmv4szfA46luVnTbf0ZNFyqUpxcKnpv2kUclWiqT9LvF8O5mtRNnZO+ChSTccAOzsGShVhSlullrhFHPObnNyfLKiAHQW2lKfngqxk3dgSyhwm4TUk3dPDIPV0OrhWltmlGr2fkxYrqrUVVg0+ez8GdHPGhLa0/uaRMFeW1K9wrd6e7vJ2XhFRjKnGqtlNtPyyTB5MrqV28nRFxnf3JgogSqp0FBKzRVZlAEKKyFCbjO6eUQexotaq6UKjtUXf+xz5ccWOmas91u2RKVjHZSlaTe210yoipqHNbY3UfyTVEI7nbyUc+r0vLiuFc1Kjz7WNItSxkmCShAAAgCeXewURbTusNBHs6LWRrpQqO1T8nOzGmtamnG1unK9gM4UknfgqNYxSzZsBuTtiOL2ZKPJ1lFU6zt0yymblRhYonuQD4KEAAOXIUkEa6f8AcQ90Sq92r2+5zioXWvY0jQqJkRXn/EeiHuywcS4NI//Z'
11
+
12
+ const baseOptions = {
13
+ ldapOpts: {
14
+ url: url,
15
+ },
16
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
17
+ adminPassword: 'password',
18
+ verifyUserExists: true,
19
+ userSearchBase: 'dc=example,dc=com',
20
+ usernameAttribute: 'uid',
21
+ }
22
+
23
+ it('Add jpegPhoto attribute', async () => {
24
+ let client = new ldapts.Client({
25
+ ...baseOptions.ldapOpts,
26
+ })
27
+ try {
28
+ await client.bind(baseOptions.adminDn, baseOptions.adminPassword)
29
+
30
+ // https://github.com/ldapts/ldapts/issues/12
31
+ await client.modify(
32
+ 'cn=gauss,ou=users,dc=example,dc=com',
33
+ new Change({
34
+ operation: 'replace',
35
+ modification: new Attribute({
36
+ type: 'jpegPhoto',
37
+ values: [Buffer.from(jpegPhotoBase64, 'base64')],
38
+ }),
39
+ })
40
+ )
41
+ } finally {
42
+ await client.unbind()
43
+ }
44
+ })
45
+
46
+ it('Should return broken jpegPhoto attribute (no attribute selection nor ;binary) - But it really depends on LDAP server, it is not always true. Sometimes a buffer is returned directly.', async () => {
47
+ let user = await authenticate({
48
+ ...baseOptions,
49
+ username: 'gauss',
50
+ })
51
+
52
+ expect(user).toBeTruthy()
53
+ expect(user.uid).toEqual('gauss')
54
+ expect(user.sn).toEqual('Bar1')
55
+ expect(typeof user.uidNumber === 'string').toBe(true)
56
+ expect(user.uidNumber).toEqual('1000')
57
+
58
+ expect(user.jpegPhoto).toBeDefined()
59
+ // some ldap server returns a string, some ldap server returns a buffer
60
+ expect(
61
+ typeof user.jpegPhoto === 'string' || Buffer.isBuffer(user.jpegPhoto)
62
+ ).toBe(true)
63
+ if (typeof user.jpegPhoto === 'string') {
64
+ expect(user.jpegPhoto).not.toEqual(jpegPhotoBase64)
65
+ }
66
+ if (Buffer.isBuffer(user.jpegPhoto)) {
67
+ expect(
68
+ user.jpegPhoto.equals(Buffer.from(jpegPhotoBase64, 'base64'))
69
+ ).toBeTrue()
70
+ }
71
+ })
72
+
73
+ it('Should return nothing in the base64 jpegPhoto (using ;binary)', async () => {
74
+ let user = await authenticate({
75
+ ...baseOptions,
76
+ username: 'gauss',
77
+ attributes: ['uid', 'sn', 'jpegPhoto;binary'],
78
+ })
79
+
80
+ expect(user).toBeTruthy()
81
+ expect(user.uid).toEqual('gauss')
82
+ expect(user.sn).toEqual('Bar1')
83
+ expect(user.cn).toBeUndefined()
84
+
85
+ expect(user.jpegPhoto).toBeUndefined()
86
+
87
+ expect(user['jpegPhoto;binary']).toBeDefined()
88
+ expect(Array.isArray(user['jpegPhoto;binary'])).toBe(true)
89
+ expect(user['jpegPhoto;binary'].length).toBe(0)
90
+ })
91
+
92
+ it('Should return base64 jpegPhoto (using explicitBufferAttributes)', async () => {
93
+ let user = await authenticate({
94
+ ...baseOptions,
95
+ username: 'gauss',
96
+ attributes: ['uid', 'sn', 'jpegPhoto'],
97
+ explicitBufferAttributes: ['jpegPhoto'],
98
+ })
99
+
100
+ expect(user).toBeTruthy()
101
+ expect(user.uid).toEqual('gauss')
102
+ expect(user.sn).toEqual('Bar1')
103
+ expect(user.cn).toBeUndefined()
104
+
105
+ expect(user['jpegPhoto;binary']).toBeUndefined()
106
+
107
+ expect(user.jpegPhoto).toBeDefined()
108
+ expect(typeof user.jpegPhoto === 'string').toBe(true)
109
+ expect(user.jpegPhoto).toEqual(jpegPhotoBase64)
110
+
111
+ const buffer = Buffer.from(user.jpegPhoto, 'base64')
112
+ expect(buffer).toBeDefined()
113
+ expect(buffer.length).toBeGreaterThan(0)
114
+ })
115
+ })
@@ -0,0 +1,126 @@
1
+ const { authenticate, LdapAuthenticationError } = require('../index.js')
2
+
3
+ const url = process.env.INGITHUB ? 'ldap://localhost:1389' : 'ldap://ldap:1389'
4
+
5
+ describe('ldap-authentication StartTLS and TLS options test', () => {
6
+ it('Plain LDAP with tlsOptions in ldapOpts should work (ldap:// protocol)', async () => {
7
+ // Regression test: Before fix, having tlsOptions with ldap:// URL caused issues
8
+ // After fix: tlsOptions are properly excluded from Client constructor for ldap:// URLs
9
+ let options = {
10
+ ldapOpts: {
11
+ url: url,
12
+ tlsOptions: {
13
+ rejectUnauthorized: false,
14
+ },
15
+ },
16
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
17
+ adminPassword: 'password',
18
+ userPassword: 'password',
19
+ userSearchBase: 'dc=example,dc=com',
20
+ usernameAttribute: 'uid',
21
+ username: 'gauss',
22
+ }
23
+
24
+ let user = await authenticate(options)
25
+ expect(user).toBeTruthy()
26
+ expect(user.uid).toEqual('gauss')
27
+ })
28
+
29
+ it('Use an admin user to authenticate with StartTLS (may skip if TLS not configured)', async () => {
30
+ // Note: This test may not fully succeed if the LDAP server lacks TLS certificates
31
+ // However, it should NOT fail with the original ECONNRESET bug
32
+ let options = {
33
+ ldapOpts: {
34
+ url: url,
35
+ tlsOptions: {
36
+ rejectUnauthorized: false,
37
+ minVersion: 'TLSv1.2',
38
+ },
39
+ },
40
+ starttls: true,
41
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
42
+ adminPassword: 'password',
43
+ userPassword: 'password',
44
+ userSearchBase: 'dc=example,dc=com',
45
+ usernameAttribute: 'uid',
46
+ username: 'gauss',
47
+ }
48
+
49
+ try {
50
+ let user = await authenticate(options)
51
+ // If this succeeds, StartTLS is fully working!
52
+ expect(user).toBeTruthy()
53
+ expect(user.uid).toEqual('gauss')
54
+ } catch (error) {
55
+ // Expected if StartTLS is not configured on the server
56
+ // The critical check: should NOT be the original ECONNRESET bug
57
+ if (error.code === 'ECONNRESET' &&
58
+ error.message && error.message.includes('Client network socket disconnected before secure TLS connection')) {
59
+ fail('ECONNRESET bug detected: tlsOptions should NOT be passed to Client constructor when using ldap:// URL')
60
+ }
61
+ // Other errors are acceptable (e.g., server doesn't support StartTLS)
62
+ expect(error).toBeTruthy()
63
+ }
64
+ })
65
+
66
+ it('Use a regular user to authenticate with StartTLS (self mode)', async () => {
67
+ let options = {
68
+ ldapOpts: {
69
+ url: url,
70
+ tlsOptions: {
71
+ rejectUnauthorized: false,
72
+ minVersion: 'TLSv1.2',
73
+ },
74
+ },
75
+ starttls: true,
76
+ userDn: 'cn=einstein,ou=users,dc=example,dc=com',
77
+ userPassword: 'password',
78
+ userSearchBase: 'dc=example,dc=com',
79
+ usernameAttribute: 'uid',
80
+ username: 'einstein',
81
+ }
82
+
83
+ try {
84
+ let user = await authenticate(options)
85
+ expect(user).toBeTruthy()
86
+ expect(user.uid).toEqual('einstein')
87
+ } catch (error) {
88
+ if (error.code === 'ECONNRESET' &&
89
+ error.message && error.message.includes('Client network socket disconnected before secure TLS connection')) {
90
+ fail('ECONNRESET bug detected: tlsOptions should NOT be passed to Client constructor when using ldap:// URL')
91
+ }
92
+ expect(error).toBeTruthy()
93
+ }
94
+ })
95
+
96
+ it('Verify user exists with StartTLS', async () => {
97
+ let options = {
98
+ ldapOpts: {
99
+ url: url,
100
+ tlsOptions: {
101
+ rejectUnauthorized: false,
102
+ minVersion: 'TLSv1.2',
103
+ },
104
+ },
105
+ starttls: true,
106
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
107
+ adminPassword: 'password',
108
+ verifyUserExists: true,
109
+ userSearchBase: 'dc=example,dc=com',
110
+ usernameAttribute: 'uid',
111
+ username: 'gauss',
112
+ }
113
+
114
+ try {
115
+ let user = await authenticate(options)
116
+ expect(user).toBeTruthy()
117
+ expect(user.uid).toEqual('gauss')
118
+ } catch (error) {
119
+ if (error.code === 'ECONNRESET' &&
120
+ error.message && error.message.includes('Client network socket disconnected before secure TLS connection')) {
121
+ fail('ECONNRESET bug detected: tlsOptions should NOT be passed to Client constructor when using ldap:// URL')
122
+ }
123
+ expect(error).toBeTruthy()
124
+ }
125
+ })
126
+ })
package/test/test.spec.js CHANGED
@@ -144,7 +144,7 @@ describe('ldap-authentication test', () => {
144
144
  let user = await authenticate(options)
145
145
  expect(user).toBeTruthy()
146
146
  expect(user.groups.length).toBeGreaterThan(0)
147
- expect(user.groups[0].dn).toEqual('cn=科学A部,ou=users,dc=example,dc=com')
147
+ expect(user.groups[0].dn).toEqual('cn=科学A部,ou=groups,dc=example,dc=com')
148
148
  })
149
149
  it('Use regular user to authenticate and fetch user group information', async () => {
150
150
  let options = {
@@ -165,10 +165,10 @@ describe('ldap-authentication test', () => {
165
165
  let user = await authenticate(options)
166
166
  expect(user).toBeTruthy()
167
167
  expect(user.groups.length).toBeGreaterThan(0)
168
- expect(user.groups[0].dn).toEqual('cn=科学A部,ou=users,dc=example,dc=com')
168
+ expect(user.groups[0].dn).toEqual('cn=科学A部,ou=groups,dc=example,dc=com')
169
169
  // backward compatible with 3.2
170
170
  expect(user.groups[0].objectName).toEqual(
171
- 'cn=科学A部,ou=users,dc=example,dc=com'
171
+ 'cn=科学A部,ou=groups,dc=example,dc=com'
172
172
  )
173
173
  })
174
174
  it('Not specifying groupMemberAttribute or groupMemberUserAttribute should not cause an error and fallback to default values', async () => {