Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,43 @@ async function auth() {
auth()
```

### Example with StartTLS

```javascript
import { authenticate } from 'ldap-authentication'

async function auth() {
// auth with admin
let options = {
ldapOpts: {
url: 'ldap://ldap.example.com',
tlsOptions: {
rejectUnauthorized: false, // For self-signed certificates
minVersion: 'TLSv1.2',
servername: 'ldap.example.com' // For SNI (Server Name Indication)
}
},
starttls: true, // Enable StartTLS
adminDn: 'cn=admin,dc=example,dc=com',
adminPassword: 'password',
userPassword: 'password',
userSearchBase: 'dc=example,dc=com',
usernameAttribute: 'uid',
username: 'testuser'
}

let user = await authenticate(options)
console.log(user)
}

auth()
```

**Important Notes for StartTLS:**
- Use `ldap://` URLs with `starttls: true` (not `ldaps://`)
- For `ldaps://` URLs, omit `starttls` and the connection will use TLS from the start
- TLS options like `rejectUnauthorized`, `minVersion`, and `servername` can be specified in `ldapOpts.tlsOptions`

## Parameters

- `ldapOpts`: This is passed to `ldapts` client directly
Expand All @@ -172,7 +209,9 @@ auth()
to find the user and get user details in LDAP. Example: `some user input`
- `attributes`: A list of attributes of a user details to be returned from the LDAP server.
If is set to `[]` or ommited, all details will be returned. Example: `['sn', 'cn']`
- `starttls`: Boolean. Use `STARTTLS` or not
- `starttls`: Boolean. Use `STARTTLS` or not. When `true`, the connection will be upgraded to TLS
using the STARTTLS extended operation. TLS options can be specified in `ldapOpts.tlsOptions`.
Note: Use `starttls: true` with `ldap://` URLs, not `ldaps://` URLs
- `groupsSearchBase`: if specified with groupClass, will serve as search base for authenticated user groups
- `groupClass`: if specified with groupsSearchBase, will be used as objectClass in search filter for authenticated user groups
- `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
Expand Down
20 changes: 19 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,25 @@ async function _ldapBind(dn, password, starttls, ldapOpts) {
// TODO: check if ldapts expects escaped dn or not (possible double escaping problems?)
dn = _ldapEscapeDN(dn)
ldapOpts.connectTimeout = ldapOpts.connectTimeout || 5000
let client = new ldapts.Client(ldapOpts)

// When using StartTLS, we need to exclude tlsOptions from the Client constructor
// and only pass them to the startTLS() method to avoid connection conflicts.
// According to ldapts documentation:
// - For LDAPS (ldaps://): pass tlsOptions to Client constructor
// - For StartTLS (ldap://): do NOT pass tlsOptions to Client constructor, only to startTLS()
// - For plain LDAP (ldap://): do NOT pass tlsOptions to Client constructor
let clientOpts = ldapOpts
const isLdaps = ldapOpts.url && ldapOpts.url.startsWith('ldaps://')

// Only pass tlsOptions to Client constructor if using ldaps:// protocol
// For ldap:// protocol (plain or StartTLS), exclude tlsOptions from constructor
if (!isLdaps && ldapOpts.tlsOptions) {
// Create a shallow copy of ldapOpts without tlsOptions for the Client constructor
const { tlsOptions, ...optsWithoutTls } = ldapOpts
clientOpts = optsWithoutTls
}

let client = new ldapts.Client(clientOpts)

if (starttls) {
await client.startTLS(ldapOpts.tlsOptions)
Expand Down
126 changes: 126 additions & 0 deletions test/starttls.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
const { authenticate, LdapAuthenticationError } = require('../index.js')

const url = process.env.INGITHUB ? 'ldap://localhost:1389' : 'ldap://ldap:1389'

describe('ldap-authentication StartTLS and TLS options test', () => {
it('Plain LDAP with tlsOptions in ldapOpts should work (ldap:// protocol)', async () => {
// Regression test: Before fix, having tlsOptions with ldap:// URL caused issues
// After fix: tlsOptions are properly excluded from Client constructor for ldap:// URLs
let options = {
ldapOpts: {
url: url,
tlsOptions: {
rejectUnauthorized: false,
},
},
adminDn: 'cn=read-only-admin,dc=example,dc=com',
adminPassword: 'password',
userPassword: 'password',
userSearchBase: 'dc=example,dc=com',
usernameAttribute: 'uid',
username: 'gauss',
}

let user = await authenticate(options)
expect(user).toBeTruthy()
expect(user.uid).toEqual('gauss')
})

it('Use an admin user to authenticate with StartTLS (may skip if TLS not configured)', async () => {
// Note: This test may not fully succeed if the LDAP server lacks TLS certificates
// However, it should NOT fail with the original ECONNRESET bug
let options = {
ldapOpts: {
url: url,
tlsOptions: {
rejectUnauthorized: false,
minVersion: 'TLSv1.2',
},
},
starttls: true,
adminDn: 'cn=read-only-admin,dc=example,dc=com',
adminPassword: 'password',
userPassword: 'password',
userSearchBase: 'dc=example,dc=com',
usernameAttribute: 'uid',
username: 'gauss',
}

try {
let user = await authenticate(options)
// If this succeeds, StartTLS is fully working!
expect(user).toBeTruthy()
expect(user.uid).toEqual('gauss')
} catch (error) {
// Expected if StartTLS is not configured on the server
// The critical check: should NOT be the original ECONNRESET bug
if (error.code === 'ECONNRESET' &&
error.message && error.message.includes('Client network socket disconnected before secure TLS connection')) {
fail('ECONNRESET bug detected: tlsOptions should NOT be passed to Client constructor when using ldap:// URL')
}
// Other errors are acceptable (e.g., server doesn't support StartTLS)
expect(error).toBeTruthy()
}
})

it('Use a regular user to authenticate with StartTLS (self mode)', async () => {
let options = {
ldapOpts: {
url: url,
tlsOptions: {
rejectUnauthorized: false,
minVersion: 'TLSv1.2',
},
},
starttls: true,
userDn: 'cn=einstein,ou=users,dc=example,dc=com',
userPassword: 'password',
userSearchBase: 'dc=example,dc=com',
usernameAttribute: 'uid',
username: 'einstein',
}

try {
let user = await authenticate(options)
expect(user).toBeTruthy()
expect(user.uid).toEqual('einstein')
} catch (error) {
if (error.code === 'ECONNRESET' &&
error.message && error.message.includes('Client network socket disconnected before secure TLS connection')) {
fail('ECONNRESET bug detected: tlsOptions should NOT be passed to Client constructor when using ldap:// URL')
}
expect(error).toBeTruthy()
}
})

it('Verify user exists with StartTLS', async () => {
let options = {
ldapOpts: {
url: url,
tlsOptions: {
rejectUnauthorized: false,
minVersion: 'TLSv1.2',
},
},
starttls: true,
adminDn: 'cn=read-only-admin,dc=example,dc=com',
adminPassword: 'password',
verifyUserExists: true,
userSearchBase: 'dc=example,dc=com',
usernameAttribute: 'uid',
username: 'gauss',
}

try {
let user = await authenticate(options)
expect(user).toBeTruthy()
expect(user.uid).toEqual('gauss')
} catch (error) {
if (error.code === 'ECONNRESET' &&
error.message && error.message.includes('Client network socket disconnected before secure TLS connection')) {
fail('ECONNRESET bug detected: tlsOptions should NOT be passed to Client constructor when using ldap:// URL')
}
expect(error).toBeTruthy()
}
})
})
Loading