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/index.js CHANGED
@@ -1,6 +1,6 @@
1
- const assert = require('assert')
2
1
  const ldapts = require('ldapts')
3
- // escape the , in CN in DN
2
+
3
+ // escape the , in the value of the first RDN of a DN
4
4
  function _ldapEscapeDN(s) {
5
5
  let ret = ''
6
6
  let comaPositions = []
@@ -48,6 +48,31 @@ const AUTH_RESULT_FAILURE_UNCATEGORIZED = -4
48
48
  const DEFAULT_FETCH_USERS_FILTER = '(|(uid=*)(sAMAccountName=*))'
49
49
  const DEFAULT_FETCH_USERS_PAGE_SIZE = 1000
50
50
 
51
+ /**
52
+ * Thrown by authenticate()/authenticateResult()/fetchUsers() on failure.
53
+ * `message` describes the failure; `code` (when set) is one of the
54
+ * AUTH_RESULT_* constants, mirroring the outcome that authenticateResult()
55
+ * reports for the same failure. Missing required options also throw this
56
+ * error, with all the missing fields listed in `message`.
57
+ */
58
+ class LdapAuthenticationError extends Error {
59
+ constructor(message, code) {
60
+ super(message)
61
+ // Ensure the name of this error is the same as the class name
62
+ this.name = this.constructor.name
63
+ if (code !== undefined) {
64
+ this.code = code
65
+ }
66
+ // This clips the constructor invocation from the stack trace.
67
+ // It's not absolutely essential, but it does make the stack trace a little nicer.
68
+ Error.captureStackTrace(this, this.constructor)
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Result object returned by {@link authenticateResult}. Inspect `code` (one
74
+ * of the AUTH_RESULT_* constants) and `messages` to classify failures.
75
+ */
51
76
  class AuthenticationResult {
52
77
  #authCode = AUTH_RESULT_FAILURE_UNCATEGORIZED
53
78
  #identity
@@ -93,11 +118,17 @@ const authenticationMessages = {
93
118
  AUTH_RESULT_FAILURE_UNCATEGORIZED: 'Uncategorized authentication failure',
94
119
  }
95
120
 
96
- // bind and return the ldap client
97
- async function _ldapBind(dn, password, starttls, ldapOpts) {
98
- // TODO: check if ldapts expects escaped dn or not (possible double escaping problems?)
121
+ // bind with (dn, password) and return the connected ldap client.
122
+ // If the connection or the bind fails, the client is unbound again before
123
+ // the error is rethrown, so callers never leak a connected socket.
124
+ async function _ldapBind(dn, password, { starttls, ldapOpts }) {
125
+ // ldapts passes a string DN through to the server as-is (no escaping is
126
+ // done by ldapts), so the value of the first RDN is escaped here (a DN
127
+ // like `cn=Doe, John,ou=users,...` would otherwise be parsed by the server
128
+ // as three RDNs instead of one)
99
129
  dn = _ldapEscapeDN(dn)
100
- ldapOpts.connectTimeout = ldapOpts.connectTimeout || 5000
130
+ const opts = { ...ldapOpts }
131
+ opts.connectTimeout = ldapOpts.connectTimeout || 5000
101
132
 
102
133
  // When using StartTLS, we need to exclude tlsOptions from the Client constructor
103
134
  // and only pass them to the startTLS() method to avoid connection conflicts.
@@ -105,74 +136,93 @@ async function _ldapBind(dn, password, starttls, ldapOpts) {
105
136
  // - For LDAPS (ldaps://): pass tlsOptions to Client constructor
106
137
  // - For StartTLS (ldap://): do NOT pass tlsOptions to Client constructor, only to startTLS()
107
138
  // - For plain LDAP (ldap://): do NOT pass tlsOptions to Client constructor
108
- let clientOpts = ldapOpts
109
- const isLdaps = ldapOpts.url && ldapOpts.url.startsWith('ldaps://')
110
-
111
- // Only pass tlsOptions to Client constructor if using ldaps:// protocol
112
- // For ldap:// protocol (plain or StartTLS), exclude tlsOptions from constructor
113
- if (!isLdaps && ldapOpts.tlsOptions) {
114
- // Create a shallow copy of ldapOpts without tlsOptions for the Client constructor
115
- const { tlsOptions, ...optsWithoutTls } = ldapOpts
116
- clientOpts = optsWithoutTls
139
+ const isLdaps = opts.url && opts.url.startsWith('ldaps://')
140
+ if (!isLdaps) {
141
+ delete opts.tlsOptions
117
142
  }
118
143
 
119
- let client = new ldapts.Client(clientOpts)
120
-
121
- if (starttls) {
122
- await client.startTLS(ldapOpts.tlsOptions)
144
+ const client = new ldapts.Client(opts)
145
+ try {
146
+ if (starttls) {
147
+ await client.startTLS(ldapOpts.tlsOptions)
148
+ }
149
+ await client.bind(dn, password)
150
+ } catch (error) {
151
+ if (client.isConnected) {
152
+ try {
153
+ await client.unbind()
154
+ } catch (unbindError) {
155
+ // the socket was probably already closed; nothing else to do
156
+ }
157
+ }
158
+ throw error
123
159
  }
124
-
125
- await client.bind(dn, password)
126
160
  ldapOpts.log && ldapOpts.log.trace('bind success!')
127
161
  return client
128
162
  }
129
163
 
130
- // replace username in filter
131
-
164
+ // convert attribute values that ldapts returned as Buffer (attributes with
165
+ // a `;binary` suffix, or the ones listed in explicitBufferAttributes) into
166
+ // base64 strings
167
+ function _toBase64Attributes(user, attributes, explicitBufferAttributes) {
168
+ if (user == null) {
169
+ return
170
+ }
171
+ // when attribute endwith ;binary, ldapts returns Buffer, we convert them into base64 string
172
+ if (attributes != null) {
173
+ for (let attr of attributes) {
174
+ if (attr.endsWith(';binary') && Buffer.isBuffer(user[attr])) {
175
+ user[attr] = user[attr].toString('base64')
176
+ }
177
+ }
178
+ }
179
+ // when attribute is one of the explicitBufferAttributes, should convert to base64 string
180
+ if (explicitBufferAttributes != null) {
181
+ for (let attr of explicitBufferAttributes) {
182
+ if (Buffer.isBuffer(user[attr])) {
183
+ user[attr] = user[attr].toString('base64')
184
+ }
185
+ }
186
+ }
187
+ }
132
188
 
133
189
  // search a user and return the object
134
- async function _searchUser(
135
- ldapClient,
136
- searchBase,
137
- usernameFilter,
138
- usernameAttribute,
139
- username,
140
- attributes = null,
141
- explicitBufferAttributes = null
142
- ) {
143
- let filter;
144
- if(usernameFilter){
145
- filter = usernameFilter.replaceAll("{{username}}",username.replaceAll(/[&|!*()]/g,""));
146
- }
147
- else{
190
+ async function _searchUser(client, options) {
191
+ const {
192
+ userSearchBase,
193
+ usernameFilter,
194
+ usernameAttribute,
195
+ username,
196
+ attributes = null,
197
+ explicitBufferAttributes = null,
198
+ } = options
199
+
200
+ let filter
201
+ if (usernameFilter) {
202
+ // replace `{{username}}` with the RFC 2254-escaped username, so LDAP
203
+ // filter metacharacters inside the username cannot change the structure
204
+ // of the filter
205
+ filter = usernameFilter.replaceAll('{{username}}', ldapts.Filter.escape(username))
206
+ } else {
148
207
  filter = new ldapts.EqualityFilter({
149
208
  attribute: usernameAttribute,
150
209
  value: username,
151
210
  })
152
211
  }
153
-
212
+
154
213
  let searchOptions = {
155
214
  filter: filter,
156
215
  scope: 'sub',
157
- attributes: attributes,
158
216
  }
159
217
  if (attributes) {
160
218
  searchOptions.attributes = attributes
161
219
  }
162
- if(explicitBufferAttributes) {
220
+ if (explicitBufferAttributes) {
163
221
  searchOptions.explicitBufferAttributes = explicitBufferAttributes
164
222
  }
165
223
 
166
224
  // TODO: we don't support reference yet
167
- // If the server was able to locate the entry referred to by the baseObject
168
- // but could not search one or more non-local entries,
169
- // the server may return one or more SearchResultReference messages,
170
- // each containing a reference to another set of servers for continuing the operation.
171
- // referral.uris
172
- const { searchEntries, searchReferences } = await ldapClient.search(
173
- searchBase,
174
- searchOptions
175
- )
225
+ const { searchEntries } = await client.search(userSearchBase, searchOptions)
176
226
 
177
227
  let user
178
228
  if (
@@ -181,64 +231,52 @@ async function _searchUser(
181
231
  !searchEntries[0] ||
182
232
  !searchEntries[0].dn
183
233
  ) {
234
+ user = null
235
+ } else if (searchEntries.length > 1) {
184
236
  return new AuthenticationResult(
185
- AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND,
237
+ AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS,
186
238
  username,
187
239
  null,
188
- [authenticationMessages.AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND],
189
- ldapClient
240
+ [authenticationMessages.AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS],
241
+ client
190
242
  )
191
243
  } else {
192
- if (searchEntries.length > 1) {
193
- return new AuthenticationResult(
194
- AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS,
195
- username,
196
- null,
197
- [authenticationMessages.AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS],
198
- ldapClient
199
- )
200
- }
201
-
202
244
  user = searchEntries[0]
203
245
  }
204
246
 
205
- // when attribute endwith ;binary, ldapts returns Buffer, we convert them into base64 string
206
- if (user != null && attributes != null) {
207
- for (let attr of attributes) {
208
- if (attr.endsWith(';binary') && Buffer.isBuffer(user[attr])) {
209
- user[attr] = user[attr].toString('base64')
210
- }
211
- }
212
- }
213
- // when attribute is one of the explicitBufferAttributes, should convert to base64 string
214
- if (user != null && explicitBufferAttributes != null) {
215
- for (let attr of explicitBufferAttributes) {
216
- if (Buffer.isBuffer(user[attr])) {
217
- user[attr] = user[attr].toString('base64')
218
- }
219
- }
247
+ if (!user) {
248
+ return new AuthenticationResult(
249
+ AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND,
250
+ username,
251
+ null,
252
+ [authenticationMessages.AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND],
253
+ client
254
+ )
220
255
  }
221
256
 
257
+ _toBase64Attributes(user, attributes, explicitBufferAttributes)
222
258
  return new AuthenticationResult(
223
259
  AUTH_RESULT_SUCCESS,
224
260
  username,
225
261
  user,
226
262
  [authenticationMessages.AUTH_RESULT_SUCCESS],
227
- ldapClient
263
+ client
228
264
  )
229
265
  }
230
266
 
231
- // search a groups which user is member
232
- async function _searchUserGroups(
233
- ldapClient,
234
- searchBase,
235
- user,
236
- groupClass,
237
- groupMemberAttribute = 'member',
238
- groupMemberUserAttribute = 'dn'
239
- ) {
240
- // Below works, but prefer using ldapts Filter subclasses to build this search, so that correct escaping is done
241
- // const filter = `(&(objectclass=${groupClass})(${groupMemberAttribute}=${user[groupMemberUserAttribute]}))`
267
+ // search the groups which user is member and attach them to user.groups;
268
+ // does nothing when group lookup is not configured
269
+ async function _attachGroups(client, user, options) {
270
+ const {
271
+ groupsSearchBase,
272
+ groupClass,
273
+ groupMemberAttribute = 'member',
274
+ groupMemberUserAttribute = 'dn',
275
+ } = options
276
+ if (!groupsSearchBase || !groupClass || !groupMemberAttribute) {
277
+ return
278
+ }
279
+
242
280
  const filter = new ldapts.AndFilter({
243
281
  filters: [
244
282
  new ldapts.EqualityFilter({
@@ -252,20 +290,12 @@ async function _searchUserGroups(
252
290
  ],
253
291
  })
254
292
 
255
- const { searchEntries, searchReferences } = await ldapClient.search(
256
- searchBase,
257
- {
258
- filter: filter,
259
- scope: 'sub',
260
- }
261
- )
293
+ const { searchEntries } = await client.search(groupsSearchBase, {
294
+ filter: filter,
295
+ scope: 'sub',
296
+ })
262
297
 
263
- let groups
264
- if (!searchEntries || searchEntries.length < 1) {
265
- groups = []
266
- } else {
267
- groups = searchEntries
268
- }
298
+ let groups = searchEntries || []
269
299
  // ldapjs has group.objectName, ldapts does not have it. instead, use dn
270
300
  // add objectName back for backward compatibility
271
301
  for (let group of groups) {
@@ -273,22 +303,21 @@ async function _searchUserGroups(
273
303
  group.objectName = group.dn
274
304
  }
275
305
  }
276
- return groups
306
+ user.groups = groups
277
307
  }
278
308
 
279
309
  // 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
310
+ async function _fetchAllUsers(client, options) {
311
+ const {
312
+ userSearchBase,
313
+ userFilter,
314
+ attributes = null,
315
+ explicitBufferAttributes = null,
316
+ pageSize = DEFAULT_FETCH_USERS_PAGE_SIZE,
317
+ } = options
289
318
 
290
319
  let searchOptions = {
291
- filter: filter,
320
+ filter: userFilter || DEFAULT_FETCH_USERS_FILTER,
292
321
  scope: 'sub',
293
322
  // always use paged results, so more than the usual server-side limit
294
323
  // (usually 1000 entries per page) can be returned
@@ -301,60 +330,24 @@ async function _fetchAllUsers(
301
330
  searchOptions.explicitBufferAttributes = explicitBufferAttributes
302
331
  }
303
332
 
304
- const { searchEntries } = await ldapClient.search(searchBase, searchOptions)
333
+ const { searchEntries } = await client.search(userSearchBase, searchOptions)
305
334
 
306
335
  let users = searchEntries || []
307
- // when attribute endwith ;binary, ldapts returns Buffer, we convert them into base64 string
308
336
  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
- }
337
+ _toBase64Attributes(user, attributes, explicitBufferAttributes)
324
338
  }
325
339
  return users
326
340
  }
327
341
 
328
- async function authenticateWithAdmin(
329
- adminDn,
330
- adminPassword,
331
- userSearchBase,
332
- usernameFilter,
333
- usernameAttribute,
334
- username,
335
- userPassword,
336
- starttls,
337
- ldapOpts,
338
- groupsSearchBase,
339
- groupClass,
340
- groupMemberAttribute = 'member',
341
- groupMemberUserAttribute = 'dn',
342
- attributes = null,
343
- explicitBufferAttributes = null
344
- ) {
342
+ async function authenticateWithAdmin(options) {
343
+ const { username, ldapOpts } = options
345
344
  let ldapAdminClient
346
345
  try {
347
- ldapAdminClient = await _ldapBind(
348
- adminDn,
349
- adminPassword,
350
- starttls,
351
- ldapOpts
352
- )
346
+ ldapAdminClient = await _ldapBind(options.adminDn, options.adminPassword, {
347
+ starttls: options.starttls,
348
+ ldapOpts,
349
+ })
353
350
  } catch (error) {
354
- if (ldapAdminClient && ldapAdminClient.isConnected) {
355
- await ldapAdminClient.unbind()
356
- }
357
-
358
351
  return new AuthenticationResult(
359
352
  AUTH_RESULT_FAILURE,
360
353
  username,
@@ -364,96 +357,69 @@ async function authenticateWithAdmin(
364
357
  )
365
358
  }
366
359
 
367
- let searchResult = await _searchUser(
368
- ldapAdminClient,
369
- userSearchBase,
370
- usernameFilter,
371
- usernameAttribute,
372
- username,
373
- attributes,
374
- explicitBufferAttributes
375
- )
360
+ try {
361
+ let searchResult = await _searchUser(ldapAdminClient, options)
376
362
 
377
- let user = searchResult.user
363
+ let user = searchResult.user
378
364
 
379
- if (!user || !user.dn) {
380
- ldapOpts.log &&
381
- ldapOpts.log.trace(
382
- `admin did not find user! (${usernameAttribute}=${username})`
365
+ if (!user || !user.dn) {
366
+ ldapOpts.log &&
367
+ ldapOpts.log.trace(
368
+ `admin did not find user! (${options.usernameAttribute}=${username})`
369
+ )
370
+ return new AuthenticationResult(
371
+ AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND,
372
+ username,
373
+ null,
374
+ [authenticationMessages.AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND],
375
+ ldapAdminClient
383
376
  )
384
- await ldapAdminClient.unbind()
385
- return new AuthenticationResult(
386
- AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND,
387
- username,
388
- null,
389
- [authenticationMessages.AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND],
390
- ldapAdminClient
391
- )
392
- }
393
- let userDn = user.dn
394
- let ldapUserClient
395
- try {
396
- ldapUserClient = await _ldapBind(userDn, userPassword, starttls, ldapOpts)
397
- } catch (error) {
398
- if (ldapUserClient && ldapUserClient.isConnected) {
377
+ }
378
+ let userDn = user.dn
379
+ let ldapUserClient
380
+ try {
381
+ ldapUserClient = await _ldapBind(userDn, options.userPassword, {
382
+ starttls: options.starttls,
383
+ ldapOpts,
384
+ })
385
+ } catch (error) {
386
+ return new AuthenticationResult(
387
+ AUTH_RESULT_FAILURE_CREDENTIAL_INVALID,
388
+ username,
389
+ null,
390
+ [
391
+ authenticationMessages.AUTH_RESULT_FAILURE_CREDENTIAL_INVALID,
392
+ error.message || 'invalid credentials',
393
+ ],
394
+ ldapAdminClient
395
+ )
396
+ }
397
+ try {
398
+ await _attachGroups(ldapAdminClient, user, options)
399
+ return new AuthenticationResult(
400
+ AUTH_RESULT_SUCCESS,
401
+ username,
402
+ user,
403
+ [authenticationMessages.AUTH_RESULT_SUCCESS],
404
+ ldapAdminClient
405
+ )
406
+ } finally {
399
407
  await ldapUserClient.unbind()
400
408
  }
401
-
402
- return new AuthenticationResult(
403
- AUTH_RESULT_FAILURE_CREDENTIAL_INVALID,
404
- username,
405
- null,
406
- [authenticationMessages.AUTH_RESULT_FAILURE_CREDENTIAL_INVALID, error.message || 'invalid credentials'],
407
- ldapAdminClient
408
- )
409
- }
410
- if (groupsSearchBase && groupClass && groupMemberAttribute) {
411
- let groups = await _searchUserGroups(
412
- ldapAdminClient,
413
- groupsSearchBase,
414
- user,
415
- groupClass,
416
- groupMemberAttribute,
417
- groupMemberUserAttribute
418
- )
419
- user.groups = groups
409
+ } finally {
410
+ await ldapAdminClient.unbind()
420
411
  }
421
- await ldapAdminClient.unbind()
422
- await ldapUserClient.unbind()
423
-
424
- return new AuthenticationResult(
425
- AUTH_RESULT_SUCCESS,
426
- username,
427
- user,
428
- [authenticationMessages.AUTH_RESULT_SUCCESS],
429
- ldapAdminClient
430
- )
431
412
  }
432
413
 
433
- async function authenticateWithUser(
434
- userDn,
435
- userSearchBase,
436
- usernameFilter,
437
- usernameAttribute,
438
- username,
439
- userPassword,
440
- starttls,
441
- ldapOpts,
442
- groupsSearchBase,
443
- groupClass,
444
- groupMemberAttribute = 'member',
445
- groupMemberUserAttribute = 'dn',
446
- attributes = null,
447
- explicitBufferAttributes = null
448
- ) {
414
+ async function authenticateWithUser(options) {
415
+ const { username, usernameAttribute, userSearchBase, ldapOpts } = options
449
416
  let ldapUserClient
450
417
  try {
451
- ldapUserClient = await _ldapBind(userDn, userPassword, starttls, ldapOpts)
418
+ ldapUserClient = await _ldapBind(options.userDn, options.userPassword, {
419
+ starttls: options.starttls,
420
+ ldapOpts,
421
+ })
452
422
  } catch (error) {
453
- if (ldapUserClient && ldapUserClient.isConnected) {
454
- await ldapUserClient.unbind()
455
- }
456
-
457
423
  return new AuthenticationResult(
458
424
  AUTH_RESULT_FAILURE,
459
425
  username,
@@ -462,98 +428,61 @@ async function authenticateWithUser(
462
428
  ldapUserClient
463
429
  )
464
430
  }
465
- if (!usernameAttribute || !userSearchBase) {
466
- // if usernameAttribute is not provided, no user detail is needed.
467
- await ldapUserClient.unbind()
468
- return new AuthenticationResult(
469
- AUTH_RESULT_SUCCESS,
470
- username,
471
- {},
472
- [authenticationMessages.AUTH_RESULT_SUCCESS],
473
- ldapUserClient
474
- )
475
- }
431
+ try {
432
+ if (!usernameAttribute || !userSearchBase) {
433
+ // if usernameAttribute is not provided, no user detail is needed.
434
+ return new AuthenticationResult(
435
+ AUTH_RESULT_SUCCESS,
436
+ username,
437
+ {},
438
+ [authenticationMessages.AUTH_RESULT_SUCCESS],
439
+ ldapUserClient
440
+ )
441
+ }
476
442
 
477
- let searchResult = await _searchUser(
478
- ldapUserClient,
479
- userSearchBase,
480
- usernameFilter,
481
- usernameAttribute,
482
- username,
483
- attributes,
484
- explicitBufferAttributes
485
- )
443
+ let searchResult = await _searchUser(ldapUserClient, options)
486
444
 
487
- let user = searchResult.user
445
+ let user = searchResult.user
488
446
 
489
- if (!user || !user.dn) {
490
- ldapOpts.log &&
491
- ldapOpts.log.trace(
492
- `user logged in, but user details could not be found. (${usernameAttribute}=${username}). Probabaly wrong attribute or searchBase?`
447
+ if (!user || !user.dn) {
448
+ ldapOpts.log &&
449
+ ldapOpts.log.trace(
450
+ `user logged in, but user details could not be found. (${usernameAttribute}=${username}). Probabaly wrong attribute or searchBase?`
451
+ )
452
+ return new AuthenticationResult(
453
+ AUTH_RESULT_FAILURE,
454
+ username,
455
+ null,
456
+ [
457
+ authenticationMessages.AUTH_RESULT_FAILURE,
458
+ 'user logged in, but user details could not be found. Probabaly usernameAttribute or userSearchBase is wrong?',
459
+ ],
460
+ ldapUserClient
493
461
  )
494
- await ldapUserClient.unbind()
462
+ }
463
+ await _attachGroups(ldapUserClient, user, options)
495
464
 
496
465
  return new AuthenticationResult(
497
- AUTH_RESULT_FAILURE,
466
+ AUTH_RESULT_SUCCESS,
498
467
  username,
499
- null,
500
- [
501
- authenticationMessages.AUTH_RESULT_FAILURE,
502
- 'user logged in, but user details could not be found. Probabaly usernameAttribute or userSearchBase is wrong?',
503
- ],
504
- ldapUserClient
505
- )
506
- }
507
- if (groupsSearchBase && groupClass && groupMemberAttribute) {
508
- let groups = await _searchUserGroups(
509
- ldapUserClient,
510
- groupsSearchBase,
511
468
  user,
512
- groupClass,
513
- groupMemberAttribute,
514
- groupMemberUserAttribute
469
+ [authenticationMessages.AUTH_RESULT_SUCCESS],
470
+ ldapUserClient
515
471
  )
516
- user.groups = groups
472
+ } finally {
473
+ await ldapUserClient.unbind()
517
474
  }
518
- await ldapUserClient.unbind()
519
-
520
- return new AuthenticationResult(
521
- AUTH_RESULT_SUCCESS,
522
- username,
523
- user,
524
- [authenticationMessages.AUTH_RESULT_SUCCESS],
525
- ldapUserClient
526
- )
527
475
  }
528
476
 
529
- async function verifyUserExists(
530
- adminDn,
531
- adminPassword,
532
- userSearchBase,
533
- usernameFilter,
534
- usernameAttribute,
535
- username,
536
- starttls,
537
- ldapOpts,
538
- groupsSearchBase,
539
- groupClass,
540
- groupMemberAttribute = 'member',
541
- groupMemberUserAttribute = 'dn',
542
- attributes = null,
543
- explicitBufferAttributes = null
544
- ) {
477
+ async function verifyUserExists(options) {
478
+ const { username, ldapOpts } = options
545
479
  let ldapAdminClient
546
480
  try {
547
- ldapAdminClient = await _ldapBind(
548
- adminDn,
549
- adminPassword,
550
- starttls,
551
- ldapOpts
552
- )
481
+ ldapAdminClient = await _ldapBind(options.adminDn, options.adminPassword, {
482
+ starttls: options.starttls,
483
+ ldapOpts,
484
+ })
553
485
  } catch (error) {
554
- if (ldapAdminClient && ldapAdminClient.isConnected) {
555
- await ldapAdminClient.unbind()
556
- }
557
486
  return new AuthenticationResult(
558
487
  AUTH_RESULT_FAILURE,
559
488
  username,
@@ -563,209 +492,182 @@ async function verifyUserExists(
563
492
  )
564
493
  }
565
494
 
566
- let searchResult = await _searchUser(
567
- ldapAdminClient,
568
- userSearchBase,
569
- usernameFilter,
570
- usernameAttribute,
571
- username,
572
- attributes,
573
- explicitBufferAttributes
574
- )
495
+ try {
496
+ let searchResult = await _searchUser(ldapAdminClient, options)
575
497
 
576
- let user = searchResult.user
498
+ let user = searchResult.user
577
499
 
578
- if (!user || !user.dn) {
579
- ldapOpts.log &&
580
- ldapOpts.log.trace(
581
- `admin did not find user! (${usernameAttribute}=${username})`
500
+ if (!user || !user.dn) {
501
+ ldapOpts.log &&
502
+ ldapOpts.log.trace(
503
+ `admin did not find user! (${options.usernameAttribute}=${username})`
504
+ )
505
+ return new AuthenticationResult(
506
+ AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND,
507
+ username,
508
+ null,
509
+ [
510
+ authenticationMessages.AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND,
511
+ 'user not found or usernameAttribute is wrong',
512
+ ],
513
+ ldapAdminClient
582
514
  )
583
- await ldapAdminClient.unbind()
515
+ }
516
+ await _attachGroups(ldapAdminClient, user, options)
584
517
  return new AuthenticationResult(
585
- AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND,
518
+ AUTH_RESULT_SUCCESS,
586
519
  username,
587
- null,
588
- [
589
- authenticationMessages.AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND,
590
- 'user not found or usernameAttribute is wrong'
591
- ],
520
+ user,
521
+ [authenticationMessages.AUTH_RESULT_SUCCESS],
592
522
  ldapAdminClient
593
523
  )
524
+ } finally {
525
+ await ldapAdminClient.unbind()
594
526
  }
595
- if (groupsSearchBase && groupClass && groupMemberAttribute) {
596
- let groups = await _searchUserGroups(
597
- ldapAdminClient,
598
- groupsSearchBase,
599
- user,
600
- groupClass,
601
- groupMemberAttribute,
602
- groupMemberUserAttribute
527
+ }
528
+
529
+ // validate the options of authenticate()/authenticateResult() and throw a
530
+ // single LdapAuthenticationError listing all missing fields at once
531
+ function _validateOptions(options) {
532
+ if (!options) {
533
+ throw new LdapAuthenticationError('authenticate: options object is required')
534
+ }
535
+
536
+ let missing = []
537
+ if (!options.ldapOpts || !options.ldapOpts.url) {
538
+ missing.push('ldapOpts.url')
539
+ }
540
+
541
+ if (options.verifyUserExists) {
542
+ if (!options.adminDn) missing.push('adminDn')
543
+ if (!options.adminPassword) missing.push('adminPassword')
544
+ if (!options.userSearchBase) missing.push('userSearchBase')
545
+ if (!options.usernameAttribute && !options.usernameFilter) {
546
+ missing.push('usernameAttribute or usernameFilter')
547
+ }
548
+ if (!options.username) missing.push('username')
549
+ } else if (options.adminDn) {
550
+ if (!options.adminPassword) missing.push('adminPassword')
551
+ if (!options.userSearchBase) missing.push('userSearchBase')
552
+ if (!options.usernameAttribute && !options.usernameFilter) {
553
+ missing.push('usernameAttribute or usernameFilter')
554
+ }
555
+ if (!options.username) missing.push('username')
556
+ if (!options.userPassword) missing.push('userPassword')
557
+ } else if (options.userDn) {
558
+ if (!options.userPassword) missing.push('userPassword')
559
+ } else {
560
+ missing.push('adminDn or userDn')
561
+ }
562
+
563
+ if (missing.length > 0) {
564
+ throw new LdapAuthenticationError(
565
+ `authenticate: missing required option(s): ${missing.join(', ')}`
603
566
  )
604
- user.groups = groups
605
567
  }
606
- await ldapAdminClient.unbind()
607
- return new AuthenticationResult(
608
- AUTH_RESULT_SUCCESS,
609
- username,
610
- user,
611
- [authenticationMessages.AUTH_RESULT_SUCCESS],
612
- ldapAdminClient
613
- )
614
568
  }
615
569
 
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.
570
+ /**
571
+ * Fetch all users under `userSearchBase`, using the admin account to search.
572
+ * No individual username or password is required. The search always uses
573
+ * LDAP paged results, so the common server-side limit of 1000 entries does
574
+ * not apply. Returns an empty array if no user matches.
575
+ *
576
+ * @param {FetchUsersOptions} options - required: `ldapOpts` (with `url`),
577
+ * `adminDn`, `adminPassword`, `userSearchBase`; optional: `userFilter`,
578
+ * `attributes`, `explicitBufferAttributes`, `pageSize`, `starttls`.
579
+ * See the types in index.d.ts and the README for details.
580
+ * @returns {Promise<LdapUserEntry[]>} one entry per matched user, each with
581
+ * its `dn` and the returned attributes.
582
+ * @throws {LdapAuthenticationError} if required options are missing, or if
583
+ * the admin bind or the search fails.
584
+ */
619
585
  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')
586
+ let missing = []
587
+ if (!options.ldapOpts || !options.ldapOpts.url) missing.push('ldapOpts.url')
588
+ if (!options.adminDn) missing.push('adminDn')
589
+ if (!options.adminPassword) missing.push('adminPassword')
590
+ if (!options.userSearchBase) missing.push('userSearchBase')
591
+ if (missing.length > 0) {
592
+ throw new LdapAuthenticationError(
593
+ `fetchUsers: missing required option(s): ${missing.join(', ')}`
594
+ )
595
+ }
627
596
 
628
597
  let ldapAdminClient
629
598
  try {
630
- ldapAdminClient = await _ldapBind(
631
- options.adminDn,
632
- options.adminPassword,
633
- options.starttls,
634
- options.ldapOpts
635
- )
599
+ ldapAdminClient = await _ldapBind(options.adminDn, options.adminPassword, {
600
+ starttls: options.starttls,
601
+ ldapOpts: options.ldapOpts,
602
+ })
636
603
  } catch (error) {
637
- if (ldapAdminClient && ldapAdminClient.isConnected) {
638
- await ldapAdminClient.unbind()
639
- }
640
604
  throw new LdapAuthenticationError(error.message || 'admin bind failed')
641
605
  }
642
606
 
643
607
  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
- )
608
+ return await _fetchAllUsers(ldapAdminClient, options)
652
609
  } catch (error) {
653
610
  throw new LdapAuthenticationError(error.message || 'user search failed')
654
611
  } finally {
655
- if (ldapAdminClient && ldapAdminClient.isConnected) {
612
+ if (ldapAdminClient) {
656
613
  await ldapAdminClient.unbind()
657
614
  }
658
615
  }
659
616
  }
660
617
 
618
+ /**
619
+ * Authenticate a user against the LDAP server.
620
+ *
621
+ * Modes (see the README for a full option reference):
622
+ * - Admin mode: `adminDn` + `adminPassword` + `userSearchBase` +
623
+ * `usernameAttribute` (or `usernameFilter`) + `username` + `userPassword`.
624
+ * The library binds as admin, finds the user's DN, then binds as the user.
625
+ * - Self mode: `userDn` + `userPassword`. Optionally `userSearchBase` and
626
+ * `usernameAttribute` to also return the user's details.
627
+ * - Verify mode: `verifyUserExists: true` with admin credentials; verifies
628
+ * that the user exists without checking the password.
629
+ *
630
+ * @param {AuthenticationOptions} options
631
+ * @returns {Promise<any>} the user object if authentication succeeded.
632
+ * @throws {LdapAuthenticationError} if authentication failed (its `code`
633
+ * property then holds the corresponding AUTH_RESULT_* constant) or if
634
+ * required options are missing.
635
+ */
661
636
  async function authenticate(options) {
662
637
  const result = await authenticateResult(options)
663
638
 
664
639
  if (result.code !== AUTH_RESULT_SUCCESS) {
665
640
  throw new LdapAuthenticationError(
666
- result.messages[result.messages.length - 1]
641
+ result.messages[result.messages.length - 1],
642
+ result.code
667
643
  )
668
644
  }
669
645
 
670
646
  return result.user
671
647
  }
672
648
 
649
+ /**
650
+ * Same options and behavior as {@link authenticate}, but never throws on
651
+ * authentication failure - it returns an {@link AuthenticationResult} whose
652
+ * `code` identifies the outcome (useful for custom error handling).
653
+ *
654
+ * @param {AuthenticationOptions} options
655
+ * @returns {Promise<AuthenticationResult>}
656
+ * @throws {LdapAuthenticationError} if required options are missing;
657
+ * network errors from the LDAP server propagate as-is.
658
+ */
673
659
  async function authenticateResult(options) {
674
- if (!options.userDn) {
675
- assert(options.adminDn, 'Admin mode adminDn must be provided')
676
- assert(options.adminPassword, 'Admin mode adminPassword must be provided')
677
- assert(options.userSearchBase, 'Admin mode userSearchBase must be provided')
678
- assert(
679
- options.usernameAttribute || options.usernameFilter,
680
- 'Admin mode usernameAttribute or usernameFilter must be provided'
681
- )
682
- assert(options.username, 'Admin mode username must be provided')
683
- } else {
684
- assert(options.userDn, 'User mode userDn must be provided')
685
- }
686
- assert(
687
- options.ldapOpts && options.ldapOpts.url,
688
- 'ldapOpts.url must be provided'
689
- )
660
+ _validateOptions(options)
690
661
 
691
662
  if (options.verifyUserExists) {
692
- assert(options.adminDn, 'Admin mode adminDn must be provided')
693
- assert(
694
- options.adminPassword,
695
- 'adminDn and adminPassword must be both provided.'
696
- )
697
- return await verifyUserExists(
698
- options.adminDn,
699
- options.adminPassword,
700
- options.userSearchBase,
701
- options.usernameFilter,
702
- options.usernameAttribute,
703
- options.username,
704
- options.starttls,
705
- options.ldapOpts,
706
- options.groupsSearchBase,
707
- options.groupClass,
708
- options.groupMemberAttribute,
709
- options.groupMemberUserAttribute,
710
- options.attributes,
711
- options.explicitBufferAttributes
712
- )
663
+ return await verifyUserExists(options)
713
664
  }
714
665
 
715
- assert(options.userPassword, 'userPassword must be provided')
716
666
  if (options.adminDn) {
717
- assert(
718
- options.adminPassword,
719
- 'adminDn and adminPassword must be both provided.'
720
- )
721
- return await authenticateWithAdmin(
722
- options.adminDn,
723
- options.adminPassword,
724
- options.userSearchBase,
725
- options.usernameFilter,
726
- options.usernameAttribute,
727
- options.username,
728
- options.userPassword,
729
- options.starttls,
730
- options.ldapOpts,
731
- options.groupsSearchBase,
732
- options.groupClass,
733
- options.groupMemberAttribute,
734
- options.groupMemberUserAttribute,
735
- options.attributes,
736
- options.explicitBufferAttributes
737
- )
667
+ return await authenticateWithAdmin(options)
738
668
  }
739
669
 
740
- assert(options.userDn, 'adminDn/adminPassword OR userDn must be provided')
741
- return await authenticateWithUser(
742
- options.userDn,
743
- options.userSearchBase,
744
- options.usernameFilter,
745
- options.usernameAttribute,
746
- options.username,
747
- options.userPassword,
748
- options.starttls,
749
- options.ldapOpts,
750
- options.groupsSearchBase,
751
- options.groupClass,
752
- options.groupMemberAttribute,
753
- options.groupMemberUserAttribute,
754
- options.attributes,
755
- options.explicitBufferAttributes
756
- )
757
- }
758
-
759
- class LdapAuthenticationError extends Error {
760
- constructor(message) {
761
- super(message)
762
- // Ensure the name of this error is the same as the class name
763
- this.name = this.constructor.name
764
- // This clips the constructor invocation from the stack trace.
765
- // It's not absolutely essential, but it does make the stack trace a little nicer.
766
- // @see Node.js reference (bottom)
767
- Error.captureStackTrace(this, this.constructor)
768
- }
670
+ return await authenticateWithUser(options)
769
671
  }
770
672
 
771
673
  module.exports.AUTH_RESULT_FAILURE = AUTH_RESULT_FAILURE