ldap-authentication 4.0.4 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.devcontainer/docker-compose.yml +7 -8
- package/.github/workflows/publish.yml +53 -0
- package/README.md +40 -1
- package/docker/ldap/10-ldap-test-data.ldif +47 -0
- package/docker/ldap/20-ldap-test-acl.ldif +21 -0
- package/docker/ldap/Dockerfile +7 -0
- package/docker-compose.yml +10 -8
- package/index.d.ts +29 -0
- package/index.js +124 -6
- package/index.mjs +17 -0
- package/package.json +10 -1
- package/test/fetch-users.spec.js +136 -0
|
@@ -8,14 +8,13 @@ services:
|
|
|
8
8
|
command: sleep infinity
|
|
9
9
|
|
|
10
10
|
ldap:
|
|
11
|
-
|
|
11
|
+
build:
|
|
12
|
+
context: ..
|
|
13
|
+
dockerfile: docker/ldap/Dockerfile
|
|
12
14
|
ports:
|
|
13
|
-
- '1389:
|
|
14
|
-
- '1636:
|
|
15
|
+
- '1389:389'
|
|
16
|
+
- '1636:636'
|
|
15
17
|
environment:
|
|
16
|
-
-
|
|
17
|
-
- LDAP_ADMIN_USERNAME=read-only-admin
|
|
18
|
+
- LDAP_DOMAIN=example.com
|
|
18
19
|
- LDAP_ADMIN_PASSWORD=password
|
|
19
|
-
-
|
|
20
|
-
- LDAP_PASSWORDS=password,password
|
|
21
|
-
- LDAP_GROUP=科学A部
|
|
20
|
+
- LDAP_TLS=true
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: 'Publish to npm'
|
|
2
|
+
|
|
3
|
+
env:
|
|
4
|
+
INGITHUB: true
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
release:
|
|
8
|
+
types: [created]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
name: 'Publish ${{ github.ref_name }}'
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v3
|
|
17
|
+
|
|
18
|
+
- name: Install LDAP tools
|
|
19
|
+
run: sudo apt-get install -y ldap-utils
|
|
20
|
+
|
|
21
|
+
- name: Start containers
|
|
22
|
+
run: docker compose -f "docker-compose.yml" up -d
|
|
23
|
+
|
|
24
|
+
- name: Wait for LDAP server to become available
|
|
25
|
+
uses: nick-fields/retry@v2
|
|
26
|
+
with:
|
|
27
|
+
timeout_minutes: 5
|
|
28
|
+
max_attempts: 10
|
|
29
|
+
polling_interval_seconds: 3
|
|
30
|
+
command: |
|
|
31
|
+
ldapsearch -LLL -o 'ldif-wrap=no' \
|
|
32
|
+
-H ldap://localhost:1389 \
|
|
33
|
+
-D 'cn=read-only-admin,dc=example,dc=com' \
|
|
34
|
+
-w password \
|
|
35
|
+
-b 'cn=einstein,ou=users,dc=example,dc=com' DN
|
|
36
|
+
|
|
37
|
+
- name: Use Node.js 22.x
|
|
38
|
+
uses: actions/setup-node@v3
|
|
39
|
+
with:
|
|
40
|
+
node-version: 22.x
|
|
41
|
+
registry-url: 'https://registry.npmjs.org'
|
|
42
|
+
|
|
43
|
+
- run: npm ci
|
|
44
|
+
- run: npm run test
|
|
45
|
+
|
|
46
|
+
- name: Stop containers
|
|
47
|
+
if: always()
|
|
48
|
+
run: docker compose -f "docker-compose.yml" down
|
|
49
|
+
|
|
50
|
+
- name: Publish to npm
|
|
51
|
+
run: npm publish
|
|
52
|
+
env:
|
|
53
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/README.md
CHANGED
|
@@ -25,7 +25,11 @@ This library use `ldapts` as the underneath library. It has three modes of authe
|
|
|
25
25
|
then does a search on the user and return the user's details.
|
|
26
26
|
|
|
27
27
|
3. **Verify user exists**. If an `verifyUserExists : true` is provided, the library will login (ldap bind) with the admin user,
|
|
28
|
-
|
|
28
|
+
then search for the user to be verified. If the user exists, user details will be returned (without verifying the user's password).
|
|
29
|
+
|
|
30
|
+
In addition, the `fetchUsers()` function can be used to fetch all users under a search base, using the admin
|
|
31
|
+
account to search (without any username or password of the individual users). The search always uses
|
|
32
|
+
LDAP paged results, so the common server-side limit of 1000 entries per search does not apply.
|
|
29
33
|
|
|
30
34
|
## Features
|
|
31
35
|
|
|
@@ -102,8 +106,32 @@ let authenticated = await authenticate({
|
|
|
102
106
|
})
|
|
103
107
|
```
|
|
104
108
|
|
|
109
|
+
#### Fetch all users (admin search, without user passwords)
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
const { fetchUsers } = require('ldap-authentication')
|
|
113
|
+
|
|
114
|
+
let users = await fetchUsers({
|
|
115
|
+
ldapOpts: { url: 'ldap://ldap.forumsys.com' },
|
|
116
|
+
adminDn: 'cn=read-only-admin,dc=example,dc=com',
|
|
117
|
+
adminPassword: 'password',
|
|
118
|
+
userSearchBase: 'dc=example,dc=com',
|
|
119
|
+
// userFilter: '(objectClass=person)', // default: (|(uid=*)(sAMAccountName=*))
|
|
120
|
+
// attributes: ['uid', 'sn', 'mail'], // omitted = all attributes
|
|
121
|
+
// pageSize: 500, // default: 1000
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
105
125
|
#### Complete example
|
|
106
126
|
|
|
127
|
+
The library works with both CommonJS and ES modules:
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
import { authenticate } from 'ldap-authentication'
|
|
131
|
+
// or
|
|
132
|
+
const { authenticate } = require('ldap-authentication')
|
|
133
|
+
```
|
|
134
|
+
|
|
107
135
|
```javascript
|
|
108
136
|
const { authenticate } = require('ldap-authentication')
|
|
109
137
|
|
|
@@ -204,9 +232,18 @@ auth()
|
|
|
204
232
|
if this value is not set, then authenticate will return true right after user bind succeed. No user details
|
|
205
233
|
from LDAP search will be performed and returned.
|
|
206
234
|
Example: `uid`
|
|
235
|
+
- `usernameFilter`: Prioritized alternative to usernameAttribute, allows you to provide a filter where `{{username}}` will
|
|
236
|
+
be replaced with the username provided
|
|
237
|
+
Example: `(|(uid={{username}})(mail={{username}}))`
|
|
207
238
|
- `username`: The username to authenticate with. It is used together with the name in `usernameAttribute`
|
|
208
239
|
to construct an ldap filter as `({attribute}={username})`
|
|
209
240
|
to find the user and get user details in LDAP. Example: `some user input`
|
|
241
|
+
- `userFilter`: (used by `fetchUsers()`) The ldap search filter to select the users to return.
|
|
242
|
+
By default it is `(|(uid=*)(sAMAccountName=*))`, which matches both POSIX (`uid`)
|
|
243
|
+
and Active Directory (`sAMAccountName`) users. Example: `'(objectClass=person)'`,
|
|
244
|
+
or `'(objectClass=*)'` to match everything
|
|
245
|
+
- `pageSize`: (used by `fetchUsers()`) The number of entries to fetch per page for the paged
|
|
246
|
+
search. Default: `1000`
|
|
210
247
|
- `attributes`: A list of attributes of a user details to be returned from the LDAP server.
|
|
211
248
|
If is set to `[]` or ommited, all details will be returned. Example: `['sn', 'cn']`
|
|
212
249
|
- `starttls`: Boolean. Use `STARTTLS` or not. When `true`, the connection will be upgraded to TLS
|
|
@@ -223,6 +260,8 @@ The user object if `authenticate()` is success.
|
|
|
223
260
|
|
|
224
261
|
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
262
|
|
|
263
|
+
`fetchUsers()` returns an array of user objects, one per matched LDAP entry (each with its `dn` and the returned attributes), or an empty array if no user matches the filter.
|
|
264
|
+
|
|
226
265
|
|
|
227
266
|
### AuthenticationResult Object
|
|
228
267
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Test directory seed data for ldap-authentication integration tests.
|
|
2
|
+
# Loaded by osixia/openldap at first start via
|
|
3
|
+
# /container/service/slapd/assets/config/bootstrap/ldif/custom
|
|
4
|
+
# (directory bind-mounted in docker-compose.yml).
|
|
5
|
+
# See https://github.com/osixia/container-openldap (v1, tag 1.5.0)
|
|
6
|
+
|
|
7
|
+
dn: ou=users,dc=example,dc=com
|
|
8
|
+
objectClass: organizationalUnit
|
|
9
|
+
ou: users
|
|
10
|
+
|
|
11
|
+
dn: ou=groups,dc=example,dc=com
|
|
12
|
+
objectClass: organizationalUnit
|
|
13
|
+
ou: groups
|
|
14
|
+
|
|
15
|
+
dn: cn=read-only-admin,dc=example,dc=com
|
|
16
|
+
objectClass: simpleSecurityObject
|
|
17
|
+
objectClass: organizationalRole
|
|
18
|
+
cn: read-only-admin
|
|
19
|
+
description: LDAP admin user for tests (password: password)
|
|
20
|
+
userPassword: {SSHA}u/l8yXCyiWhP3iXpt0UWMyM9ch5sZGFwLXRlc3QtMjAyNg==
|
|
21
|
+
|
|
22
|
+
dn: cn=gauss,ou=users,dc=example,dc=com
|
|
23
|
+
objectClass: inetOrgPerson
|
|
24
|
+
objectClass: posixAccount
|
|
25
|
+
cn: gauss
|
|
26
|
+
sn: Bar1
|
|
27
|
+
uid: gauss
|
|
28
|
+
uidNumber: 1000
|
|
29
|
+
gidNumber: 1000
|
|
30
|
+
homeDirectory: /home/gauss
|
|
31
|
+
userPassword: {SSHA}u/l8yXCyiWhP3iXpt0UWMyM9ch5sZGFwLXRlc3QtMjAyNg==
|
|
32
|
+
|
|
33
|
+
dn: cn=einstein,ou=users,dc=example,dc=com
|
|
34
|
+
objectClass: inetOrgPerson
|
|
35
|
+
objectClass: posixAccount
|
|
36
|
+
cn: einstein
|
|
37
|
+
sn: Bar2
|
|
38
|
+
uid: einstein
|
|
39
|
+
uidNumber: 1001
|
|
40
|
+
gidNumber: 1001
|
|
41
|
+
homeDirectory: /home/einstein
|
|
42
|
+
userPassword: {SSHA}u/l8yXCyiWhP3iXpt0UWMyM9ch5sZGFwLXRlc3QtMjAyNg==
|
|
43
|
+
|
|
44
|
+
dn: cn=科学A部,ou=groups,dc=example,dc=com
|
|
45
|
+
objectClass: groupOfNames
|
|
46
|
+
cn: 科学A部
|
|
47
|
+
member: cn=gauss,ou=users,dc=example,dc=com
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# ACLs for the test LDAP directory, applied on top of the osixia/openldap
|
|
2
|
+
# defaults (replaces the data database access rules at first start).
|
|
3
|
+
#
|
|
4
|
+
# The userPassword rule must come before the generic "to *" rule: OpenLDAP
|
|
5
|
+
# evaluates the first matching "to" clause for an attribute.
|
|
6
|
+
#
|
|
7
|
+
# - EXTERNAL (slapd local socket): full manage (used by the image bootstrap)
|
|
8
|
+
# - cn=admin,dc=example,dc=com: full manage
|
|
9
|
+
# - cn=read-only-admin,dc=example,dc=com: full manage (tests use it as the
|
|
10
|
+
# admin, including the entry modify in binary.spec.js)
|
|
11
|
+
# - normal users: read own entry and read group entries (group lookup tests)
|
|
12
|
+
# - anonymous: no access (simple bind does not consult other ACLs)
|
|
13
|
+
|
|
14
|
+
dn: olcDatabase={1}mdb,cn=config
|
|
15
|
+
changetype: modify
|
|
16
|
+
delete: olcAccess
|
|
17
|
+
-
|
|
18
|
+
add: olcAccess
|
|
19
|
+
olcAccess: to attrs=userPassword,shadowLastChange by self write by dn="cn=admin,dc=example,dc=com" manage by dn.exact="cn=read-only-admin,dc=example,dc=com" manage by anonymous auth by * none
|
|
20
|
+
olcAccess: to dn.subtree="ou=groups,dc=example,dc=com" by users read by dn.exact="cn=read-only-admin,dc=example,dc=com" read by dn="cn=admin,dc=example,dc=com" manage
|
|
21
|
+
olcAccess: to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by dn="cn=admin,dc=example,dc=com" manage by dn.exact="cn=read-only-admin,dc=example,dc=com" manage by users read by self read by * none
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
FROM osixia/openldap:1.5.0
|
|
2
|
+
|
|
3
|
+
# Seed the test directory (dc=example,dc=com) with the users, groups and
|
|
4
|
+
# admin account used by the integration tests, and grant the test admin
|
|
5
|
+
# (cn=read-only-admin) the rights it needs. Files are applied by the
|
|
6
|
+
# osixia/openldap bootstrap at the container's first start.
|
|
7
|
+
COPY docker/ldap/10-ldap-test-data.ldif docker/ldap/20-ldap-test-acl.ldif /container/service/slapd/assets/config/bootstrap/ldif/custom/
|
package/docker-compose.yml
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
services:
|
|
2
2
|
ldap:
|
|
3
|
-
|
|
3
|
+
# LDAP server used by the integration tests, based on osixia/openldap.
|
|
4
|
+
# The Dockerfile in docker/ldap/ seeds the directory with the users,
|
|
5
|
+
# groups and admin account the tests rely on.
|
|
6
|
+
build:
|
|
7
|
+
context: .
|
|
8
|
+
dockerfile: docker/ldap/Dockerfile
|
|
4
9
|
ports:
|
|
5
|
-
- '1389:
|
|
6
|
-
- '1636:
|
|
10
|
+
- '1389:389'
|
|
11
|
+
- '1636:636'
|
|
7
12
|
environment:
|
|
8
|
-
-
|
|
9
|
-
- LDAP_ADMIN_USERNAME=read-only-admin
|
|
13
|
+
- LDAP_DOMAIN=example.com
|
|
10
14
|
- LDAP_ADMIN_PASSWORD=password
|
|
11
|
-
-
|
|
12
|
-
- LDAP_PASSWORDS=password,password
|
|
13
|
-
- LDAP_GROUP=科学A部
|
|
15
|
+
- LDAP_TLS=true
|
package/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ declare module 'ldap-authentication' {
|
|
|
8
8
|
adminPassword?: string
|
|
9
9
|
userSearchBase?: string
|
|
10
10
|
usernameAttribute?: string
|
|
11
|
+
usernameFilter?:string
|
|
11
12
|
username?: string
|
|
12
13
|
verifyUserExists?: boolean
|
|
13
14
|
starttls?: boolean
|
|
@@ -36,9 +37,37 @@ declare module 'ldap-authentication' {
|
|
|
36
37
|
readonly client: any
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
export interface FetchUsersOptions {
|
|
41
|
+
ldapOpts: ClientOptions
|
|
42
|
+
adminDn: string
|
|
43
|
+
adminPassword: string
|
|
44
|
+
userSearchBase: string
|
|
45
|
+
/**
|
|
46
|
+
* LDAP search filter used to select the users to return.
|
|
47
|
+
* Defaults to `(|(uid=*)(sAMAccountName=*))`, which matches both
|
|
48
|
+
* POSIX (`uid`) and Active Directory (`sAMAccountName`) users.
|
|
49
|
+
* Example: `'(objectClass=person)'`, or `'(objectClass=*)'` to match everything.
|
|
50
|
+
*/
|
|
51
|
+
userFilter?: string
|
|
52
|
+
/** A list of attributes of the users to be returned from the LDAP server. If omitted, all details will be returned. */
|
|
53
|
+
attributes?: string[]
|
|
54
|
+
/** A list of attributes to be returned as base64-encoded strings. */
|
|
55
|
+
explicitBufferAttributes?: string[]
|
|
56
|
+
/** Number of entries to fetch per page for the paged search. Defaults to 1000. */
|
|
57
|
+
pageSize?: number
|
|
58
|
+
starttls?: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
39
61
|
export function authenticate(options: AuthenticationOptions): Promise<any>
|
|
40
62
|
export function authenticateResult(options: AuthenticationOptions): Promise<AuthenticationResult>
|
|
41
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Bind with the admin account and search all users under `userSearchBase`.
|
|
66
|
+
* The search always uses paged results, so results are not limited by the
|
|
67
|
+
* common server-side limit of 1000 entries.
|
|
68
|
+
*/
|
|
69
|
+
export function fetchUsers(options: FetchUsersOptions): Promise<any[]>
|
|
70
|
+
|
|
42
71
|
export class LdapAuthenticationError extends Error {
|
|
43
72
|
constructor(message: any)
|
|
44
73
|
name: string
|
package/index.js
CHANGED
|
@@ -45,6 +45,9 @@ const AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS = -2
|
|
|
45
45
|
const AUTH_RESULT_FAILURE_CREDENTIAL_INVALID = -3
|
|
46
46
|
const AUTH_RESULT_FAILURE_UNCATEGORIZED = -4
|
|
47
47
|
|
|
48
|
+
const DEFAULT_FETCH_USERS_FILTER = '(|(uid=*)(sAMAccountName=*))'
|
|
49
|
+
const DEFAULT_FETCH_USERS_PAGE_SIZE = 1000
|
|
50
|
+
|
|
48
51
|
class AuthenticationResult {
|
|
49
52
|
#authCode = AUTH_RESULT_FAILURE_UNCATEGORIZED
|
|
50
53
|
#identity
|
|
@@ -124,19 +127,30 @@ async function _ldapBind(dn, password, starttls, ldapOpts) {
|
|
|
124
127
|
return client
|
|
125
128
|
}
|
|
126
129
|
|
|
130
|
+
// replace username in filter
|
|
131
|
+
|
|
132
|
+
|
|
127
133
|
// search a user and return the object
|
|
128
134
|
async function _searchUser(
|
|
129
135
|
ldapClient,
|
|
130
136
|
searchBase,
|
|
137
|
+
usernameFilter,
|
|
131
138
|
usernameAttribute,
|
|
132
139
|
username,
|
|
133
140
|
attributes = null,
|
|
134
141
|
explicitBufferAttributes = null
|
|
135
142
|
) {
|
|
136
|
-
let filter
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
143
|
+
let filter;
|
|
144
|
+
if(usernameFilter){
|
|
145
|
+
filter = usernameFilter.replaceAll("{{username}}",username.replaceAll(/[&|!*()]/g,""));
|
|
146
|
+
}
|
|
147
|
+
else{
|
|
148
|
+
filter = new ldapts.EqualityFilter({
|
|
149
|
+
attribute: usernameAttribute,
|
|
150
|
+
value: username,
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
140
154
|
let searchOptions = {
|
|
141
155
|
filter: filter,
|
|
142
156
|
scope: 'sub',
|
|
@@ -262,10 +276,60 @@ async function _searchUserGroups(
|
|
|
262
276
|
return groups
|
|
263
277
|
}
|
|
264
278
|
|
|
279
|
+
// search all users under the search base and return the list of user objects
|
|
280
|
+
async function _fetchAllUsers(
|
|
281
|
+
ldapClient,
|
|
282
|
+
searchBase,
|
|
283
|
+
userFilter,
|
|
284
|
+
attributes = null,
|
|
285
|
+
explicitBufferAttributes = null,
|
|
286
|
+
pageSize = DEFAULT_FETCH_USERS_PAGE_SIZE
|
|
287
|
+
) {
|
|
288
|
+
let filter = userFilter || DEFAULT_FETCH_USERS_FILTER
|
|
289
|
+
|
|
290
|
+
let searchOptions = {
|
|
291
|
+
filter: filter,
|
|
292
|
+
scope: 'sub',
|
|
293
|
+
// always use paged results, so more than the usual server-side limit
|
|
294
|
+
// (usually 1000 entries per page) can be returned
|
|
295
|
+
paged: { pageSize: pageSize },
|
|
296
|
+
}
|
|
297
|
+
if (attributes) {
|
|
298
|
+
searchOptions.attributes = attributes
|
|
299
|
+
}
|
|
300
|
+
if (explicitBufferAttributes) {
|
|
301
|
+
searchOptions.explicitBufferAttributes = explicitBufferAttributes
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const { searchEntries } = await ldapClient.search(searchBase, searchOptions)
|
|
305
|
+
|
|
306
|
+
let users = searchEntries || []
|
|
307
|
+
// when attribute endwith ;binary, ldapts returns Buffer, we convert them into base64 string
|
|
308
|
+
for (let user of users) {
|
|
309
|
+
if (user != null && attributes != null) {
|
|
310
|
+
for (let attr of attributes) {
|
|
311
|
+
if (attr.endsWith(';binary') && Buffer.isBuffer(user[attr])) {
|
|
312
|
+
user[attr] = user[attr].toString('base64')
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// when attribute is one of the explicitBufferAttributes, should convert to base64 string
|
|
317
|
+
if (user != null && explicitBufferAttributes != null) {
|
|
318
|
+
for (let attr of explicitBufferAttributes) {
|
|
319
|
+
if (Buffer.isBuffer(user[attr])) {
|
|
320
|
+
user[attr] = user[attr].toString('base64')
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return users
|
|
326
|
+
}
|
|
327
|
+
|
|
265
328
|
async function authenticateWithAdmin(
|
|
266
329
|
adminDn,
|
|
267
330
|
adminPassword,
|
|
268
331
|
userSearchBase,
|
|
332
|
+
usernameFilter,
|
|
269
333
|
usernameAttribute,
|
|
270
334
|
username,
|
|
271
335
|
userPassword,
|
|
@@ -303,6 +367,7 @@ async function authenticateWithAdmin(
|
|
|
303
367
|
let searchResult = await _searchUser(
|
|
304
368
|
ldapAdminClient,
|
|
305
369
|
userSearchBase,
|
|
370
|
+
usernameFilter,
|
|
306
371
|
usernameAttribute,
|
|
307
372
|
username,
|
|
308
373
|
attributes,
|
|
@@ -368,6 +433,7 @@ async function authenticateWithAdmin(
|
|
|
368
433
|
async function authenticateWithUser(
|
|
369
434
|
userDn,
|
|
370
435
|
userSearchBase,
|
|
436
|
+
usernameFilter,
|
|
371
437
|
usernameAttribute,
|
|
372
438
|
username,
|
|
373
439
|
userPassword,
|
|
@@ -411,6 +477,7 @@ async function authenticateWithUser(
|
|
|
411
477
|
let searchResult = await _searchUser(
|
|
412
478
|
ldapUserClient,
|
|
413
479
|
userSearchBase,
|
|
480
|
+
usernameFilter,
|
|
414
481
|
usernameAttribute,
|
|
415
482
|
username,
|
|
416
483
|
attributes,
|
|
@@ -463,6 +530,7 @@ async function verifyUserExists(
|
|
|
463
530
|
adminDn,
|
|
464
531
|
adminPassword,
|
|
465
532
|
userSearchBase,
|
|
533
|
+
usernameFilter,
|
|
466
534
|
usernameAttribute,
|
|
467
535
|
username,
|
|
468
536
|
starttls,
|
|
@@ -498,6 +566,7 @@ async function verifyUserExists(
|
|
|
498
566
|
let searchResult = await _searchUser(
|
|
499
567
|
ldapAdminClient,
|
|
500
568
|
userSearchBase,
|
|
569
|
+
usernameFilter,
|
|
501
570
|
usernameAttribute,
|
|
502
571
|
username,
|
|
503
572
|
attributes,
|
|
@@ -544,6 +613,51 @@ async function verifyUserExists(
|
|
|
544
613
|
)
|
|
545
614
|
}
|
|
546
615
|
|
|
616
|
+
// fetch all users under the search base, using the admin account to search.
|
|
617
|
+
// the search always uses paged results so the common server-side limit of
|
|
618
|
+
// 1000 entries does not apply.
|
|
619
|
+
async function fetchUsers(options) {
|
|
620
|
+
assert(
|
|
621
|
+
options.ldapOpts && options.ldapOpts.url,
|
|
622
|
+
'fetchUsers: ldapOpts.url must be provided'
|
|
623
|
+
)
|
|
624
|
+
assert(options.adminDn, 'fetchUsers: adminDn must be provided')
|
|
625
|
+
assert(options.adminPassword, 'fetchUsers: adminPassword must be provided')
|
|
626
|
+
assert(options.userSearchBase, 'fetchUsers: userSearchBase must be provided')
|
|
627
|
+
|
|
628
|
+
let ldapAdminClient
|
|
629
|
+
try {
|
|
630
|
+
ldapAdminClient = await _ldapBind(
|
|
631
|
+
options.adminDn,
|
|
632
|
+
options.adminPassword,
|
|
633
|
+
options.starttls,
|
|
634
|
+
options.ldapOpts
|
|
635
|
+
)
|
|
636
|
+
} catch (error) {
|
|
637
|
+
if (ldapAdminClient && ldapAdminClient.isConnected) {
|
|
638
|
+
await ldapAdminClient.unbind()
|
|
639
|
+
}
|
|
640
|
+
throw new LdapAuthenticationError(error.message || 'admin bind failed')
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
try {
|
|
644
|
+
return await _fetchAllUsers(
|
|
645
|
+
ldapAdminClient,
|
|
646
|
+
options.userSearchBase,
|
|
647
|
+
options.userFilter,
|
|
648
|
+
options.attributes,
|
|
649
|
+
options.explicitBufferAttributes,
|
|
650
|
+
options.pageSize || DEFAULT_FETCH_USERS_PAGE_SIZE
|
|
651
|
+
)
|
|
652
|
+
} catch (error) {
|
|
653
|
+
throw new LdapAuthenticationError(error.message || 'user search failed')
|
|
654
|
+
} finally {
|
|
655
|
+
if (ldapAdminClient && ldapAdminClient.isConnected) {
|
|
656
|
+
await ldapAdminClient.unbind()
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
547
661
|
async function authenticate(options) {
|
|
548
662
|
const result = await authenticateResult(options)
|
|
549
663
|
|
|
@@ -562,8 +676,8 @@ async function authenticateResult(options) {
|
|
|
562
676
|
assert(options.adminPassword, 'Admin mode adminPassword must be provided')
|
|
563
677
|
assert(options.userSearchBase, 'Admin mode userSearchBase must be provided')
|
|
564
678
|
assert(
|
|
565
|
-
options.usernameAttribute,
|
|
566
|
-
'Admin mode usernameAttribute must be provided'
|
|
679
|
+
options.usernameAttribute || options.usernameFilter,
|
|
680
|
+
'Admin mode usernameAttribute or usernameFilter must be provided'
|
|
567
681
|
)
|
|
568
682
|
assert(options.username, 'Admin mode username must be provided')
|
|
569
683
|
} else {
|
|
@@ -584,6 +698,7 @@ async function authenticateResult(options) {
|
|
|
584
698
|
options.adminDn,
|
|
585
699
|
options.adminPassword,
|
|
586
700
|
options.userSearchBase,
|
|
701
|
+
options.usernameFilter,
|
|
587
702
|
options.usernameAttribute,
|
|
588
703
|
options.username,
|
|
589
704
|
options.starttls,
|
|
@@ -607,6 +722,7 @@ async function authenticateResult(options) {
|
|
|
607
722
|
options.adminDn,
|
|
608
723
|
options.adminPassword,
|
|
609
724
|
options.userSearchBase,
|
|
725
|
+
options.usernameFilter,
|
|
610
726
|
options.usernameAttribute,
|
|
611
727
|
options.username,
|
|
612
728
|
options.userPassword,
|
|
@@ -625,6 +741,7 @@ async function authenticateResult(options) {
|
|
|
625
741
|
return await authenticateWithUser(
|
|
626
742
|
options.userDn,
|
|
627
743
|
options.userSearchBase,
|
|
744
|
+
options.usernameFilter,
|
|
628
745
|
options.usernameAttribute,
|
|
629
746
|
options.username,
|
|
630
747
|
options.userPassword,
|
|
@@ -666,6 +783,7 @@ module.exports.AuthenticationResult = AuthenticationResult
|
|
|
666
783
|
|
|
667
784
|
module.exports.authenticate = authenticate
|
|
668
785
|
module.exports.authenticateResult = authenticateResult
|
|
786
|
+
module.exports.fetchUsers = fetchUsers
|
|
669
787
|
module.exports.LdapAuthenticationError = LdapAuthenticationError
|
|
670
788
|
|
|
671
789
|
module.exports.exportForTesting = {
|
package/index.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import ldapAuthentication from './index.js'
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
AUTH_RESULT_FAILURE,
|
|
5
|
+
AUTH_RESULT_FAILURE_CREDENTIAL_INVALID,
|
|
6
|
+
AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS,
|
|
7
|
+
AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND,
|
|
8
|
+
AUTH_RESULT_FAILURE_UNCATEGORIZED,
|
|
9
|
+
AUTH_RESULT_SUCCESS,
|
|
10
|
+
AuthenticationResult,
|
|
11
|
+
LdapAuthenticationError,
|
|
12
|
+
authenticate,
|
|
13
|
+
authenticateResult,
|
|
14
|
+
fetchUsers,
|
|
15
|
+
} from './index.js'
|
|
16
|
+
|
|
17
|
+
export default ldapAuthentication
|
package/package.json
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ldap-authentication",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "A simple async nodejs library for LDAP user authentication",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "./index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./index.d.ts",
|
|
10
|
+
"import": "./index.mjs",
|
|
11
|
+
"require": "./index.js",
|
|
12
|
+
"default": "./index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
7
16
|
"engines": {
|
|
8
17
|
"node": ">=20.0.0"
|
|
9
18
|
},
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const { fetchUsers, LdapAuthenticationError } = require('../index.js')
|
|
2
|
+
|
|
3
|
+
const url = process.env.INGITHUB ? 'ldap://localhost:1389' : 'ldap://ldap:1389'
|
|
4
|
+
|
|
5
|
+
describe('ldap-authentication fetchUsers test', () => {
|
|
6
|
+
const baseOptions = {
|
|
7
|
+
ldapOpts: {
|
|
8
|
+
url: url,
|
|
9
|
+
},
|
|
10
|
+
adminDn: 'cn=read-only-admin,dc=example,dc=com',
|
|
11
|
+
adminPassword: 'password',
|
|
12
|
+
userSearchBase: 'dc=example,dc=com',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
it('Should return all users with the default filter', async () => {
|
|
16
|
+
let users = await fetchUsers(baseOptions)
|
|
17
|
+
|
|
18
|
+
expect(Array.isArray(users)).toBe(true)
|
|
19
|
+
expect(users.length).toBe(2)
|
|
20
|
+
let uids = users.map((user) => user.uid)
|
|
21
|
+
expect(uids).toContain('gauss')
|
|
22
|
+
expect(uids).toContain('einstein')
|
|
23
|
+
for (let user of users) {
|
|
24
|
+
expect(user.dn).toBeTruthy()
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('Should return only the requested attributes', async () => {
|
|
29
|
+
let users = await fetchUsers({
|
|
30
|
+
...baseOptions,
|
|
31
|
+
attributes: ['uid', 'sn'],
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
expect(users.length).toBe(2)
|
|
35
|
+
let gauss = users.find((user) => user.uid === 'gauss')
|
|
36
|
+
expect(gauss).toBeTruthy()
|
|
37
|
+
expect(gauss.sn).toEqual('Bar1')
|
|
38
|
+
expect(gauss.cn).toBeUndefined()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('Should return only matching users with a custom userFilter', async () => {
|
|
42
|
+
let users = await fetchUsers({
|
|
43
|
+
...baseOptions,
|
|
44
|
+
userFilter: '(uid=gauss)',
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
expect(users.length).toBe(1)
|
|
48
|
+
expect(users[0].uid).toEqual('gauss')
|
|
49
|
+
expect(users[0].sn).toEqual('Bar1')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('Should return all entries including non-users when userFilter matches everything', async () => {
|
|
53
|
+
let users = await fetchUsers({
|
|
54
|
+
...baseOptions,
|
|
55
|
+
userFilter: '(objectClass=*)',
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
expect(users.length).toBeGreaterThan(2)
|
|
59
|
+
let dns = users.map((user) => user.dn)
|
|
60
|
+
expect(dns).toContain('cn=gauss,ou=users,dc=example,dc=com')
|
|
61
|
+
expect(dns).toContain('cn=科学A部,ou=groups,dc=example,dc=com')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('Should return empty list when userFilter matches nothing', async () => {
|
|
65
|
+
let users = await fetchUsers({
|
|
66
|
+
...baseOptions,
|
|
67
|
+
userFilter: '(uid=does-not-exist)',
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
expect(users).toEqual([])
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('ldap-authentication fetchUsers negative test', () => {
|
|
75
|
+
const baseOptions = {
|
|
76
|
+
ldapOpts: {
|
|
77
|
+
url: url,
|
|
78
|
+
},
|
|
79
|
+
adminDn: 'cn=read-only-admin,dc=example,dc=com',
|
|
80
|
+
adminPassword: 'password',
|
|
81
|
+
userSearchBase: 'dc=example,dc=com',
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
it('wrong admin password should throw LdapAuthenticationError', async () => {
|
|
85
|
+
let options = {
|
|
86
|
+
...baseOptions,
|
|
87
|
+
adminPassword: 'wrongpassword',
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let e = null
|
|
91
|
+
try {
|
|
92
|
+
await fetchUsers(options)
|
|
93
|
+
} catch (error) {
|
|
94
|
+
e = error
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
expect(e).toBeTruthy()
|
|
98
|
+
expect(e).toBeInstanceOf(LdapAuthenticationError)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('wrong admin dn should throw LdapAuthenticationError', async () => {
|
|
102
|
+
let options = {
|
|
103
|
+
...baseOptions,
|
|
104
|
+
adminDn: 'cn=not-exist,dc=example,dc=com',
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let e = null
|
|
108
|
+
try {
|
|
109
|
+
await fetchUsers(options)
|
|
110
|
+
} catch (error) {
|
|
111
|
+
e = error
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
expect(e).toBeTruthy()
|
|
115
|
+
expect(e).toBeInstanceOf(LdapAuthenticationError)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('missing userSearchBase should throw', async () => {
|
|
119
|
+
let options = {
|
|
120
|
+
ldapOpts: {
|
|
121
|
+
url: url,
|
|
122
|
+
},
|
|
123
|
+
adminDn: 'cn=read-only-admin,dc=example,dc=com',
|
|
124
|
+
adminPassword: 'password',
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let e = null
|
|
128
|
+
try {
|
|
129
|
+
await fetchUsers(options)
|
|
130
|
+
} catch (error) {
|
|
131
|
+
e = error
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
expect(e).toBeTruthy()
|
|
135
|
+
})
|
|
136
|
+
})
|