ldap-authentication 3.3.4 → 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.
- package/.devcontainer/docker-compose.yml +1 -1
- package/.github/workflows/integration-test.yml +2 -2
- package/.github/workflows/npm-publish.yml +8 -19
- package/README.md +65 -2
- package/docker-compose.yml +1 -1
- package/index.d.ts +1 -0
- package/index.js +257 -30
- package/package.json +7 -4
- package/test/authentication-result.spec.js +425 -0
- package/test/binary.spec.js +115 -0
- package/test/starttls.spec.js +126 -0
- package/test/test.spec.js +3 -3
|
@@ -16,7 +16,7 @@ jobs:
|
|
|
16
16
|
runs-on: ubuntu-latest
|
|
17
17
|
strategy:
|
|
18
18
|
matrix:
|
|
19
|
-
node-version: [
|
|
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
|
|
49
|
+
- run: npm ci
|
|
50
50
|
- run: npm run test
|
|
51
51
|
|
|
52
52
|
- name: Stop containers
|
|
@@ -1,33 +1,22 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
@@ -147,6 +147,43 @@ async function auth() {
|
|
|
147
147
|
auth()
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
### Example with StartTLS
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
import { authenticate } from 'ldap-authentication'
|
|
154
|
+
|
|
155
|
+
async function auth() {
|
|
156
|
+
// auth with admin
|
|
157
|
+
let options = {
|
|
158
|
+
ldapOpts: {
|
|
159
|
+
url: 'ldap://ldap.example.com',
|
|
160
|
+
tlsOptions: {
|
|
161
|
+
rejectUnauthorized: false, // For self-signed certificates
|
|
162
|
+
minVersion: 'TLSv1.2',
|
|
163
|
+
servername: 'ldap.example.com' // For SNI (Server Name Indication)
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
starttls: true, // Enable StartTLS
|
|
167
|
+
adminDn: 'cn=admin,dc=example,dc=com',
|
|
168
|
+
adminPassword: 'password',
|
|
169
|
+
userPassword: 'password',
|
|
170
|
+
userSearchBase: 'dc=example,dc=com',
|
|
171
|
+
usernameAttribute: 'uid',
|
|
172
|
+
username: 'testuser'
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let user = await authenticate(options)
|
|
176
|
+
console.log(user)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
auth()
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Important Notes for StartTLS:**
|
|
183
|
+
- Use `ldap://` URLs with `starttls: true` (not `ldaps://`)
|
|
184
|
+
- For `ldaps://` URLs, omit `starttls` and the connection will use TLS from the start
|
|
185
|
+
- TLS options like `rejectUnauthorized`, `minVersion`, and `servername` can be specified in `ldapOpts.tlsOptions`
|
|
186
|
+
|
|
150
187
|
## Parameters
|
|
151
188
|
|
|
152
189
|
- `ldapOpts`: This is passed to `ldapts` client directly
|
|
@@ -172,7 +209,9 @@ auth()
|
|
|
172
209
|
to find the user and get user details in LDAP. Example: `some user input`
|
|
173
210
|
- `attributes`: A list of attributes of a user details to be returned from the LDAP server.
|
|
174
211
|
If is set to `[]` or ommited, all details will be returned. Example: `['sn', 'cn']`
|
|
175
|
-
- `starttls`: Boolean. Use `STARTTLS` or not
|
|
212
|
+
- `starttls`: Boolean. Use `STARTTLS` or not. When `true`, the connection will be upgraded to TLS
|
|
213
|
+
using the STARTTLS extended operation. TLS options can be specified in `ldapOpts.tlsOptions`.
|
|
214
|
+
Note: Use `starttls: true` with `ldap://` URLs, not `ldaps://` URLs
|
|
176
215
|
- `groupsSearchBase`: if specified with groupClass, will serve as search base for authenticated user groups
|
|
177
216
|
- `groupClass`: if specified with groupsSearchBase, will be used as objectClass in search filter for authenticated user groups
|
|
178
217
|
- `groupMemberAttribute`: if specified with groupClass and groupsSearchBase, will be used as member name (if not specified this defaults to `member`) in search filter for authenticated user groups
|
|
@@ -182,6 +221,28 @@ auth()
|
|
|
182
221
|
|
|
183
222
|
The user object if `authenticate()` is success.
|
|
184
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
|
+
|
|
185
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).
|
|
186
247
|
|
|
187
248
|
Buffer data can now be accessed by `user.raw.profilePhoto`, etc, instead of `user.profilePhoto`.
|
|
@@ -221,4 +282,6 @@ export async function verifyLogin(email: string, password: string) {
|
|
|
221
282
|
|
|
222
283
|
Version 2 supports Node version 12, 14, 15, 16, 17 and 18.
|
|
223
284
|
|
|
224
|
-
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/docker-compose.yml
CHANGED
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -38,12 +38,82 @@ 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
|
+
|
|
99
|
+
// When using StartTLS, we need to exclude tlsOptions from the Client constructor
|
|
100
|
+
// and only pass them to the startTLS() method to avoid connection conflicts.
|
|
101
|
+
// According to ldapts documentation:
|
|
102
|
+
// - For LDAPS (ldaps://): pass tlsOptions to Client constructor
|
|
103
|
+
// - For StartTLS (ldap://): do NOT pass tlsOptions to Client constructor, only to startTLS()
|
|
104
|
+
// - For plain LDAP (ldap://): do NOT pass tlsOptions to Client constructor
|
|
105
|
+
let clientOpts = ldapOpts
|
|
106
|
+
const isLdaps = ldapOpts.url && ldapOpts.url.startsWith('ldaps://')
|
|
107
|
+
|
|
108
|
+
// Only pass tlsOptions to Client constructor if using ldaps:// protocol
|
|
109
|
+
// For ldap:// protocol (plain or StartTLS), exclude tlsOptions from constructor
|
|
110
|
+
if (!isLdaps && ldapOpts.tlsOptions) {
|
|
111
|
+
// Create a shallow copy of ldapOpts without tlsOptions for the Client constructor
|
|
112
|
+
const { tlsOptions, ...optsWithoutTls } = ldapOpts
|
|
113
|
+
clientOpts = optsWithoutTls
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let client = new ldapts.Client(clientOpts)
|
|
47
117
|
|
|
48
118
|
if (starttls) {
|
|
49
119
|
await client.startTLS(ldapOpts.tlsOptions)
|
|
@@ -60,7 +130,8 @@ async function _searchUser(
|
|
|
60
130
|
searchBase,
|
|
61
131
|
usernameAttribute,
|
|
62
132
|
username,
|
|
63
|
-
attributes = null
|
|
133
|
+
attributes = null,
|
|
134
|
+
explicitBufferAttributes = null
|
|
64
135
|
) {
|
|
65
136
|
let filter = new ldapts.EqualityFilter({
|
|
66
137
|
attribute: usernameAttribute,
|
|
@@ -74,6 +145,9 @@ async function _searchUser(
|
|
|
74
145
|
if (attributes) {
|
|
75
146
|
searchOptions.attributes = attributes
|
|
76
147
|
}
|
|
148
|
+
if(explicitBufferAttributes) {
|
|
149
|
+
searchOptions.explicitBufferAttributes = explicitBufferAttributes
|
|
150
|
+
}
|
|
77
151
|
|
|
78
152
|
// TODO: we don't support reference yet
|
|
79
153
|
// If the server was able to locate the entry referred to by the baseObject
|
|
@@ -93,8 +167,24 @@ async function _searchUser(
|
|
|
93
167
|
!searchEntries[0] ||
|
|
94
168
|
!searchEntries[0].dn
|
|
95
169
|
) {
|
|
96
|
-
|
|
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
|
+
)
|
|
97
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
|
+
|
|
98
188
|
user = searchEntries[0]
|
|
99
189
|
}
|
|
100
190
|
|
|
@@ -106,7 +196,22 @@ async function _searchUser(
|
|
|
106
196
|
}
|
|
107
197
|
}
|
|
108
198
|
}
|
|
109
|
-
|
|
199
|
+
// when attribute is one of the explicitBufferAttributes, should convert to base64 string
|
|
200
|
+
if (user != null && explicitBufferAttributes != null) {
|
|
201
|
+
for (let attr of explicitBufferAttributes) {
|
|
202
|
+
if (Buffer.isBuffer(user[attr])) {
|
|
203
|
+
user[attr] = user[attr].toString('base64')
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return new AuthenticationResult(
|
|
209
|
+
AUTH_RESULT_SUCCESS,
|
|
210
|
+
username,
|
|
211
|
+
user,
|
|
212
|
+
[authenticationMessages.AUTH_RESULT_SUCCESS],
|
|
213
|
+
ldapClient
|
|
214
|
+
)
|
|
110
215
|
}
|
|
111
216
|
|
|
112
217
|
// search a groups which user is member
|
|
@@ -170,7 +275,8 @@ async function authenticateWithAdmin(
|
|
|
170
275
|
groupClass,
|
|
171
276
|
groupMemberAttribute = 'member',
|
|
172
277
|
groupMemberUserAttribute = 'dn',
|
|
173
|
-
attributes = null
|
|
278
|
+
attributes = null,
|
|
279
|
+
explicitBufferAttributes = null
|
|
174
280
|
) {
|
|
175
281
|
let ldapAdminClient
|
|
176
282
|
try {
|
|
@@ -184,23 +290,39 @@ async function authenticateWithAdmin(
|
|
|
184
290
|
if (ldapAdminClient && ldapAdminClient.isConnected) {
|
|
185
291
|
await ldapAdminClient.unbind()
|
|
186
292
|
}
|
|
187
|
-
|
|
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
|
+
)
|
|
188
301
|
}
|
|
189
|
-
|
|
302
|
+
|
|
303
|
+
let searchResult = await _searchUser(
|
|
190
304
|
ldapAdminClient,
|
|
191
305
|
userSearchBase,
|
|
192
306
|
usernameAttribute,
|
|
193
307
|
username,
|
|
194
|
-
attributes
|
|
308
|
+
attributes,
|
|
309
|
+
explicitBufferAttributes
|
|
195
310
|
)
|
|
311
|
+
|
|
312
|
+
let user = searchResult.user
|
|
313
|
+
|
|
196
314
|
if (!user || !user.dn) {
|
|
197
315
|
ldapOpts.log &&
|
|
198
316
|
ldapOpts.log.trace(
|
|
199
317
|
`admin did not find user! (${usernameAttribute}=${username})`
|
|
200
318
|
)
|
|
201
319
|
await ldapAdminClient.unbind()
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
204
326
|
)
|
|
205
327
|
}
|
|
206
328
|
let userDn = user.dn
|
|
@@ -211,7 +333,14 @@ async function authenticateWithAdmin(
|
|
|
211
333
|
if (ldapUserClient && ldapUserClient.isConnected) {
|
|
212
334
|
await ldapUserClient.unbind()
|
|
213
335
|
}
|
|
214
|
-
|
|
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
|
+
)
|
|
215
344
|
}
|
|
216
345
|
if (groupsSearchBase && groupClass && groupMemberAttribute) {
|
|
217
346
|
let groups = await _searchUserGroups(
|
|
@@ -226,7 +355,14 @@ async function authenticateWithAdmin(
|
|
|
226
355
|
}
|
|
227
356
|
await ldapAdminClient.unbind()
|
|
228
357
|
await ldapUserClient.unbind()
|
|
229
|
-
|
|
358
|
+
|
|
359
|
+
return new AuthenticationResult(
|
|
360
|
+
AUTH_RESULT_SUCCESS,
|
|
361
|
+
username,
|
|
362
|
+
user,
|
|
363
|
+
[authenticationMessages.AUTH_RESULT_SUCCESS],
|
|
364
|
+
ldapAdminClient
|
|
365
|
+
)
|
|
230
366
|
}
|
|
231
367
|
|
|
232
368
|
async function authenticateWithUser(
|
|
@@ -241,7 +377,8 @@ async function authenticateWithUser(
|
|
|
241
377
|
groupClass,
|
|
242
378
|
groupMemberAttribute = 'member',
|
|
243
379
|
groupMemberUserAttribute = 'dn',
|
|
244
|
-
attributes = null
|
|
380
|
+
attributes = null,
|
|
381
|
+
explicitBufferAttributes = null
|
|
245
382
|
) {
|
|
246
383
|
let ldapUserClient
|
|
247
384
|
try {
|
|
@@ -250,28 +387,54 @@ async function authenticateWithUser(
|
|
|
250
387
|
if (ldapUserClient && ldapUserClient.isConnected) {
|
|
251
388
|
await ldapUserClient.unbind()
|
|
252
389
|
}
|
|
253
|
-
|
|
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
|
+
)
|
|
254
398
|
}
|
|
255
399
|
if (!usernameAttribute || !userSearchBase) {
|
|
256
400
|
// if usernameAttribute is not provided, no user detail is needed.
|
|
257
401
|
await ldapUserClient.unbind()
|
|
258
|
-
return
|
|
402
|
+
return new AuthenticationResult(
|
|
403
|
+
AUTH_RESULT_SUCCESS,
|
|
404
|
+
username,
|
|
405
|
+
{},
|
|
406
|
+
[authenticationMessages.AUTH_RESULT_SUCCESS],
|
|
407
|
+
ldapUserClient
|
|
408
|
+
)
|
|
259
409
|
}
|
|
260
|
-
|
|
410
|
+
|
|
411
|
+
let searchResult = await _searchUser(
|
|
261
412
|
ldapUserClient,
|
|
262
413
|
userSearchBase,
|
|
263
414
|
usernameAttribute,
|
|
264
415
|
username,
|
|
265
|
-
attributes
|
|
416
|
+
attributes,
|
|
417
|
+
explicitBufferAttributes
|
|
266
418
|
)
|
|
419
|
+
|
|
420
|
+
let user = searchResult.user
|
|
421
|
+
|
|
267
422
|
if (!user || !user.dn) {
|
|
268
423
|
ldapOpts.log &&
|
|
269
424
|
ldapOpts.log.trace(
|
|
270
425
|
`user logged in, but user details could not be found. (${usernameAttribute}=${username}). Probabaly wrong attribute or searchBase?`
|
|
271
426
|
)
|
|
272
427
|
await ldapUserClient.unbind()
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
275
438
|
)
|
|
276
439
|
}
|
|
277
440
|
if (groupsSearchBase && groupClass && groupMemberAttribute) {
|
|
@@ -286,7 +449,14 @@ async function authenticateWithUser(
|
|
|
286
449
|
user.groups = groups
|
|
287
450
|
}
|
|
288
451
|
await ldapUserClient.unbind()
|
|
289
|
-
|
|
452
|
+
|
|
453
|
+
return new AuthenticationResult(
|
|
454
|
+
AUTH_RESULT_SUCCESS,
|
|
455
|
+
username,
|
|
456
|
+
user,
|
|
457
|
+
[authenticationMessages.AUTH_RESULT_SUCCESS],
|
|
458
|
+
ldapUserClient
|
|
459
|
+
)
|
|
290
460
|
}
|
|
291
461
|
|
|
292
462
|
async function verifyUserExists(
|
|
@@ -301,7 +471,8 @@ async function verifyUserExists(
|
|
|
301
471
|
groupClass,
|
|
302
472
|
groupMemberAttribute = 'member',
|
|
303
473
|
groupMemberUserAttribute = 'dn',
|
|
304
|
-
attributes = null
|
|
474
|
+
attributes = null,
|
|
475
|
+
explicitBufferAttributes = null
|
|
305
476
|
) {
|
|
306
477
|
let ldapAdminClient
|
|
307
478
|
try {
|
|
@@ -315,23 +486,41 @@ async function verifyUserExists(
|
|
|
315
486
|
if (ldapAdminClient && ldapAdminClient.isConnected) {
|
|
316
487
|
await ldapAdminClient.unbind()
|
|
317
488
|
}
|
|
318
|
-
|
|
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
|
+
)
|
|
319
496
|
}
|
|
320
|
-
|
|
497
|
+
|
|
498
|
+
let searchResult = await _searchUser(
|
|
321
499
|
ldapAdminClient,
|
|
322
500
|
userSearchBase,
|
|
323
501
|
usernameAttribute,
|
|
324
502
|
username,
|
|
325
|
-
attributes
|
|
503
|
+
attributes,
|
|
504
|
+
explicitBufferAttributes
|
|
326
505
|
)
|
|
506
|
+
|
|
507
|
+
let user = searchResult.user
|
|
508
|
+
|
|
327
509
|
if (!user || !user.dn) {
|
|
328
510
|
ldapOpts.log &&
|
|
329
511
|
ldapOpts.log.trace(
|
|
330
512
|
`admin did not find user! (${usernameAttribute}=${username})`
|
|
331
513
|
)
|
|
332
514
|
await ldapAdminClient.unbind()
|
|
333
|
-
|
|
334
|
-
|
|
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
|
|
335
524
|
)
|
|
336
525
|
}
|
|
337
526
|
if (groupsSearchBase && groupClass && groupMemberAttribute) {
|
|
@@ -346,10 +535,28 @@ async function verifyUserExists(
|
|
|
346
535
|
user.groups = groups
|
|
347
536
|
}
|
|
348
537
|
await ldapAdminClient.unbind()
|
|
349
|
-
return
|
|
538
|
+
return new AuthenticationResult(
|
|
539
|
+
AUTH_RESULT_SUCCESS,
|
|
540
|
+
username,
|
|
541
|
+
user,
|
|
542
|
+
[authenticationMessages.AUTH_RESULT_SUCCESS],
|
|
543
|
+
ldapAdminClient
|
|
544
|
+
)
|
|
350
545
|
}
|
|
351
546
|
|
|
352
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) {
|
|
353
560
|
if (!options.userDn) {
|
|
354
561
|
assert(options.adminDn, 'Admin mode adminDn must be provided')
|
|
355
562
|
assert(options.adminPassword, 'Admin mode adminPassword must be provided')
|
|
@@ -366,6 +573,7 @@ async function authenticate(options) {
|
|
|
366
573
|
options.ldapOpts && options.ldapOpts.url,
|
|
367
574
|
'ldapOpts.url must be provided'
|
|
368
575
|
)
|
|
576
|
+
|
|
369
577
|
if (options.verifyUserExists) {
|
|
370
578
|
assert(options.adminDn, 'Admin mode adminDn must be provided')
|
|
371
579
|
assert(
|
|
@@ -384,9 +592,11 @@ async function authenticate(options) {
|
|
|
384
592
|
options.groupClass,
|
|
385
593
|
options.groupMemberAttribute,
|
|
386
594
|
options.groupMemberUserAttribute,
|
|
387
|
-
options.attributes
|
|
595
|
+
options.attributes,
|
|
596
|
+
options.explicitBufferAttributes
|
|
388
597
|
)
|
|
389
598
|
}
|
|
599
|
+
|
|
390
600
|
assert(options.userPassword, 'userPassword must be provided')
|
|
391
601
|
if (options.adminDn) {
|
|
392
602
|
assert(
|
|
@@ -406,9 +616,11 @@ async function authenticate(options) {
|
|
|
406
616
|
options.groupClass,
|
|
407
617
|
options.groupMemberAttribute,
|
|
408
618
|
options.groupMemberUserAttribute,
|
|
409
|
-
options.attributes
|
|
619
|
+
options.attributes,
|
|
620
|
+
options.explicitBufferAttributes
|
|
410
621
|
)
|
|
411
622
|
}
|
|
623
|
+
|
|
412
624
|
assert(options.userDn, 'adminDn/adminPassword OR userDn must be provided')
|
|
413
625
|
return await authenticateWithUser(
|
|
414
626
|
options.userDn,
|
|
@@ -422,7 +634,8 @@ async function authenticate(options) {
|
|
|
422
634
|
options.groupClass,
|
|
423
635
|
options.groupMemberAttribute,
|
|
424
636
|
options.groupMemberUserAttribute,
|
|
425
|
-
options.attributes
|
|
637
|
+
options.attributes,
|
|
638
|
+
options.explicitBufferAttributes
|
|
426
639
|
)
|
|
427
640
|
}
|
|
428
641
|
|
|
@@ -438,7 +651,21 @@ class LdapAuthenticationError extends Error {
|
|
|
438
651
|
}
|
|
439
652
|
}
|
|
440
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
|
+
|
|
441
667
|
module.exports.authenticate = authenticate
|
|
668
|
+
module.exports.authenticateResult = authenticateResult
|
|
442
669
|
module.exports.LdapAuthenticationError = LdapAuthenticationError
|
|
443
670
|
|
|
444
671
|
module.exports.exportForTesting = {
|
package/package.json
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ldap-authentication",
|
|
3
|
-
"version": "
|
|
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": "^
|
|
42
|
+
"ldapts": "^8.1.3"
|
|
40
43
|
},
|
|
41
44
|
"devDependencies": {
|
|
42
|
-
"jasmine": "^
|
|
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
|
+
})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const { Change } = require('ldapts')
|
|
2
|
+
const { authenticate, LdapAuthenticationError } = require('../index.js')
|
|
3
|
+
const ldapts = require('ldapts')
|
|
4
|
+
const { Attribute } = require('ldapts')
|
|
5
|
+
|
|
6
|
+
const url = process.env.INGITHUB ? 'ldap://localhost:1389' : 'ldap://ldap:1389'
|
|
7
|
+
|
|
8
|
+
describe('ldap-authentication binary attributes test', () => {
|
|
9
|
+
const jpegPhotoBase64 =
|
|
10
|
+
'/9j/4AAQSkZJRgABAQEASABIAAD/2wBDACgcHiMeGSgjISMtKygwPGRBPDc3PHtYXUlkkYCZlo+AjIqgtObDoKrarYqMyP/L2u71////m8H////6/+b9//j/2wBDASstLTw1PHZBQXb4pYyl+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj/wAARCACBAGQDAREAAhEBAxEB/8QAGQAAAwEBAQAAAAAAAAAAAAAAAAECAwQF/8QALRAAAgEDAwMCBQQDAAAAAAAAAAECAxEhBBIxMkFRInEFE2GBsRU0QlJikaH/xAAXAQEBAQEAAAAAAAAAAAAAAAAAAQID/8QAGxEBAQEAAwEBAAAAAAAAAAAAAAERAhIhMUH/2gAMAwEAAhEDEQA/APMIAAKAgChp2AdwAAACBlAQUkmu5RkAAACuAAADsAZQDWQKRA7AL7gAEFCbAQDAYAAXAdwBYAogAAAAgokBgADAEr8Ab09JWqZcdq8yJpjb9Pa5qr/ROy4l6Ga6ZxfvgdoYiemqw6o49yoyAAACCiQKa4AAN9PpnWy3aP5Jbiya9ClRhBJRjby/Ji1qRqRQBMkuwMZSk2trdiys2PPqR2zaNspAAIKE0A+yA201H51TPSuSW4smvTilFJJWSOdbi0RTZQghPgK56nNwzXLXtvx4NxhiUAEABQ0nJpJcgejpkoR2rtyznWo3uRpE5S4i8hfRCrPKm0wLlUUUrgZSruXTD/oROZPISuat1s3GGRQgIKADp0kFNy8pYM2rJrshBQ4M1qNErojTOdFyjZTcfqhKWaqFPObuyAdeKlBfRhcZrT01JzXdWsXWc/U7XF5IVlqWm1waYczKhXKIKEB0aOW2uvDwZvxZ9ejbBlq+CLsZaWrMAckvSgpSW6DQVNKW6ObXRUTVd5BmuKp1M05s2USBmaABpS60Qeg41rqUXeMcszjW61auroliyi7sZaFk1YKirVnSVopyXkqajTNtSv3YouXU/oWM8nHLLZWENFCsBiaAB1aCj86tnpjlko9javBlUVY29SWO4VizNaiVBJcu/m5FJwvyFTGmqct+Como3Juz5EZrnZplLKhWAw5NDZxisW47gen8NpqOncsepmarrIBZwwOWunSf+L4ZmxqVnvI1puoU1nObs347BLS7FZLZ8x27sIwqQcJuMuUaRAGdKN5X8ZKHJu+Qrq02venpbNilnGbDB3UNXHUJ7dqmv4szfA46luVnTbf0ZNFyqUpxcKnpv2kUclWiqT9LvF8O5mtRNnZO+ChSTccAOzsGShVhSlullrhFHPObnNyfLKiAHQW2lKfngqxk3dgSyhwm4TUk3dPDIPV0OrhWltmlGr2fkxYrqrUVVg0+ez8GdHPGhLa0/uaRMFeW1K9wrd6e7vJ2XhFRjKnGqtlNtPyyTB5MrqV28nRFxnf3JgogSqp0FBKzRVZlAEKKyFCbjO6eUQexotaq6UKjtUXf+xz5ccWOmas91u2RKVjHZSlaTe210yoipqHNbY3UfyTVEI7nbyUc+r0vLiuFc1Kjz7WNItSxkmCShAAAgCeXewURbTusNBHs6LWRrpQqO1T8nOzGmtamnG1unK9gM4UknfgqNYxSzZsBuTtiOL2ZKPJ1lFU6zt0yymblRhYonuQD4KEAAOXIUkEa6f8AcQ90Sq92r2+5zioXWvY0jQqJkRXn/EeiHuywcS4NI//Z'
|
|
11
|
+
|
|
12
|
+
const baseOptions = {
|
|
13
|
+
ldapOpts: {
|
|
14
|
+
url: url,
|
|
15
|
+
},
|
|
16
|
+
adminDn: 'cn=read-only-admin,dc=example,dc=com',
|
|
17
|
+
adminPassword: 'password',
|
|
18
|
+
verifyUserExists: true,
|
|
19
|
+
userSearchBase: 'dc=example,dc=com',
|
|
20
|
+
usernameAttribute: 'uid',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
it('Add jpegPhoto attribute', async () => {
|
|
24
|
+
let client = new ldapts.Client({
|
|
25
|
+
...baseOptions.ldapOpts,
|
|
26
|
+
})
|
|
27
|
+
try {
|
|
28
|
+
await client.bind(baseOptions.adminDn, baseOptions.adminPassword)
|
|
29
|
+
|
|
30
|
+
// https://github.com/ldapts/ldapts/issues/12
|
|
31
|
+
await client.modify(
|
|
32
|
+
'cn=gauss,ou=users,dc=example,dc=com',
|
|
33
|
+
new Change({
|
|
34
|
+
operation: 'replace',
|
|
35
|
+
modification: new Attribute({
|
|
36
|
+
type: 'jpegPhoto',
|
|
37
|
+
values: [Buffer.from(jpegPhotoBase64, 'base64')],
|
|
38
|
+
}),
|
|
39
|
+
})
|
|
40
|
+
)
|
|
41
|
+
} finally {
|
|
42
|
+
await client.unbind()
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('Should return broken jpegPhoto attribute (no attribute selection nor ;binary) - But it really depends on LDAP server, it is not always true. Sometimes a buffer is returned directly.', async () => {
|
|
47
|
+
let user = await authenticate({
|
|
48
|
+
...baseOptions,
|
|
49
|
+
username: 'gauss',
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
expect(user).toBeTruthy()
|
|
53
|
+
expect(user.uid).toEqual('gauss')
|
|
54
|
+
expect(user.sn).toEqual('Bar1')
|
|
55
|
+
expect(typeof user.uidNumber === 'string').toBe(true)
|
|
56
|
+
expect(user.uidNumber).toEqual('1000')
|
|
57
|
+
|
|
58
|
+
expect(user.jpegPhoto).toBeDefined()
|
|
59
|
+
// some ldap server returns a string, some ldap server returns a buffer
|
|
60
|
+
expect(
|
|
61
|
+
typeof user.jpegPhoto === 'string' || Buffer.isBuffer(user.jpegPhoto)
|
|
62
|
+
).toBe(true)
|
|
63
|
+
if (typeof user.jpegPhoto === 'string') {
|
|
64
|
+
expect(user.jpegPhoto).not.toEqual(jpegPhotoBase64)
|
|
65
|
+
}
|
|
66
|
+
if (Buffer.isBuffer(user.jpegPhoto)) {
|
|
67
|
+
expect(
|
|
68
|
+
user.jpegPhoto.equals(Buffer.from(jpegPhotoBase64, 'base64'))
|
|
69
|
+
).toBeTrue()
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('Should return nothing in the base64 jpegPhoto (using ;binary)', async () => {
|
|
74
|
+
let user = await authenticate({
|
|
75
|
+
...baseOptions,
|
|
76
|
+
username: 'gauss',
|
|
77
|
+
attributes: ['uid', 'sn', 'jpegPhoto;binary'],
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
expect(user).toBeTruthy()
|
|
81
|
+
expect(user.uid).toEqual('gauss')
|
|
82
|
+
expect(user.sn).toEqual('Bar1')
|
|
83
|
+
expect(user.cn).toBeUndefined()
|
|
84
|
+
|
|
85
|
+
expect(user.jpegPhoto).toBeUndefined()
|
|
86
|
+
|
|
87
|
+
expect(user['jpegPhoto;binary']).toBeDefined()
|
|
88
|
+
expect(Array.isArray(user['jpegPhoto;binary'])).toBe(true)
|
|
89
|
+
expect(user['jpegPhoto;binary'].length).toBe(0)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('Should return base64 jpegPhoto (using explicitBufferAttributes)', async () => {
|
|
93
|
+
let user = await authenticate({
|
|
94
|
+
...baseOptions,
|
|
95
|
+
username: 'gauss',
|
|
96
|
+
attributes: ['uid', 'sn', 'jpegPhoto'],
|
|
97
|
+
explicitBufferAttributes: ['jpegPhoto'],
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
expect(user).toBeTruthy()
|
|
101
|
+
expect(user.uid).toEqual('gauss')
|
|
102
|
+
expect(user.sn).toEqual('Bar1')
|
|
103
|
+
expect(user.cn).toBeUndefined()
|
|
104
|
+
|
|
105
|
+
expect(user['jpegPhoto;binary']).toBeUndefined()
|
|
106
|
+
|
|
107
|
+
expect(user.jpegPhoto).toBeDefined()
|
|
108
|
+
expect(typeof user.jpegPhoto === 'string').toBe(true)
|
|
109
|
+
expect(user.jpegPhoto).toEqual(jpegPhotoBase64)
|
|
110
|
+
|
|
111
|
+
const buffer = Buffer.from(user.jpegPhoto, 'base64')
|
|
112
|
+
expect(buffer).toBeDefined()
|
|
113
|
+
expect(buffer.length).toBeGreaterThan(0)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const { authenticate, LdapAuthenticationError } = require('../index.js')
|
|
2
|
+
|
|
3
|
+
const url = process.env.INGITHUB ? 'ldap://localhost:1389' : 'ldap://ldap:1389'
|
|
4
|
+
|
|
5
|
+
describe('ldap-authentication StartTLS and TLS options test', () => {
|
|
6
|
+
it('Plain LDAP with tlsOptions in ldapOpts should work (ldap:// protocol)', async () => {
|
|
7
|
+
// Regression test: Before fix, having tlsOptions with ldap:// URL caused issues
|
|
8
|
+
// After fix: tlsOptions are properly excluded from Client constructor for ldap:// URLs
|
|
9
|
+
let options = {
|
|
10
|
+
ldapOpts: {
|
|
11
|
+
url: url,
|
|
12
|
+
tlsOptions: {
|
|
13
|
+
rejectUnauthorized: false,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
adminDn: 'cn=read-only-admin,dc=example,dc=com',
|
|
17
|
+
adminPassword: 'password',
|
|
18
|
+
userPassword: 'password',
|
|
19
|
+
userSearchBase: 'dc=example,dc=com',
|
|
20
|
+
usernameAttribute: 'uid',
|
|
21
|
+
username: 'gauss',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let user = await authenticate(options)
|
|
25
|
+
expect(user).toBeTruthy()
|
|
26
|
+
expect(user.uid).toEqual('gauss')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('Use an admin user to authenticate with StartTLS (may skip if TLS not configured)', async () => {
|
|
30
|
+
// Note: This test may not fully succeed if the LDAP server lacks TLS certificates
|
|
31
|
+
// However, it should NOT fail with the original ECONNRESET bug
|
|
32
|
+
let options = {
|
|
33
|
+
ldapOpts: {
|
|
34
|
+
url: url,
|
|
35
|
+
tlsOptions: {
|
|
36
|
+
rejectUnauthorized: false,
|
|
37
|
+
minVersion: 'TLSv1.2',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
starttls: true,
|
|
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
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
let user = await authenticate(options)
|
|
51
|
+
// If this succeeds, StartTLS is fully working!
|
|
52
|
+
expect(user).toBeTruthy()
|
|
53
|
+
expect(user.uid).toEqual('gauss')
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// Expected if StartTLS is not configured on the server
|
|
56
|
+
// The critical check: should NOT be the original ECONNRESET bug
|
|
57
|
+
if (error.code === 'ECONNRESET' &&
|
|
58
|
+
error.message && error.message.includes('Client network socket disconnected before secure TLS connection')) {
|
|
59
|
+
fail('ECONNRESET bug detected: tlsOptions should NOT be passed to Client constructor when using ldap:// URL')
|
|
60
|
+
}
|
|
61
|
+
// Other errors are acceptable (e.g., server doesn't support StartTLS)
|
|
62
|
+
expect(error).toBeTruthy()
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('Use a regular user to authenticate with StartTLS (self mode)', async () => {
|
|
67
|
+
let options = {
|
|
68
|
+
ldapOpts: {
|
|
69
|
+
url: url,
|
|
70
|
+
tlsOptions: {
|
|
71
|
+
rejectUnauthorized: false,
|
|
72
|
+
minVersion: 'TLSv1.2',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
starttls: true,
|
|
76
|
+
userDn: 'cn=einstein,ou=users,dc=example,dc=com',
|
|
77
|
+
userPassword: 'password',
|
|
78
|
+
userSearchBase: 'dc=example,dc=com',
|
|
79
|
+
usernameAttribute: 'uid',
|
|
80
|
+
username: 'einstein',
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
let user = await authenticate(options)
|
|
85
|
+
expect(user).toBeTruthy()
|
|
86
|
+
expect(user.uid).toEqual('einstein')
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error.code === 'ECONNRESET' &&
|
|
89
|
+
error.message && error.message.includes('Client network socket disconnected before secure TLS connection')) {
|
|
90
|
+
fail('ECONNRESET bug detected: tlsOptions should NOT be passed to Client constructor when using ldap:// URL')
|
|
91
|
+
}
|
|
92
|
+
expect(error).toBeTruthy()
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('Verify user exists with StartTLS', async () => {
|
|
97
|
+
let options = {
|
|
98
|
+
ldapOpts: {
|
|
99
|
+
url: url,
|
|
100
|
+
tlsOptions: {
|
|
101
|
+
rejectUnauthorized: false,
|
|
102
|
+
minVersion: 'TLSv1.2',
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
starttls: true,
|
|
106
|
+
adminDn: 'cn=read-only-admin,dc=example,dc=com',
|
|
107
|
+
adminPassword: 'password',
|
|
108
|
+
verifyUserExists: true,
|
|
109
|
+
userSearchBase: 'dc=example,dc=com',
|
|
110
|
+
usernameAttribute: 'uid',
|
|
111
|
+
username: 'gauss',
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
let user = await authenticate(options)
|
|
116
|
+
expect(user).toBeTruthy()
|
|
117
|
+
expect(user.uid).toEqual('gauss')
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error.code === 'ECONNRESET' &&
|
|
120
|
+
error.message && error.message.includes('Client network socket disconnected before secure TLS connection')) {
|
|
121
|
+
fail('ECONNRESET bug detected: tlsOptions should NOT be passed to Client constructor when using ldap:// URL')
|
|
122
|
+
}
|
|
123
|
+
expect(error).toBeTruthy()
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
})
|
package/test/test.spec.js
CHANGED
|
@@ -144,7 +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].dn).toEqual('cn=科学A部,ou=
|
|
147
|
+
expect(user.groups[0].dn).toEqual('cn=科学A部,ou=groups,dc=example,dc=com')
|
|
148
148
|
})
|
|
149
149
|
it('Use regular user to authenticate and fetch user group information', async () => {
|
|
150
150
|
let options = {
|
|
@@ -165,10 +165,10 @@ describe('ldap-authentication test', () => {
|
|
|
165
165
|
let user = await authenticate(options)
|
|
166
166
|
expect(user).toBeTruthy()
|
|
167
167
|
expect(user.groups.length).toBeGreaterThan(0)
|
|
168
|
-
expect(user.groups[0].dn).toEqual('cn=科学A部,ou=
|
|
168
|
+
expect(user.groups[0].dn).toEqual('cn=科学A部,ou=groups,dc=example,dc=com')
|
|
169
169
|
// backward compatible with 3.2
|
|
170
170
|
expect(user.groups[0].objectName).toEqual(
|
|
171
|
-
'cn=科学A部,ou=
|
|
171
|
+
'cn=科学A部,ou=groups,dc=example,dc=com'
|
|
172
172
|
)
|
|
173
173
|
})
|
|
174
174
|
it('Not specifying groupMemberAttribute or groupMemberUserAttribute should not cause an error and fallback to default values', async () => {
|