ldap-authentication 3.2.6 → 3.3.2

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/README.md CHANGED
@@ -10,7 +10,7 @@ Make authentication with an LDAP server easy.
10
10
 
11
11
  ## Description
12
12
 
13
- This library use `ldapjs` as the underneath library. It has three modes of authentications:
13
+ This library use `ldapts` as the underneath library. It has three modes of authentications:
14
14
 
15
15
  1. **Admin authenticate mode**. If an admin user is provided, the library will login (ldap bind) with the admin user,
16
16
  then search for the user to be authenticated, get its DN (distinguish name), then use
@@ -149,7 +149,7 @@ auth()
149
149
 
150
150
  ## Parameters
151
151
 
152
- - `ldapOpts`: This is passed to `ldapjs` client directly
152
+ - `ldapOpts`: This is passed to `ldapts` client directly
153
153
  - `url`: url of the ldap server. Example: `ldap://ldap.forumsys.com`
154
154
  - `tlsOptions`: options to pass to node tls. Example: `{ rejectUnauthorized: false }`
155
155
  - `connectTimeout`: Int. Default: `5000`. Connect timeout in ms
@@ -210,7 +210,7 @@ export async function verifyLogin(email: string, password: string) {
210
210
  const profilePhoto = ldapUser['thumbnailPhoto;binary'];
211
211
 
212
212
  /* using the image
213
- <img src={`data:image/*;base64,${profilePhoto}`} />
213
+ <img src={`data:image/*;base64,${profilePhoto}`} />
214
214
  */
215
215
  return { user: ldapUser };
216
216
  }
package/example/index.js CHANGED
@@ -4,7 +4,7 @@ async function auth() {
4
4
  // auth with admin
5
5
  let options = {
6
6
  ldapOpts: {
7
- url: 'ldap://ldap.forumsys.com',
7
+ url: 'ldap://localhost:1389',
8
8
  // tlsOptions: { rejectUnauthorized: false }
9
9
  },
10
10
  adminDn: 'cn=read-only-admin,dc=example,dc=com',
@@ -55,4 +55,4 @@ async function auth() {
55
55
  console.log(`user = ${JSON.stringify(user, null, 2)}`)
56
56
  }
57
57
 
58
- auth()
58
+ auth().then()
package/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ClientOptions } from 'ldapjs'
1
+ import { ClientOptions } from 'ldapts'
2
2
 
3
3
  declare module 'ldap-authentication' {
4
4
  export interface AuthenticationOptions {
package/index.js CHANGED
@@ -1,148 +1,57 @@
1
1
  const assert = require('assert')
2
- const ldap = require('ldapjs')
2
+ const ldapts = require('ldapts')
3
3
  // escape the , in CN in DN
4
4
  function _ldapEscapeDN(s) {
5
- let ret = "";
6
- let comaPositions = [];
7
- let done = false;
8
- let countEq = 0;
5
+ let ret = ''
6
+ let comaPositions = []
7
+ let done = false
8
+ let countEq = 0
9
9
  for (let i = 0; !done && i < s.length; i++) {
10
10
  switch (s[i]) {
11
- case "\\":
11
+ case '\\':
12
12
  // user already escapped, continue
13
- i++;
14
- break;
15
- case ",":
13
+ i++
14
+ break
15
+ case ',':
16
16
  if (countEq == 1) {
17
- comaPositions.push(i);
17
+ comaPositions.push(i)
18
18
  }
19
- break;
20
- case "=":
21
- countEq++;
19
+ break
20
+ case '=':
21
+ countEq++
22
22
  if (countEq == 2) {
23
- done = true;
23
+ done = true
24
24
  }
25
- break;
25
+ break
26
26
  }
27
27
  }
28
28
  if (done) {
29
- comaPositions.pop();
29
+ comaPositions.pop()
30
30
  }
31
- let lastIndex = 0;
31
+ let lastIndex = 0
32
32
  for (let i of comaPositions) {
33
- ret += s.substring(lastIndex, i);
34
- ret += "\\,";
35
- lastIndex = i + 1;
33
+ ret += s.substring(lastIndex, i)
34
+ ret += '\\,'
35
+ lastIndex = i + 1
36
36
  }
37
- ret += s.substring(lastIndex);
38
- return ret;
37
+ ret += s.substring(lastIndex)
38
+ return ret
39
39
  }
40
40
 
41
- // convert an escaped utf8 string returned from ldapjs
42
- function _isHex(c) {
43
- return (
44
- (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
45
- )
46
- }
47
- function _parseEscapedHexToUtf8(s) {
48
- // convert 'cn=\\e7\\a0\\94\\e5\\8f\\91A\\e9\\83\\a8,ou=users,dc=example,dc=com'
49
- // to 'cn=研发A部,ou=users,dc=example,dc=com'
50
- let ret = Buffer.alloc(0)
51
- let len = s.length
52
- for (let i = 0; i < len; i++) {
53
- let c = s[i]
54
- let item
55
- if (c == '\\' && i < len - 2 && _isHex(s[i + 1]) && _isHex(s[i + 2])) {
56
- item = Buffer.from(s.substring(i + 1, i + 3), 'hex')
57
- i += 2
58
- } else {
59
- item = Buffer.from(c)
60
- }
61
- ret = Buffer.concat([ret, item])
62
- }
63
- return ret.toString()
64
- }
65
-
66
- function _recursiveParseHexString(obj) {
67
- if (Array.isArray(obj)) {
68
- return obj.map((ele) => _recursiveParseHexString(ele))
69
- }
70
- if (typeof obj == 'string') {
71
- return _parseEscapedHexToUtf8(obj)
72
- }
73
- if (typeof obj == 'object') {
74
- for (let key in obj) {
75
- obj[key] = _recursiveParseHexString(obj[key])
76
- }
77
- return obj
78
- }
79
- return obj
80
- }
81
- // convert a SearchResultEntry object in ldapjs 3.0
82
- // to a user object to maintain backward compatibility
83
-
84
- function _searchResultToUser(pojo) {
85
- assert(pojo.type == 'SearchResultEntry')
86
- let user = { dn: pojo.objectName }
87
- pojo.attributes.forEach((attribute) => {
88
- user[attribute.type] =
89
- attribute.values.length == 1 ? attribute.values[0] : attribute.values
90
- })
91
- return _recursiveParseHexString(user)
92
- }
93
41
  // bind and return the ldap client
94
- function _ldapBind(dn, password, starttls, ldapOpts) {
42
+ async function _ldapBind(dn, password, starttls, ldapOpts) {
43
+ // TODO: check if ldapts expects escaped dn or not (possible double escaping problems?)
95
44
  dn = _ldapEscapeDN(dn)
96
- return new Promise(function (resolve, reject) {
97
- ldapOpts.connectTimeout = ldapOpts.connectTimeout || 5000
98
- let client = ldap.createClient(ldapOpts)
99
-
100
- client.on('connect', function () {
101
- if (starttls) {
102
- client.starttls(ldapOpts.tlsOptions, null, function (error) {
103
- if (error) {
104
- reject(error)
105
- return
106
- }
107
- client.bind(dn, password, function (err) {
108
- if (err) {
109
- reject(err)
110
- return
111
- }
112
- ldapOpts.log && ldapOpts.log.trace('bind success!')
113
- resolve(client)
114
- })
115
- })
116
- } else {
117
- client.bind(dn, password, function (err) {
118
- if (err) {
119
- reject(err)
120
- return
121
- }
122
- ldapOpts.log && ldapOpts.log.trace('bind success!')
123
- resolve(client)
124
- })
125
- }
126
- })
45
+ ldapOpts.connectTimeout = ldapOpts.connectTimeout || 5000
46
+ let client = new ldapts.Client(ldapOpts)
127
47
 
128
- //Fix for issue https://github.com/shaozi/ldap-authentication/issues/13
129
- client.on('timeout', (err) => {
130
- reject(err)
131
- })
132
- client.on('connectTimeout', (err) => {
133
- reject(err)
134
- })
135
- client.on('error', (err) => {
136
- reject(err)
137
- })
48
+ if (starttls) {
49
+ await client.startTLS(ldapOpts.tlsOptions)
50
+ }
138
51
 
139
- client.on('connectError', function (error) {
140
- if (error) {
141
- reject(error)
142
- return
143
- }
144
- })
145
- })
52
+ await client.bind(dn, password)
53
+ ldapOpts.log && ldapOpts.log.trace('bind success!')
54
+ return client
146
55
  }
147
56
 
148
57
  // search a user and return the object
@@ -153,48 +62,43 @@ async function _searchUser(
153
62
  username,
154
63
  attributes = null
155
64
  ) {
156
- return new Promise(function (resolve, reject) {
157
- let filter = new ldap.filters.EqualityFilter({
158
- attribute: usernameAttribute,
159
- value: username,
160
- })
161
- let searchOptions = {
162
- filter: filter,
163
- scope: 'sub',
164
- attributes: attributes,
165
- }
166
- if (attributes) {
167
- searchOptions.attributes = attributes
168
- }
169
- ldapClient.search(searchBase, searchOptions, function (err, res) {
170
- let user = null
171
- if (err) {
172
- reject(err)
173
- return
174
- }
175
- res.on('searchEntry', function (entry) {
176
- user = _searchResultToUser(entry.pojo)
177
- })
178
- res.on('searchReference', function (referral) {
179
- // TODO: we don't support reference yet
180
- // If the server was able to locate the entry referred to by the baseObject
181
- // but could not search one or more non-local entries,
182
- // the server may return one or more SearchResultReference messages,
183
- // each containing a reference to another set of servers for continuing the operation.
184
- // referral.uris
185
- })
186
- res.on('error', function (err) {
187
- reject(err)
188
- })
189
- res.on('end', function (result) {
190
- if (result.status != 0) {
191
- reject(new Error('ldap search status is not 0, search failed'))
192
- } else {
193
- resolve(user)
194
- }
195
- })
196
- })
65
+ let filter = new ldapts.EqualityFilter({
66
+ attribute: usernameAttribute,
67
+ value: username,
197
68
  })
69
+ let searchOptions = {
70
+ filter: filter,
71
+ scope: 'sub',
72
+ attributes: attributes,
73
+ }
74
+ if (attributes) {
75
+ searchOptions.attributes = attributes
76
+ }
77
+
78
+ // TODO: we don't support reference yet
79
+ // If the server was able to locate the entry referred to by the baseObject
80
+ // but could not search one or more non-local entries,
81
+ // the server may return one or more SearchResultReference messages,
82
+ // each containing a reference to another set of servers for continuing the operation.
83
+ // referral.uris
84
+ const { searchEntries, searchReferences } = await ldapClient.search(
85
+ searchBase,
86
+ searchOptions
87
+ )
88
+
89
+ let user
90
+ if (
91
+ !searchEntries ||
92
+ searchEntries.length < 1 ||
93
+ !searchEntries[0] ||
94
+ !searchEntries[0].dn
95
+ ) {
96
+ user = null
97
+ } else {
98
+ user = searchEntries[0]
99
+ }
100
+
101
+ return user
198
102
  }
199
103
 
200
104
  // search a groups which user is member
@@ -206,36 +110,43 @@ async function _searchUserGroups(
206
110
  groupMemberAttribute = 'member',
207
111
  groupMemberUserAttribute = 'dn'
208
112
  ) {
209
- return new Promise(function (resolve, reject) {
210
- ldapClient.search(
211
- searchBase,
212
- {
213
- filter: `(&(objectclass=${groupClass})(${groupMemberAttribute}=${user[groupMemberUserAttribute]}))`,
214
- scope: 'sub',
215
- },
216
- function (err, res) {
217
- let groups = []
218
- if (err) {
219
- reject(err)
220
- return
221
- }
222
- res.on('searchEntry', function (entry) {
223
- groups.push(_recursiveParseHexString(entry.pojo))
224
- })
225
- res.on('searchReference', function (referral) {})
226
- res.on('error', function (err) {
227
- reject(err)
228
- })
229
- res.on('end', function (result) {
230
- if (result.status != 0) {
231
- reject(new Error('ldap search status is not 0, search failed'))
232
- } else {
233
- resolve(groups)
234
- }
235
- })
236
- }
237
- )
113
+ // Below works, but prefer using ldapts Filter subclasses to build this search, so that correct escaping is done
114
+ // const filter = `(&(objectclass=${groupClass})(${groupMemberAttribute}=${user[groupMemberUserAttribute]}))`
115
+ const filter = new ldapts.AndFilter({
116
+ filters: [
117
+ new ldapts.EqualityFilter({
118
+ attribute: 'objectclass',
119
+ value: groupClass,
120
+ }),
121
+ new ldapts.EqualityFilter({
122
+ attribute: groupMemberAttribute,
123
+ value: user[groupMemberUserAttribute],
124
+ }),
125
+ ],
238
126
  })
127
+
128
+ const { searchEntries, searchReferences } = await ldapClient.search(
129
+ searchBase,
130
+ {
131
+ filter: filter,
132
+ scope: 'sub',
133
+ }
134
+ )
135
+
136
+ let groups
137
+ if (!searchEntries || searchEntries.length < 1) {
138
+ groups = []
139
+ } else {
140
+ groups = searchEntries
141
+ }
142
+ // ldapjs has group.objectName, ldapts does not have it. instead, use dn
143
+ // add objectName back for backward compatibility
144
+ for (let group of groups) {
145
+ if (typeof group.objectName === 'undefined') {
146
+ group.objectName = group.dn
147
+ }
148
+ }
149
+ return groups
239
150
  }
240
151
 
241
152
  async function authenticateWithAdmin(
@@ -262,6 +173,9 @@ async function authenticateWithAdmin(
262
173
  ldapOpts
263
174
  )
264
175
  } catch (error) {
176
+ if (ldapAdminClient.isConnected) {
177
+ await ldapAdminClient.unbind()
178
+ }
265
179
  throw { admin: error }
266
180
  }
267
181
  let user = await _searchUser(
@@ -276,6 +190,7 @@ async function authenticateWithAdmin(
276
190
  ldapOpts.log.trace(
277
191
  `admin did not find user! (${usernameAttribute}=${username})`
278
192
  )
193
+ await ldapAdminClient.unbind()
279
194
  throw new LdapAuthenticationError(
280
195
  'user not found or usernameAttribute is wrong'
281
196
  )
@@ -285,6 +200,9 @@ async function authenticateWithAdmin(
285
200
  try {
286
201
  ldapUserClient = await _ldapBind(userDn, userPassword, starttls, ldapOpts)
287
202
  } catch (error) {
203
+ if (ldapUserClient.isConnected) {
204
+ await ldapUserClient.unbind()
205
+ }
288
206
  throw error
289
207
  }
290
208
  if (groupsSearchBase && groupClass && groupMemberAttribute) {
@@ -298,6 +216,8 @@ async function authenticateWithAdmin(
298
216
  )
299
217
  user.groups = groups
300
218
  }
219
+ await ldapAdminClient.unbind()
220
+ await ldapUserClient.unbind()
301
221
  return user
302
222
  }
303
223
 
@@ -319,10 +239,14 @@ async function authenticateWithUser(
319
239
  try {
320
240
  ldapUserClient = await _ldapBind(userDn, userPassword, starttls, ldapOpts)
321
241
  } catch (error) {
242
+ if (ldapUserClient.isConnected) {
243
+ await ldapUserClient.unbind()
244
+ }
322
245
  throw error
323
246
  }
324
247
  if (!usernameAttribute || !userSearchBase) {
325
248
  // if usernameAttribute is not provided, no user detail is needed.
249
+ await ldapUserClient.unbind()
326
250
  return true
327
251
  }
328
252
  let user = await _searchUser(
@@ -337,6 +261,7 @@ async function authenticateWithUser(
337
261
  ldapOpts.log.trace(
338
262
  `user logged in, but user details could not be found. (${usernameAttribute}=${username}). Probabaly wrong attribute or searchBase?`
339
263
  )
264
+ await ldapUserClient.unbind()
340
265
  throw new LdapAuthenticationError(
341
266
  'user logged in, but user details could not be found. Probabaly usernameAttribute or userSearchBase is wrong?'
342
267
  )
@@ -352,6 +277,7 @@ async function authenticateWithUser(
352
277
  )
353
278
  user.groups = groups
354
279
  }
280
+ await ldapUserClient.unbind()
355
281
  return user
356
282
  }
357
283
 
@@ -378,6 +304,9 @@ async function verifyUserExists(
378
304
  ldapOpts
379
305
  )
380
306
  } catch (error) {
307
+ if (ldapAdminClient.isConnected) {
308
+ await ldapAdminClient.unbind()
309
+ }
381
310
  throw { admin: error }
382
311
  }
383
312
  let user = await _searchUser(
@@ -392,6 +321,7 @@ async function verifyUserExists(
392
321
  ldapOpts.log.trace(
393
322
  `admin did not find user! (${usernameAttribute}=${username})`
394
323
  )
324
+ await ldapAdminClient.unbind()
395
325
  throw new LdapAuthenticationError(
396
326
  'user not found or usernameAttribute is wrong'
397
327
  )
@@ -407,6 +337,7 @@ async function verifyUserExists(
407
337
  )
408
338
  user.groups = groups
409
339
  }
340
+ await ldapAdminClient.unbind()
410
341
  return user
411
342
  }
412
343
 
@@ -503,8 +434,5 @@ module.exports.authenticate = authenticate
503
434
  module.exports.LdapAuthenticationError = LdapAuthenticationError
504
435
 
505
436
  module.exports.exportForTesting = {
506
- _isHex,
507
- _parseEscapedHexToUtf8,
508
- _recursiveParseHexString,
509
- _ldapEscapeDN
437
+ _ldapEscapeDN,
510
438
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldap-authentication",
3
- "version": "3.2.6",
3
+ "version": "3.3.2",
4
4
  "description": "A simple async nodejs library for LDAP user authentication",
5
5
  "main": "index.js",
6
6
  "types": "./index.d.ts",
@@ -16,6 +16,7 @@
16
16
  "authenticate",
17
17
  "authentication",
18
18
  "ldapjs",
19
+ "ldapts",
19
20
  "security",
20
21
  "simple",
21
22
  "lightweight",
@@ -36,9 +37,9 @@
36
37
  },
37
38
  "homepage": "https://github.com/shaozi/ldap-authentication#readme",
38
39
  "dependencies": {
39
- "ldapjs": "^3.0.7"
40
+ "ldapts": "^7.3.1"
40
41
  },
41
42
  "devDependencies": {
42
- "jasmine": "^5.3.0"
43
+ "jasmine": "^5.6.0"
43
44
  }
44
45
  }
@@ -1,62 +1,21 @@
1
1
  const { exportForTesting } = require('../index.js')
2
- const { _isHex, _parseEscapedHexToUtf8, _recursiveParseHexString, _ldapEscapeDN } =
3
- exportForTesting
2
+ const { _ldapEscapeDN } = exportForTesting
4
3
 
5
- describe('string conversion test', () => {
6
- it('unescape string test', () => {
7
- let s =
8
- 'cn=\\e7\\a0\\94\\e5\\8f\\91A\\e9\\83\\a8,ou=users,dc=example,dc=com'
9
- let us = _parseEscapedHexToUtf8(s)
10
- expect(us).toEqual('cn=研发A部,ou=users,dc=example,dc=com')
11
- })
12
- it('unescape string test2', () => {
13
- let s =
14
- 'cn=\\e7\\a0\\94\\e5\\8f\\91A\\e9\\83\\a8\\c2\\a9,ou=users,dc=example,dc=com'
15
- let us = _parseEscapedHexToUtf8(s)
16
- expect(us).toEqual('cn=研发A部©,ou=users,dc=example,dc=com')
17
- })
18
- it('unescape string test3', () => {
19
- let s = 'cn=ABC,ou=users,dc=example,dc=com'
20
- let us = _parseEscapedHexToUtf8(s)
21
- expect(us).toEqual('cn=ABC,ou=users,dc=example,dc=com')
22
- })
23
- it('convert obj', () => {
24
- let target = {
25
- a: ['研发A部©', 'abc'],
26
- b: 'xyz',
27
- c: true,
28
- d: null,
29
- e: '研发A部©',
30
- f: 1000,
31
- }
32
- let obj = {
33
- a: ['\\e7\\a0\\94\\e5\\8f\\91A\\e9\\83\\a8\\c2\\a9', 'abc'],
34
- b: 'xyz',
35
- c: true,
36
- d: null,
37
- e: '\\e7\\a0\\94\\e5\\8f\\91A\\e9\\83\\a8\\c2\\a9',
38
- f: 1000,
39
- }
40
- let converted = _recursiveParseHexString(obj)
41
- expect(converted).toEqual(target)
42
- })
43
- })
44
-
45
- describe('escape , in the DN test', ()=>{
4
+ describe('escape , in the DN test', () => {
46
5
  let cases = [
47
- { s: "a", want: "a" },
48
- { s: "", want: "" },
49
- { s: "CN=a,DN=b", want: "CN=a,DN=b" },
50
- { s: "CN=a, c,DN=b", want: "CN=a\\, c,DN=b" },
51
- { s: "CN=a\\, c,DN=b", want: "CN=a\\, c,DN=b" },
52
- { s: "CN=a, b, c,DN=b", want: "CN=a\\, b\\, c,DN=b" },
53
- { s: "CN=a, c", want: "CN=a\\, c" },
6
+ { s: 'a', want: 'a' },
7
+ { s: '', want: '' },
8
+ { s: 'CN=a,DN=b', want: 'CN=a,DN=b' },
9
+ { s: 'CN=a, c,DN=b', want: 'CN=a\\, c,DN=b' },
10
+ { s: 'CN=a\\, c,DN=b', want: 'CN=a\\, c,DN=b' },
11
+ { s: 'CN=a, b, c,DN=b', want: 'CN=a\\, b\\, c,DN=b' },
12
+ { s: 'CN=a, c', want: 'CN=a\\, c' },
54
13
  ]
55
14
  for (let c of cases) {
56
- it("escape "+c.s, ()=>{
15
+ it('escape ' + c.s, () => {
57
16
  let got = _ldapEscapeDN(c.s)
58
17
  expect(got).toEqual(c.want)
59
18
  })
60
-
61
19
  }
62
- })
20
+ })
21
+
package/test/test.spec.js CHANGED
@@ -144,9 +144,7 @@ describe('ldap-authentication test', () => {
144
144
  let user = await authenticate(options)
145
145
  expect(user).toBeTruthy()
146
146
  expect(user.groups.length).toBeGreaterThan(0)
147
- expect(user.groups[0].objectName).toEqual(
148
- 'cn=科学A部,ou=users,dc=example,dc=com'
149
- )
147
+ expect(user.groups[0].dn).toEqual('cn=科学A部,ou=users,dc=example,dc=com')
150
148
  })
151
149
  it('Use regular user to authenticate and fetch user group information', async () => {
152
150
  let options = {
@@ -167,6 +165,8 @@ describe('ldap-authentication test', () => {
167
165
  let user = await authenticate(options)
168
166
  expect(user).toBeTruthy()
169
167
  expect(user.groups.length).toBeGreaterThan(0)
168
+ expect(user.groups[0].dn).toEqual('cn=科学A部,ou=users,dc=example,dc=com')
169
+ // backward compatible with 3.2
170
170
  expect(user.groups[0].objectName).toEqual(
171
171
  'cn=科学A部,ou=users,dc=example,dc=com'
172
172
  )