ldap-authentication 3.3.6 → 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.
@@ -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
@@ -221,6 +221,28 @@ auth()
221
221
 
222
222
  The user object if `authenticate()` is success.
223
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
+
224
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).
225
247
 
226
248
  Buffer data can now be accessed by `user.raw.profilePhoto`, etc, instead of `user.profilePhoto`.
@@ -260,4 +282,6 @@ export async function verifyLogin(email: string, password: string) {
260
282
 
261
283
  Version 2 supports Node version 12, 14, 15, 16, 17 and 18.
262
284
 
263
- 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.
package/index.js CHANGED
@@ -38,12 +38,64 @@ 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
-
98
+
47
99
  // When using StartTLS, we need to exclude tlsOptions from the Client constructor
48
100
  // and only pass them to the startTLS() method to avoid connection conflicts.
49
101
  // According to ldapts documentation:
@@ -52,7 +104,7 @@ async function _ldapBind(dn, password, starttls, ldapOpts) {
52
104
  // - For plain LDAP (ldap://): do NOT pass tlsOptions to Client constructor
53
105
  let clientOpts = ldapOpts
54
106
  const isLdaps = ldapOpts.url && ldapOpts.url.startsWith('ldaps://')
55
-
107
+
56
108
  // Only pass tlsOptions to Client constructor if using ldaps:// protocol
57
109
  // For ldap:// protocol (plain or StartTLS), exclude tlsOptions from constructor
58
110
  if (!isLdaps && ldapOpts.tlsOptions) {
@@ -60,7 +112,7 @@ async function _ldapBind(dn, password, starttls, ldapOpts) {
60
112
  const { tlsOptions, ...optsWithoutTls } = ldapOpts
61
113
  clientOpts = optsWithoutTls
62
114
  }
63
-
115
+
64
116
  let client = new ldapts.Client(clientOpts)
65
117
 
66
118
  if (starttls) {
@@ -115,8 +167,24 @@ async function _searchUser(
115
167
  !searchEntries[0] ||
116
168
  !searchEntries[0].dn
117
169
  ) {
118
- 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
+ )
119
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
+
120
188
  user = searchEntries[0]
121
189
  }
122
190
 
@@ -136,7 +204,14 @@ async function _searchUser(
136
204
  }
137
205
  }
138
206
  }
139
- return user
207
+
208
+ return new AuthenticationResult(
209
+ AUTH_RESULT_SUCCESS,
210
+ username,
211
+ user,
212
+ [authenticationMessages.AUTH_RESULT_SUCCESS],
213
+ ldapClient
214
+ )
140
215
  }
141
216
 
142
217
  // search a groups which user is member
@@ -215,9 +290,17 @@ async function authenticateWithAdmin(
215
290
  if (ldapAdminClient && ldapAdminClient.isConnected) {
216
291
  await ldapAdminClient.unbind()
217
292
  }
218
- 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
+ )
219
301
  }
220
- let user = await _searchUser(
302
+
303
+ let searchResult = await _searchUser(
221
304
  ldapAdminClient,
222
305
  userSearchBase,
223
306
  usernameAttribute,
@@ -225,14 +308,21 @@ async function authenticateWithAdmin(
225
308
  attributes,
226
309
  explicitBufferAttributes
227
310
  )
311
+
312
+ let user = searchResult.user
313
+
228
314
  if (!user || !user.dn) {
229
315
  ldapOpts.log &&
230
316
  ldapOpts.log.trace(
231
317
  `admin did not find user! (${usernameAttribute}=${username})`
232
318
  )
233
319
  await ldapAdminClient.unbind()
234
- throw new LdapAuthenticationError(
235
- '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
236
326
  )
237
327
  }
238
328
  let userDn = user.dn
@@ -243,7 +333,14 @@ async function authenticateWithAdmin(
243
333
  if (ldapUserClient && ldapUserClient.isConnected) {
244
334
  await ldapUserClient.unbind()
245
335
  }
246
- 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
+ )
247
344
  }
248
345
  if (groupsSearchBase && groupClass && groupMemberAttribute) {
249
346
  let groups = await _searchUserGroups(
@@ -258,7 +355,14 @@ async function authenticateWithAdmin(
258
355
  }
259
356
  await ldapAdminClient.unbind()
260
357
  await ldapUserClient.unbind()
261
- return user
358
+
359
+ return new AuthenticationResult(
360
+ AUTH_RESULT_SUCCESS,
361
+ username,
362
+ user,
363
+ [authenticationMessages.AUTH_RESULT_SUCCESS],
364
+ ldapAdminClient
365
+ )
262
366
  }
263
367
 
264
368
  async function authenticateWithUser(
@@ -283,14 +387,28 @@ async function authenticateWithUser(
283
387
  if (ldapUserClient && ldapUserClient.isConnected) {
284
388
  await ldapUserClient.unbind()
285
389
  }
286
- 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
+ )
287
398
  }
288
399
  if (!usernameAttribute || !userSearchBase) {
289
400
  // if usernameAttribute is not provided, no user detail is needed.
290
401
  await ldapUserClient.unbind()
291
- return true
402
+ return new AuthenticationResult(
403
+ AUTH_RESULT_SUCCESS,
404
+ username,
405
+ {},
406
+ [authenticationMessages.AUTH_RESULT_SUCCESS],
407
+ ldapUserClient
408
+ )
292
409
  }
293
- let user = await _searchUser(
410
+
411
+ let searchResult = await _searchUser(
294
412
  ldapUserClient,
295
413
  userSearchBase,
296
414
  usernameAttribute,
@@ -298,14 +416,25 @@ async function authenticateWithUser(
298
416
  attributes,
299
417
  explicitBufferAttributes
300
418
  )
419
+
420
+ let user = searchResult.user
421
+
301
422
  if (!user || !user.dn) {
302
423
  ldapOpts.log &&
303
424
  ldapOpts.log.trace(
304
425
  `user logged in, but user details could not be found. (${usernameAttribute}=${username}). Probabaly wrong attribute or searchBase?`
305
426
  )
306
427
  await ldapUserClient.unbind()
307
- throw new LdapAuthenticationError(
308
- '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
309
438
  )
310
439
  }
311
440
  if (groupsSearchBase && groupClass && groupMemberAttribute) {
@@ -320,7 +449,14 @@ async function authenticateWithUser(
320
449
  user.groups = groups
321
450
  }
322
451
  await ldapUserClient.unbind()
323
- return user
452
+
453
+ return new AuthenticationResult(
454
+ AUTH_RESULT_SUCCESS,
455
+ username,
456
+ user,
457
+ [authenticationMessages.AUTH_RESULT_SUCCESS],
458
+ ldapUserClient
459
+ )
324
460
  }
325
461
 
326
462
  async function verifyUserExists(
@@ -350,9 +486,16 @@ async function verifyUserExists(
350
486
  if (ldapAdminClient && ldapAdminClient.isConnected) {
351
487
  await ldapAdminClient.unbind()
352
488
  }
353
- 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
+ )
354
496
  }
355
- let user = await _searchUser(
497
+
498
+ let searchResult = await _searchUser(
356
499
  ldapAdminClient,
357
500
  userSearchBase,
358
501
  usernameAttribute,
@@ -360,14 +503,24 @@ async function verifyUserExists(
360
503
  attributes,
361
504
  explicitBufferAttributes
362
505
  )
506
+
507
+ let user = searchResult.user
508
+
363
509
  if (!user || !user.dn) {
364
510
  ldapOpts.log &&
365
511
  ldapOpts.log.trace(
366
512
  `admin did not find user! (${usernameAttribute}=${username})`
367
513
  )
368
514
  await ldapAdminClient.unbind()
369
- throw new LdapAuthenticationError(
370
- '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
371
524
  )
372
525
  }
373
526
  if (groupsSearchBase && groupClass && groupMemberAttribute) {
@@ -382,10 +535,28 @@ async function verifyUserExists(
382
535
  user.groups = groups
383
536
  }
384
537
  await ldapAdminClient.unbind()
385
- return user
538
+ return new AuthenticationResult(
539
+ AUTH_RESULT_SUCCESS,
540
+ username,
541
+ user,
542
+ [authenticationMessages.AUTH_RESULT_SUCCESS],
543
+ ldapAdminClient
544
+ )
386
545
  }
387
546
 
388
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) {
389
560
  if (!options.userDn) {
390
561
  assert(options.adminDn, 'Admin mode adminDn must be provided')
391
562
  assert(options.adminPassword, 'Admin mode adminPassword must be provided')
@@ -402,6 +573,7 @@ async function authenticate(options) {
402
573
  options.ldapOpts && options.ldapOpts.url,
403
574
  'ldapOpts.url must be provided'
404
575
  )
576
+
405
577
  if (options.verifyUserExists) {
406
578
  assert(options.adminDn, 'Admin mode adminDn must be provided')
407
579
  assert(
@@ -424,6 +596,7 @@ async function authenticate(options) {
424
596
  options.explicitBufferAttributes
425
597
  )
426
598
  }
599
+
427
600
  assert(options.userPassword, 'userPassword must be provided')
428
601
  if (options.adminDn) {
429
602
  assert(
@@ -447,6 +620,7 @@ async function authenticate(options) {
447
620
  options.explicitBufferAttributes
448
621
  )
449
622
  }
623
+
450
624
  assert(options.userDn, 'adminDn/adminPassword OR userDn must be provided')
451
625
  return await authenticateWithUser(
452
626
  options.userDn,
@@ -477,7 +651,21 @@ class LdapAuthenticationError extends Error {
477
651
  }
478
652
  }
479
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
+
480
667
  module.exports.authenticate = authenticate
668
+ module.exports.authenticateResult = authenticateResult
481
669
  module.exports.LdapAuthenticationError = LdapAuthenticationError
482
670
 
483
671
  module.exports.exportForTesting = {
package/package.json CHANGED
@@ -1,15 +1,18 @@
1
1
  {
2
2
  "name": "ldap-authentication",
3
- "version": "3.3.6",
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
+ })