node-ad-tools
v1.2.1
Published
Basic Active Directory wrapper for ldapjs
Downloads
221
Readme
node-ad-tools
NodeJS Active Directory authentication and tools. - Requires ES6 support
This is a simple wrapper around ldapjs, which is a full ldap server & client. For custom or advanced setups please see https://github.com/joyent/node-ldapjs. This is highly opinionated and lacking in many features right now, but should work for simple AD authentication.
PR's that improve the project are welcomed, right now development is primarily on an as-needed basis.
API docs generated by jsdoc available here: https://tastypackets.github.io/node-ad-tools/
All binds and searchs are done using the credentials passed to the methods, that is why no credentials / service account is needed when creating the AD object.
Features
Current - v1.2.0
- Logins process is fully implemented using users credentials, no service account needed.
- Login with UPN, DN, and sAMAccountName - DN & sAMAccountName added in 1.2
- Retrieves user object and user's groups on login
- Can retrieve all groups in an OU and it's sub OUs (Using the root you can get all groups in AD)
- Can retrieve all users in an OU and it's sub OUs (Using the root you can get all users in AD)
- Can create user and group formatted objects with DN and GUID when retrieving all users and groups. - Added in 1.2
- Resolves GUID, which is a unique ID in AD given to objects. This can be used to keep track of users and groups even if the names are changed.
- Provides error message for user accounts that are locked out:
Account is locked out
- Override user search after bind for complex AD configurations - Added in 1.2
Planned features for 2.0.0
- User password reset
- Admin / service account password reset
- Add / remove user from groups
- Return group objects with DN/GUID for all user logins, this will help ensure consistency of group based permissions using GUID.
- Change functions to object parameters / destructuring
- Provide multiple ADs to be tried if one is unreachable
Important Notes
sAMAccountName can only bind with the domain name, if you want users to be able to type their sAMAccountName in the UPN format and still get a user object you will need to pre-process this string or provide a customSearch object. For example lets say we have a username with a sAMAccountName of test
and the domain of test.local
, but the user has a UPN of [email protected]
.
These options will work by default with no changes:
- Login with
test\test
- Login with
[email protected]
- Login with
CN=Test,OU=Users,DC=test,DC=local
In order to let the user login with [email protected]
we would need to convert the string to test\test
or we would need to provide a customSearch on the loginUser method. Originally this was being added to v1.2.0 as an auto-fallback if no user was located with UPN, however due to unknown possible security issues and lack of testing time I decided it'd be safe to leave this process up to the dev and not include the auto-fallback process in v1.2.0.
Here is an example of providing a customSearch to address the issue:
const username = '[email protected]';
const sam = username.split('@')[0];
const customSearch = { filter: `(sAMAccountName=${sam})` }
myAD.loginUser(username,'Welcome1',null , customSearch)
API
Install
yarn add node-ad-tools
Setup AD
The active directory class requires a basic configuration object that will inform ldapjs of the binding and searching parameters. This is configured once by creating a new ActiveDirectory object, if you need to change these settings dynamically you can construct the object right before performing the auth.
{
url: 'ldap://192.168.1.1',
base: 'dc=domain,dc=local',
searchOptions: {scope: 'sub'}, // Optional
idleTimeout: 3000, // Optional
tlsOptions: { rejectUnauthorized: false } // Optional
}
Full Example
const { ActiveDirectory } = require('node-ad-tools');
const myADConfig = {
url: 'ldap://192.168.1.1', // You can use DNS as well, like domain.local
base: 'dc=domain,dc=local'
}
const myAD = new ActiveDirectory(myADConfig);
myAD.loginUser('[email protected]','password')
.then(res => {
// If it failed to auth user find out why
if(!res.success) {
console.log(res.message);
return;
}
const user = ActiveDirectory.createUserObj(res.entry);
console.log(user);
})
.catch(err => console.error(err))
Both the class configuration and the methods that interact with Active Directory accept a base. The class one will be the default used if the base is not passed into specific methods. Here is an example of a base:
// This searches only in the Users OU inside example.local
myAD.loginUser('[email protected]','password','cn=Users,dc=example,dc=local')
Constructor Config Options
| Key | Type | Required | Description |
| --- | ---- | -------- | ----------- |
| url | String | Required | The url to the AD server, should start with ldap://
or ldaps://
|
| base | String | Required | AD base, example.local would be dc=example, dc=local
|
| searchOptions | Object | Optional | ldapjs searchOptions, defaults to scope: 'sub'
|
| idleTimeout | Number | Optional | How long to wait for response from AD before timing out |
| tlsOptions | Object | Optional | Node TLS options used when connecting using TLS. See Node TLS API for details about options. |
Methods
loginUser(username, password, base optional, customSearch optional)
This function takes a username and password and will return a Promise. The promise will only reject client connection issues, invalid authentication will still resolve the promise. This was done to make it easier to provide a different error or to try a 2ndry auth source easily. The success key is on all types of responses and should be used to verify if user was logged in. If success is false there will be 2 additional keys, message and error.
The param customSearch was added in v1.2.0 and allows you to override the search for the user object if the default process is not sufficient. To view all available options please look at ldapjs search options. For example you can modify the search by passing in a custom filter key.
If the bind is successful, but the method is unable to locate an account the res.entry will be undefined. This means the credentials passed are valid credentials, however the filter / AD was unable to locate a matching account. You will likely need to provide a customSearch in this case.
myAD.loginUser('[email protected]','password')
.then(res => {
// If it failed to auth user find out why
if(!res.success) {
console.log(res.message);
return;
}
const user = ActiveDirectory.createUserObj(res.entry);
console.log(user);
})
.catch(err => console.error(err))
Both resolve & reject will be in the following format
| Key | Returned | Type | Description |
| --- | -------- | ---- | ----------- |
| success | Always | boolean | Indicates if the login succeeded |
| entry | Situational | Object|Undefined | Entry is the ldapjs entry response |
| message | Situational | String | User friendly message from resolveBindError, only on success: false
|
| error | Situational | String | The original error generated, only on success: false
|
getAllGroups(username, password, base optional, detailed optional)
Look-up all the groups in active directory that the user can read, which is based on read permission configuration in active directory. All groups are returned in array of strings.
The detailed param was added in v1.2.0 and will create group objects for every group returned. These group objects contain additional useful information and in v2.0.0 will be on by default.
This is all groups the user can read, not just groups the user is a member of. Regular none-detailed groups
[
'Domain Users',
'Domain Guests',
'Group 1',
'Group 2'
]
Detailed groups
[
{
name: 'My Group',
dn: 'CN=My Group,CN=Users,DC=test,DC=local',
guid: 'a4f84d99-e0c8-4e60-87e3-53444fd6fe0a',
description: 'This is my test group!',
created: '2018-07-27T20:44:07.000Z', // JS Date Obj - This is not a string, I showed a string for demonstration
changed: '2019-02-27T16:39:18.000Z' // JS Date Obj
}
]
This function takes a username and password and will return a Promise. The promise will only reject client connection issues, invalid authentication will still resolve the promise. This was done to make it easier to provide a different error or to try a 2ndry auth source easily. The success key is on all types of responses and should be used to verify if user was logged in. If success is false there will be 2 additional keys, message and error.
myAD.getAllGroups('[email protected]','password')
.then(res => {
// If it failed to auth user find out why
if(!res.success) {
console.log(res.message);
return;
}
console.log(res.groups);
})
.catch(err => console.error(err))
Get all groups in detailed mode and provide no custom base example:
myAD.getAllGroups('[email protected]','password', true)
.then(res => {
// If it failed to auth user find out why
if(!res.success) {
console.log(res.message);
return;
}
console.log(res.groups);
})
.catch(err => console.error(err))
Both resolve & reject will be in the following format
| Key | Returned | Type | Description |
| --- | -------- | ---- | ----------- |
| success | Always | boolean | Indicates if the login succeeded |
| groups | Situational | Array | An array of all the groups the user has permissions to read in AD. |
| message | Situational | String | User friendly message from resolveBindError, only on success: false
|
| error | Situational | String | The original error generated, only on success: false
|
getAllUsers(username, password, base optional, formatted optional)
Look-up all the users in active directory that the user can read, which is based on read permission configuration in active directory. All user entry objects are returned in an array.
The formatted param was added in v1.2.0 and if set to true will convert all entries into user objects using the ActiveDirectory.createUserObj() method.
{
success: true,
users: [
// This is a valid entry just like login user and can be passed to createUserObj() method.
SearchEntry,
SearchEntry
]
}
Example with formatted set to true:
{
success: true,
users: [
{
groups: [ 'Staff', 'Users' ],
phone: '',
name: 'First User',
mail: '[email protected]',
guid: '579de45e-faf5-40f8-8eff-be2d76bd20d9',
dn: 'CN=First User,OU=Users,DC=test,DC=com'
},
{
groups: [ 'Staff', 'Users' ],
phone: '',
name: 'Second User',
mail: '[email protected]',
guid: '502de45e-faf9-83f8-8eff-be6d76bd20d5',
dn: 'CN=Second User,OU=Users,DC=test,DC=com'
}
]
}
This function takes a username and password and will return a Promise. The promise will only reject client connection issues, invalid authentication will still resolve the promise. This was done to make it easier to provide a different error or to try a 2ndry auth source easily. The success key is on all types of responses and should be used to verify if user was logged in. If success is false there will be 2 additional keys, message and error.
myAD.getAllUsers('[email protected]','password','cn=Users,dc=domain,dc=local', true) // Example of only searching Users OU inside the domain
.then(res => {
// If it failed to auth user find out why
if(!res.success) {
console.log(res.message);
return;
}
console.log(res.users);
})
.catch(err => console.error(err))
Both resolve & reject will be in the following format
| Key | Returned | Type | Description |
| --- | -------- | ---- | ----------- |
| success | Always | Boolean | Indicates if the login succeeded |
| Users | Situational | Array | An array of all the user entries the user has permissions to read in AD and match the base / scope. |
| message | Situational | String | User friendly message from resolveBindError, only on success: false
|
| error | Situational | String | The original error generated, only on success: false
|
createUserObj(entry)
Takes in the entry returned by ldapjs and creates a standardized user object. If you do not want to store all the users data it is recommended you extract the values you need from this object, because in the future there will likely be many more fields added to this. The first set of fields added were based on immediate needs.
If this does not have all the desired fields please feel free to add more in a PR or you can simply access them on the entry.objects or entry.attributes if you need the buffers.
The user DN was added to the user object in v1.2.0
const user = ActiveDirectory.resolveBindError(res.entry)
console.log(user) // {groups: [], phone: '', name: '', mail: '', guid: '', dn: ''}
Returns Object
| Returned | Type | Description | | -------- | ---- | ----------- | | groups | Array | An array of group name strings. This is the group names only, not the full AD location | | phone | String | Users phone number | | name | String | Users full name | | mail | String | Users email address | | guid | String | Unique AD key, this should be used to track and or link the user account to your app. |
resolveBindError(entry)
This function takes in the ldapjs errors and checks if it's due to invalid credentials or if the account is locked out. This does not check if an account is disabled, so it will still return as invalid credentials
const message = ActiveDirectory.resolveBindError(res.entry)
// Examples: Account is locked out, Invalid username or password, or Error resolving account.
resolveGUID(entry)
Takes in the entry returned by ldapjs and creates a GUID string. This should be used as your unique ID in your app or somehow used to link to a unique ID in your app. This will not change for the life of the object in AD, so even if the users name or email is changed this will stay the same.
const guid = ActiveDirectory.resolveGUID(res.entry)
// Example: 17d4e710-624d-4978-900b-8549cb753699
resolveGroups(entry)
Takes in the entry returned by ldapjs and creates an array of the users groups.
const guid = ActiveDirectory.resolveGroups(res.entry)
// Example: ['Group1', 'Group2']
Potential Issues
Sometimes ldapjs has issues with newer version of Node, please see ldapjs for any of these issues.