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