ldap-authentication 2.2.8 → 2.3.1

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  BSD 2-Clause License
2
2
 
3
- Copyright (c) 2021, shaozi
3
+ Copyright (c) 2022, shaozi
4
4
  All rights reserved.
5
5
 
6
6
  Redistribution and use in source and binary forms, with or without
package/README.md CHANGED
@@ -9,7 +9,7 @@ Make authentication with an LDAP server easy.
9
9
 
10
10
  ## Description
11
11
 
12
- This library use `ldapjs` as the underneath library. It has two modes of authentications:
12
+ This library use `ldapjs` as the underneath library. It has three modes of authentications:
13
13
 
14
14
  1. **Admin authenticate mode**. If an admin user is provided, the library will login (ldap bind) with the admin user,
15
15
  then search for the user to be authenticated, get its DN (distinguish name), then use
@@ -23,6 +23,9 @@ This library use `ldapjs` as the underneath library. It has two modes of authent
23
23
  Otherwise, the lib does a login with the `userDn` and `userPassword` (ldap bind),
24
24
  then does a search on the user and return the user's details.
25
25
 
26
+ 3. **Verify user exists**. If an `verifyUserExists : true` is provided, the library will login (ldap bind) with the admin user,
27
+ then search for the user to be verified. If the user exists, user details will be returned (without verifying the user's password).
28
+
26
29
  ## Features
27
30
 
28
31
  - Can use an admin to search and authenticate a user
@@ -64,6 +67,20 @@ let authenticated = await authenticate({
64
67
  userSearchBase: 'dc=example,dc=com',
65
68
  usernameAttribute: 'uid',
66
69
  username: 'gauss',
70
+ attributes: ['dn', 'sn', 'cn'],
71
+ })
72
+ ```
73
+
74
+ #### User exists verification and return user details (without user's password)
75
+
76
+ ```javascript
77
+ let authenticated = await authenticate({
78
+ ldapOpts: { url: 'ldap://ldap.forumsys.com' },
79
+ userDn: 'uid=gauss,dc=example,dc=com',
80
+ verifyUserExists: true,
81
+ userSearchBase: 'dc=example,dc=com',
82
+ usernameAttribute: 'uid',
83
+ username: 'gauss',
67
84
  })
68
85
  ```
69
86
 
@@ -139,7 +156,8 @@ auth()
139
156
  - `adminPassword`: The password of the admin.
140
157
  - `userDn`: The DN of the user to be authenticated. This is only needed if `adminDn` and `adminPassword` are not provided.
141
158
  Example: `uid=gauss,dc=example,dc=com`
142
- - `userPassword`: The password of the user,
159
+ - `userPassword`: The password of the user
160
+ - `verifyUserExists` : if `true` user existence will be verified without password
143
161
  - `userSearchBase`: The ldap base DN to search the user. Example: `dc=example,dc=com`
144
162
  - `usernameAttribute`: The ldap search equality attribute name corresponding to the user's username.
145
163
  It will be used with the value in `username` to construct an ldap filter as `({attribute}={username})`
@@ -151,6 +169,8 @@ auth()
151
169
  - `username`: The username to authenticate with. It is used together with the name in `usernameAttribute`
152
170
  to construct an ldap filter as `({attribute}={username})`
153
171
  to find the user and get user details in LDAP. Example: `some user input`
172
+ - `attributes`: A list of attributes of a user details to be returned from the LDAP server.
173
+ If is set to `[]` or ommited, all details will be returned. Example: `['sn', 'cn']`
154
174
  - `starttls`: Boolean. Use `STARTTLS` or not
155
175
  - `groupsSearchBase`: if specified with groupClass, will serve as search base for authenticated user groups
156
176
  - `groupClass`: if specified with groupsSearchBase, will be used as objectClass in search filter for authenticated user groups
package/index.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ declare module 'ldap-authentication' {
2
+ type LDAPOpts = {
3
+ url: string
4
+ tlsOptions?: {
5
+ rejectUnauthorized: boolean
6
+ }
7
+ connectTimeout?: number
8
+ }
9
+ export type AuthenticationOptions = {
10
+ ldapOpts: LDAPOpts
11
+ userDn?: string
12
+ adminDn?: string
13
+ adminPassword?: string
14
+ userSearchBase?: string
15
+ usernameAttribute?: string
16
+ username?: string
17
+ verifyUserExists?: boolean
18
+ starttls?: Boolean
19
+ groupsSearchBase?: string
20
+ groupClass?: string
21
+ groupMemberAttribute?: string
22
+ groupMemberUserAttribute?: string
23
+ userPassword?: string
24
+ attributes?: string[]
25
+ }
26
+
27
+ export function authenticate(options: AuthenticationOptions): Promise<any>
28
+
29
+ export class LdapAuthenticationError extends Error {
30
+ constructor(message: any)
31
+ name: string
32
+ }
33
+ }
package/index.js CHANGED
@@ -35,27 +35,26 @@ function _ldapBind(dn, password, starttls, ldapOpts) {
35
35
  resolve(client)
36
36
  })
37
37
  }
38
- });
38
+ })
39
39
 
40
40
  //Fix for issue https://github.com/shaozi/ldap-authentication/issues/13
41
41
  client.on('timeout', (err) => {
42
- reject(err);
43
- });
42
+ reject(err)
43
+ })
44
44
  client.on('connectTimeout', (err) => {
45
- reject(err);
46
- });
45
+ reject(err)
46
+ })
47
47
  client.on('error', (err) => {
48
- reject(err);
49
- });
48
+ reject(err)
49
+ })
50
50
 
51
- client.on('connectError', function(error){
51
+ client.on('connectError', function (error) {
52
52
  if (error) {
53
53
  reject(error)
54
54
  return
55
55
  }
56
- });
57
-
58
- });
56
+ })
57
+ })
59
58
  }
60
59
 
61
60
  // search a user and return the object
@@ -63,46 +62,53 @@ async function _searchUser(
63
62
  ldapClient,
64
63
  searchBase,
65
64
  usernameAttribute,
66
- username
65
+ username,
66
+ attributes = null
67
67
  ) {
68
68
  return new Promise(function (resolve, reject) {
69
69
  var filter = new ldap.filters.EqualityFilter({
70
70
  attribute: usernameAttribute,
71
71
  value: username,
72
72
  })
73
- ldapClient.search(
74
- searchBase,
75
- {
76
- filter: filter,
77
- scope: 'sub',
78
- },
79
- function (err, res) {
80
- var user = null
81
- if (err) {
82
- reject(err)
83
- ldapClient.unbind()
84
- return
85
- }
86
- res.on('searchEntry', function (entry) {
87
- user = entry.object
88
- })
89
- res.on('searchReference', function (referral) {
90
- console.log('referral: ' + referral.uris.join())
91
- })
92
- res.on('error', function (err) {
93
- reject(err)
94
- ldapClient.unbind()
95
- })
96
- res.on('end', function (result) {
97
- if (result.status != 0) {
98
- reject(new Error('ldap search status is not 0, search failed'))
99
- } else {
100
- resolve(user)
101
- }
102
- ldapClient.unbind()
103
- })
73
+ let searchOptions = {
74
+ filter: filter,
75
+ scope: 'sub',
76
+ attributes: attributes,
77
+ }
78
+ if (attributes) {
79
+ searchOptions.attributes = attributes
80
+ }
81
+ ldapClient.search(searchBase, searchOptions, function (err, res) {
82
+ var user = null
83
+ if (err) {
84
+ reject(err)
85
+ ldapClient.unbind()
86
+ return
104
87
  }
105
- )
88
+ res.on('searchEntry', function (entry) {
89
+ user = entry.object
90
+ })
91
+ res.on('searchReference', function (referral) {
92
+ // TODO: we don't support reference yet
93
+ // If the server was able to locate the entry referred to by the baseObject
94
+ // but could not search one or more non-local entries,
95
+ // the server may return one or more SearchResultReference messages,
96
+ // each containing a reference to another set of servers for continuing the operation.
97
+ // referral.uris
98
+ })
99
+ res.on('error', function (err) {
100
+ reject(err)
101
+ ldapClient.unbind()
102
+ })
103
+ res.on('end', function (result) {
104
+ if (result.status != 0) {
105
+ reject(new Error('ldap search status is not 0, search failed'))
106
+ } else {
107
+ resolve(user)
108
+ }
109
+ ldapClient.unbind()
110
+ })
111
+ })
106
112
  })
107
113
  }
108
114
 
@@ -132,9 +138,7 @@ async function _searchUserGroups(
132
138
  res.on('searchEntry', function (entry) {
133
139
  groups.push(entry.object)
134
140
  })
135
- res.on('searchReference', function (referral) {
136
- console.log('referral: ' + referral.uris.join())
137
- })
141
+ res.on('searchReference', function (referral) {})
138
142
  res.on('error', function (err) {
139
143
  reject(err)
140
144
  ldapClient.unbind()
@@ -164,7 +168,8 @@ async function authenticateWithAdmin(
164
168
  groupsSearchBase,
165
169
  groupClass,
166
170
  groupMemberAttribute = 'member',
167
- groupMemberUserAttribute
171
+ groupMemberUserAttribute = 'dn',
172
+ attributes = null
168
173
  ) {
169
174
  var ldapAdminClient
170
175
  try {
@@ -181,7 +186,8 @@ async function authenticateWithAdmin(
181
186
  ldapAdminClient,
182
187
  userSearchBase,
183
188
  usernameAttribute,
184
- username
189
+ username,
190
+ attributes
185
191
  )
186
192
  ldapAdminClient.unbind()
187
193
  if (!user || !user.dn) {
@@ -237,7 +243,8 @@ async function authenticateWithUser(
237
243
  groupsSearchBase,
238
244
  groupClass,
239
245
  groupMemberAttribute = 'member',
240
- groupMemberUserAttribute
246
+ groupMemberUserAttribute = 'dn',
247
+ attributes = null
241
248
  ) {
242
249
  let ldapUserClient
243
250
  try {
@@ -254,7 +261,8 @@ async function authenticateWithUser(
254
261
  ldapUserClient,
255
262
  userSearchBase,
256
263
  usernameAttribute,
257
- username
264
+ username,
265
+ attributes
258
266
  )
259
267
  if (!user || !user.dn) {
260
268
  ldapOpts.log &&
@@ -286,6 +294,73 @@ async function authenticateWithUser(
286
294
  return user
287
295
  }
288
296
 
297
+ async function verifyUserExists(
298
+ adminDn,
299
+ adminPassword,
300
+ userSearchBase,
301
+ usernameAttribute,
302
+ username,
303
+ starttls,
304
+ ldapOpts,
305
+ groupsSearchBase,
306
+ groupClass,
307
+ groupMemberAttribute = 'member',
308
+ groupMemberUserAttribute = 'dn',
309
+ attributes = null
310
+ ) {
311
+ var ldapAdminClient
312
+ try {
313
+ ldapAdminClient = await _ldapBind(
314
+ adminDn,
315
+ adminPassword,
316
+ starttls,
317
+ ldapOpts
318
+ )
319
+ } catch (error) {
320
+ throw { admin: error }
321
+ }
322
+ var user = await _searchUser(
323
+ ldapAdminClient,
324
+ userSearchBase,
325
+ usernameAttribute,
326
+ username,
327
+ attributes
328
+ )
329
+ ldapAdminClient.unbind()
330
+ if (!user || !user.dn) {
331
+ ldapOpts.log &&
332
+ ldapOpts.log.trace(
333
+ `admin did not find user! (${usernameAttribute}=${username})`
334
+ )
335
+ throw new LdapAuthenticationError(
336
+ 'user not found or usernameAttribute is wrong'
337
+ )
338
+ }
339
+ if (groupsSearchBase && groupClass && groupMemberAttribute) {
340
+ try {
341
+ ldapAdminClient = await _ldapBind(
342
+ adminDn,
343
+ adminPassword,
344
+ starttls,
345
+ ldapOpts
346
+ )
347
+ } catch (error) {
348
+ throw error
349
+ }
350
+ var groups = await _searchUserGroups(
351
+ ldapAdminClient,
352
+ groupsSearchBase,
353
+ user,
354
+ groupClass,
355
+ groupMemberAttribute,
356
+ groupMemberUserAttribute
357
+ )
358
+ user.groups = groups
359
+ ldapAdminClient.unbind()
360
+ }
361
+ return user
362
+ }
363
+
289
364
  async function authenticate(options) {
290
365
  if (!options.userDn) {
291
366
  assert(options.adminDn, 'Admin mode adminDn must be provided')
@@ -299,11 +374,31 @@ async function authenticate(options) {
299
374
  } else {
300
375
  assert(options.userDn, 'User mode userDn must be provided')
301
376
  }
302
- assert(options.userPassword, 'userPassword must be provided')
303
377
  assert(
304
378
  options.ldapOpts && options.ldapOpts.url,
305
379
  'ldapOpts.url must be provided'
306
380
  )
381
+ if (options.verifyUserExists) {
382
+ assert(options.adminDn, 'Admin mode adminDn must be provided')
383
+ assert(
384
+ options.adminPassword,
385
+ 'adminDn and adminPassword must be both provided.'
386
+ )
387
+ return await verifyUserExists(
388
+ options.adminDn,
389
+ options.adminPassword,
390
+ options.userSearchBase,
391
+ options.usernameAttribute,
392
+ options.username,
393
+ options.starttls,
394
+ options.ldapOpts,
395
+ options.groupsSearchBase,
396
+ options.groupClass,
397
+ options.groupMemberAttribute,
398
+ options.groupMemberUserAttribute
399
+ )
400
+ }
401
+ assert(options.userPassword, 'userPassword must be provided')
307
402
  if (options.adminDn) {
308
403
  assert(
309
404
  options.adminPassword,
@@ -321,7 +416,8 @@ async function authenticate(options) {
321
416
  options.groupsSearchBase,
322
417
  options.groupClass,
323
418
  options.groupMemberAttribute,
324
- options.groupMemberUserAttribute
419
+ options.groupMemberUserAttribute,
420
+ options.attributes
325
421
  )
326
422
  }
327
423
  assert(options.userDn, 'adminDn/adminPassword OR userDn must be provided')
@@ -336,7 +432,8 @@ async function authenticate(options) {
336
432
  options.groupsSearchBase,
337
433
  options.groupClass,
338
434
  options.groupMemberAttribute,
339
- options.groupMemberUserAttribute
435
+ options.groupMemberUserAttribute,
436
+ options.attributes
340
437
  )
341
438
  }
342
439
 
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "ldap-authentication",
3
- "version": "2.2.8",
3
+ "version": "2.3.1",
4
4
  "description": "A simple async nodejs library for LDAP user authentication",
5
5
  "main": "index.js",
6
+ "types": "./index.d.ts",
6
7
  "scripts": {
7
8
  "test": "node test/jasmine.js"
8
9
  },
@@ -35,10 +36,9 @@
35
36
  },
36
37
  "homepage": "https://github.com/shaozi/ldap-authentication#readme",
37
38
  "dependencies": {
38
- "ldapjs": "^2.3.1"
39
+ "ldapjs": "^2.3.3"
39
40
  },
40
41
  "devDependencies": {
41
- "jasmine": "^3.8.0",
42
- "jasmine-console-reporter": "^3.1.0"
42
+ "jasmine": "^4.3.0"
43
43
  }
44
44
  }
package/test/jasmine.js CHANGED
@@ -9,7 +9,55 @@ jasmine.loadConfig({
9
9
  stopSpecOnExpectationFailure: false,
10
10
  })
11
11
 
12
- const Reporter = require('jasmine-console-reporter')
12
+ class MyConsoleReporter {
13
+ constructor() {
14
+ this.suiteSeq = 1
15
+ }
13
16
 
14
- jasmine.jasmine.getEnv().addReporter(new Reporter())
17
+ _indentPrint(str) {
18
+ str
19
+ .trim()
20
+ .split('\n')
21
+ .forEach((line) => {
22
+ process.stdout.write(' ' + line + '\n')
23
+ })
24
+ }
25
+
26
+ jasmineStarted(suiteInfo) {
27
+ console.log(`Running suite with ${suiteInfo.totalSpecsDefined} tests.`)
28
+ }
29
+
30
+ suiteStarted(result) {
31
+ console.log(`\n${this.suiteSeq}. ${result.fullName}`)
32
+ this.suiteSeq++
33
+ }
34
+
35
+ specStarted(result) {
36
+ process.stdout.write(` - ${result.description} `)
37
+ }
38
+
39
+ specDone(result) {
40
+ process.stdout.write(
41
+ ` ${result.status} [${result.passedExpectations.length}] (${result.duration} ms)`
42
+ )
43
+
44
+ for (var i = 0; i < result.failedExpectations.length; i++) {
45
+ console.log()
46
+ this._indentPrint(result.failedExpectations[i].message)
47
+ this._indentPrint(result.failedExpectations[i].stack)
48
+ }
49
+
50
+ process.stdout.write(`\n`)
51
+ }
52
+
53
+ jasmineDone(result) {
54
+ console.log('Overall result: ' + result.overallStatus)
55
+ for (var i = 0; i < result.failedExpectations.length; i++) {
56
+ console.log('Global ' + result.failedExpectations[i].message)
57
+ console.log(result.failedExpectations[i].stack)
58
+ }
59
+ }
60
+ }
61
+
62
+ jasmine.jasmine.getEnv().addReporter(new MyConsoleReporter())
15
63
  jasmine.execute()
package/test/test.js CHANGED
@@ -1,6 +1,22 @@
1
1
  const { authenticate, LdapAuthenticationError } = require('../index.js')
2
2
 
3
3
  describe('ldap-authentication test', () => {
4
+ it('Use an admin user to check if user exists', async () => {
5
+ let options = {
6
+ ldapOpts: {
7
+ url: 'ldap://ldap.forumsys.com',
8
+ },
9
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
10
+ adminPassword: 'password',
11
+ verifyUserExists: true,
12
+ userSearchBase: 'dc=example,dc=com',
13
+ usernameAttribute: 'uid',
14
+ username: 'gauss',
15
+ }
16
+ let user = await authenticate(options)
17
+ expect(user).toBeTruthy()
18
+ expect(user.uid).toEqual('gauss')
19
+ })
4
20
  it('Use an admin user to authenticate a regular user', async () => {
5
21
  let options = {
6
22
  ldapOpts: {
@@ -17,6 +33,25 @@ describe('ldap-authentication test', () => {
17
33
  expect(user).toBeTruthy()
18
34
  expect(user.uid).toEqual('gauss')
19
35
  })
36
+ it('Use an admin user to authenticate a regular user and return attrubutes', async () => {
37
+ let options = {
38
+ ldapOpts: {
39
+ url: 'ldap://ldap.forumsys.com',
40
+ },
41
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
42
+ adminPassword: 'password',
43
+ userPassword: 'password',
44
+ userSearchBase: 'dc=example,dc=com',
45
+ usernameAttribute: 'uid',
46
+ username: 'gauss',
47
+ attributes: ['uid', 'sn'],
48
+ }
49
+ let user = await authenticate(options)
50
+ expect(user).toBeTruthy()
51
+ expect(user.uid).toEqual('gauss')
52
+ expect(user.sn).toEqual('Gauss')
53
+ expect(user.cn).toBeUndefined()
54
+ })
20
55
  it('Use an regular user to authenticate iteself', async () => {
21
56
  let options = {
22
57
  ldapOpts: {
@@ -32,6 +67,24 @@ describe('ldap-authentication test', () => {
32
67
  expect(user).toBeTruthy()
33
68
  expect(user.uid).toEqual('einstein')
34
69
  })
70
+ it('Use an regular user to authenticate iteself and return attributes', async () => {
71
+ let options = {
72
+ ldapOpts: {
73
+ url: 'ldap://ldap.forumsys.com',
74
+ },
75
+ userDn: 'uid=einstein,dc=example,dc=com',
76
+ userPassword: 'password',
77
+ userSearchBase: 'dc=example,dc=com',
78
+ usernameAttribute: 'uid',
79
+ username: 'einstein',
80
+ attributes: ['uid', 'sn'],
81
+ }
82
+ let user = await authenticate(options)
83
+ expect(user).toBeTruthy()
84
+ expect(user.uid).toEqual('einstein')
85
+ expect(user.sn).toEqual('Einstein')
86
+ expect(user.cn).toBeUndefined()
87
+ })
35
88
  it('Use an regular user to authenticate iteself without search', async () => {
36
89
  let options = {
37
90
  ldapOpts: {
@@ -277,14 +330,14 @@ describe('ldap-authentication negative test', () => {
277
330
  let options = {
278
331
  ldapOpts: {
279
332
  url: 'ldap://x.forumsys.com',
280
- connectTimeout: 2000
333
+ connectTimeout: 2000,
281
334
  },
282
335
  userDn: 'uid=einstein,dc=example,dc=com',
283
336
  userPassword: 'password',
284
337
  usernameAttribute: 'cn',
285
338
  userSearchBase: 'dc=example,dc=com',
286
339
  username: 'einstein',
287
- starttls: true
340
+ starttls: true,
288
341
  }
289
342
  try {
290
343
  await authenticate(options)