This article originally described a custom LDAP-filter extension for Ninox On-Premise 3.12.8 and 3.13.0 in July 2024. It has now been extended with a hybrid authentication pattern for Active Directory and existing local accounts. The examples below use fictional configuration values.
Updated: 4 November 2026. These are custom server-code extensions, not built-in Ninox configuration switches. File names, callback contracts and session handling must be checked against the release you actually use.
Custom LDAP filters: the original approach
A successful directory password check does not necessarily mean that an account should be allowed to use a portal. An additional LDAP filter can restrict access to a designated organisational unit and an explicit directory attribute. The node-activedirectory package used in the original implementation supports custom search filters.
The following configuration fragment illustrates that distinction. Merge a reviewed fragment into your existing server-config.json; do not replace unrelated settings. The attribute value portal-enabled is only an example of an organisation-defined access marker.
{
"adds": {
"url": "ldaps://directory.example.com:636",
"baseDN": "OU=PortalUsers,DC=example,DC=com",
"filter": "(&(objectClass=user)(|(sAMAccountName=${userPrincipalName})(userPrincipalName=${userPrincipalName}))(extensionAttribute3=portal-enabled))"
}
}
Escape values before building an LDAP filter
A username must not be inserted into a filter as an unescaped expression. RFC 4515 defines escaping for LDAP filter assertion values, including the special ASCII characters handled below. This helper is intended for ordinary, valid JavaScript text input, not arbitrary binary data.
function escapeLdapFilterValue(value) {
return String(value).replace(/[\0()*\\]/g, character =>
"\\" + character.charCodeAt(0).toString(16).padStart(2, "0")
);
}
const ldapOptions = addsConfig.filter ? {
filter: addsConfig.filter
.split("${userPrincipalName}")
.join(escapeLdapFilterValue(accountName)),
scope: "sub"
} : null;
// Apply these options in the installed version's existing AD lookup.
// Validate the configured filter and handle search errors explicitly.
This replaces the raw string-substitution step in the earlier example. LDAP-value escaping is different from JSON escaping: a JSON parser converts \u0026 to &. Validate the parsed filter, rather than judging it only by how a JSON formatter displays the file.
Hybrid login: directory users and local external users
A shared portal may also need users who have no Active Directory account, such as an external contractor. Simply creating a local Ninox account does not guarantee that the normal login screen will use it when LDAP authentication is configured. In the login paths examined for this extension, directory failures did not automatically invoke the local verifier.
A reliable hybrid implementation separates three questions: Does the directory identity exist? Is that identity permitted to use the portal? Can the selected authentication provider validate the password?
| Directory result | Authentication path |
|---|---|
| Exactly one identity exists and passes the access filter | Use the existing LDAP password check. |
| An identity exists but is excluded, disabled, locked or has an incorrect directory password | Reject the login. Do not try a local password. |
| A complete, successful search finds no directory identity | Check an existing local Ninox account with the existing local password verifier. |
| The directory is unavailable, the search is incomplete, or results are ambiguous | Reject the attempt without local fallback. |
Use a separate reader for the identity precheck
The precheck cannot depend on successfully binding with the password of a person who may not exist in the directory. Use a dedicated directory-reading account with the required visibility and no administrative role. Use encrypted LDAP transport with certificate validation and bounded connection and search timeouts.
Search for identity existence across the relevant directory naming context, independently of the narrower portal-access filter. Otherwise, an account outside the allowed organisational unit or without the required marker could be mistaken for a local external user. Only after finding an identity should the implementation apply the existing access scope and filter, and confirm that it refers to the same account.
The identity search can consider the configured login attributes and relevant mail aliases to avoid misclassifying an existing directory account. A deliverable SMTP address is not necessarily the user’s directory login name. Confirm the actual login identity instead of silently treating every alias as a valid username. The reader must be able to see all relevant accounts; referrals, partial results and insufficient visibility must not be interpreted as proof of absence.
An illustrative configuration for a custom hybrid module might look like this. The additional hybrid keys have no effect unless matching server code has been installed and wired into the actual login handlers. The password-file location is fictional; keep the real file outside publicly served directories and restrict access to the service account and authorised administrators.
{
"adds": {
"hybrid": {
"enabled": false,
"lookup": {
"url": "ldaps://directory.example.com:636",
"baseDN": "DC=example,DC=com",
"bindDN": "svc-directory-read@example.com",
"passwordFile": "C:/ProgramData/Example/NinoxAuth/ldap-reader.json",
"timeoutMs": 5000
}
}
}
}
Keep credentials out of browser responses, client-side settings, logs and published configuration examples. A private file reference also keeps the actual reader password separate from a configuration object that might be reused in a public view.
Route to the existing verifier, not a second account system
The following is an integration sketch, not a drop-in replacement for a Ninox source file. Its adapter functions must be implemented for the installed release. A real implementation must distinguish complete negative searches from errors, enforce the configured eligibility rule, and call its callback only once.
function hybridLogin(accountName, password, callback) {
lookupDirectoryIdentity(accountName, (error, matches) => {
if (error || !Array.isArray(matches) || matches.length > 1) {
return callback(authenticationUnavailable());
}
if (matches.length === 0) {
return localLoginExistingAccount(accountName, password, callback);
}
verifyPortalAccess(matches[0], accountName, (error, allowed) => {
if (error) return callback(authenticationUnavailable());
if (!allowed) return callback(authenticationDenied());
return directoryLogin(accountName, password, callback);
});
});
}
Reuse Ninox’s existing local password and account checks, including disabled-account and lockout handling. Reuse the installed version’s session creation and response format. Do not return a fabricated AD user object for a local login, and do not automatically create a local account merely because an arbitrary login name is absent from AD.
Authentication and workspace authorisation remain separate. Existing local users retain their explicitly assigned workspaces and roles. A local fallback must not inherit the automatic workspace assignment intended for newly provisioned directory accounts. Check every actively used login handler; changing a helper alone is not enough if its caller always expects an AD user.
Testing and maintaining the extension after upgrades
- Verify a permitted directory login and an existing local external login through the actual portal interface.
- Verify rejection of a wrong directory password, a directory account without portal permission, a wrong local password and an unknown local account.
- Test directory unavailability, timeouts, ambiguous identities and incomplete searches in a controlled environment. None should select local fallback.
- Confirm that existing user identities, workspace memberships and roles remain unchanged.
- Record the installed Ninox version, custom extension revision, affected files, source fingerprints and rollback procedure. Keep configuration and secrets in protected backups.
- Before a Ninox upgrade, compare the new vendor files with the recorded customisations. Reapply both the access-filter and hybrid-routing behaviour to the new interfaces, then repeat the active login tests before reopening access. Do not blindly restore an old complete source file over a newer release.
Keep the test scope aligned with the interfaces actually used in production. A retired HTML administration interface does not need to become an additional maintenance target; backend API routes still used by the current interface remain part of the authentication flow.
Conclusion
Custom LDAP filters and controlled hybrid routing address different needs: the filter limits which directory identities may enter the portal, while hybrid routing allows explicitly provisioned external accounts to use local authentication. The fallback is based on a confirmed absence of a directory identity, never simply on a failed LDAP login.
Need help adapting this pattern to your Ninox On-Premise installation? Contact Zag1 Productions.

