Authenticate against LDAP in Node Red

In my application flow, I want to authenticate a user in an on-premise LDAP.

I have tried several contrib-nodes and passport and can't get it to work.

I have been able to successfully authenticate using node-red-contrib-nr-ldapauth and node-red-node-ldap, but both require a bind user, which is not configurable on each authentication, because it's a config node. I don't have a bind user from AD, and don't want to use my personal account, so would want to use the user credentials I am trying to authenticate, but it's not configurable.

I tried passport, and ldapauth-fork, and can't seem to make it work. All examples use express which I don't think will work within Node Red.

I already have a login page and and an entire application written in Node Red, but I can't get the authentication working.

Can anyone help me with real working examples in Node Red function nodes?

Thanks in advance.

If all you need to do is prove that the given username and password are correct, then you don't always need a binding username and pwd in order to do a search. If you are not interested in the user's groups or name or other LDAP properties, you can just bind to the LDAP server using the user/pwd of the user you want to authenticate.

e.g. binding to LDAP://domainController1.acme.local:389 with a user/pwd will tell you if it is valid or not. This actually binds to the 'rootDSE' - the root of the directory and should always be readable by an authenticated user.

If the user/pwd is incorrect, you'll get an error. We do this using Java from Linux in a Microsoft Active Directory environment. Note that I have only tested it with Active Directory, but it should work for other LDAP providers.

Out of curiosity, I created a function by extracting the relevant bits from node-red-node-ldap which uses ldapjs to connect.

Here is the simple function...

const LDAP = global.get('ldapjs');

// The LDAP server or AD Domain Controller
// Would need to add additional options here to support LDAPs and cert validation etc
//
var client = LDAP.createClient({ url: 'LDAP://dc1.acme.local:389' });


// IMPORTANT - NEVER ALLOW A BLANK PASSWORD to be used as some LDAP providers will 
// not return an error, but bind anonymously, returning 'success' which would
// be wrongly interpreted as the user having authenticated.
//
var username = "domain\\username";
var password = "pwd";
if(password.length === 0)
{
    msg.payload = "Authentication failed - blank password";
    return msg;
}

// Bind to the rootDSE to test the user's credentials...
//
client.bind(username, password, function(err) 
{ 
    if (err)
    {
	    node.warn("Auth failed:" + err);
	    msg.payload = "Authentication failed";
	}
	else
	{
	   msg.payload = "Authenticated";
	}
	
    node.send(msg);
    
});

Pre-reqs - you need to install ldapjs and add it as a module in your node-red settings.js. See the info here on how to add external modules.

Don't forget to install ldapjs (npm install ldapjs) from the right path (~/.node-red).

Here is what the settings.js file looks like for me:

functionGlobalContext: { ldapjs:require("ldapjs") },

You'd need to make it bulletproof and modify it use TLS and parse input messages, change output messages etc.

FYI, the other option is to modify an existing node to take the incoming 'user to authenticate' as the binding user, but this may not always work - it depends on the permissions that the authenticating user has within your LDAP directory, which is why a lot of these libraries ask for a 'binding service account' up-front.

Be interested to know if this works for you and what LDAP provider you have (AD?)

Regards...

1 Like

It really makes me frustrated when other people make it look so easy. I'm not sure what I was doing wrong, or maybe I don't care anymore. I tried your code and it works beautifully and it's so simple. Thanks much!

1 Like

Node-RED uses Express internally so it is certainly possible to use it. How you use it depends on what you are trying to authenticate.

Thanks, but with the solution provided by @stew, I don't need to use Express at all.

Glad it helped, I might make a node for it if I ever get bored :slight_smile:

FYI, the LDAP stuff was easy when you know, but it took me the best part of an hour to figure out that I had to use node.send(msg) within the callback function instead of return msg at the end of the function. New to Javascript/Node.js and had no idea that the callback was asynchronous and that the function would run to its end without waiting!

Do you know if this async-ness is a Node.js behaviour or Javascript?

1 Like

I wrestled with that too. As I understand it, it's because the function is being called to handle the result of the client.bind or client.search, and not run inline with the sequential js. This is the only instance in my code so far that needed node.send.

Follow up question?

Using openSSL -showcerts in the Node Red docker container, I connect to a DNS name, let's say ldap.com:389, and I can bind (note no ldap://). This also works if I connect to ldap.com:636.

In my Node Red code, using ldapjs, I use ldap://ldap.com:389 and it works fine.

However, if I change the Node Red code to use ldaps://ldap.com:636 it times out. Out of desperation, I also tried ldap.com:636 (and get an error that I need the ldap:// or ldaps:// part). Neither ldaps:// nor ldap:// work.

Any suggestions?

Hi.

That is strange. Just tried from Windows 10 to an AD DC with LDAPS on port 636 and it works without changing any of the code, just using LDAPS://dc.local:636 in the URL.

However, this is a domain-joined machine with all the trusted cert chains installed locally.

Does it work for you with LDAPS using node-red-contrib-ldap (this uses ldapjs)? If it does, then you may need to modify the function to take a TLS cert that will be used to verify your LDAP server's identity and/or change other TLS options in ldapjs.

For example, it could be that the underlying Node.js TLS component is trying to verify the cert used by your LDAP server and times out for some reason.

Regards,
Stu

Asymc is an important part of JavaScript and Node.js is "just" server-side JavaScript. JavaScript basically operates on an implicit loop which enables async-like behaviour on a single thread. Wherever you see something labeled as a "callback" or "promise", you know that you are dealing with async behaviour.

1 Like

Stu, can you provide a link to the node-red-contrib-ldap node you're referring to? I can't find this exact one, but lots of similarly named ones.

It is the node-red-node-ldap node that you said you had got working in the first post - it is called node-red-contrib-ldap on Github.

https://flows.nodered.org/node/node-red-node-ldap

It might be the AD I'm connecting to, but I cannot use that node without a bind ID, so it works if I add my ID/pwd to the config node, but it doesn't accept a mustache template so I can bind with the logged in user's ID/pwd

Yes, that node requires a bind ID, the purpose of trying it was to see if it will authenticate using LDAPS for you.

  • Does it work with LDAPS? If so, how did you configure it? e.g. did you provide a cert?
  • What is the specific error coming back from the ldapjs function when you try LDAPS?

Something else to try as I came across a timeout issue - see if this updated function changes anything (added TLS option, a reconnect directive and a configurable timeout)...

 const LDAP = global.get('ldapjs');

// The LDAP server or AD Domain Controller
// Would need to add additional options here to support LDAPs and cert validation etc
//
var tlsOptions = {
    host: 'dc1.acme.local',
    port: '636',
    // Path your CA cert if you need one
   //  ca: [fs.readFileSync('./path/to/cert.pem')]
};

var client = LDAP.createClient({ url: 'LDAPS://dc1.acme.local:636',
                                    reconnect: true,
                                    tlsOptions: tlsOptions,
                                    ConnectTimeout: 10000 });


// IMPORTANT - NEVER ALLOW A BLANK PASSWORD to be used as some LDAP providers will 
// not return an error, but bind anonymously, returning 'success' which would
// be wrongly interpreted as the user having authenticated.
//
var username = "domain\username";
var password = "pwd";
if(password.length === 0)
{
    msg.payload = "Authentication failed - blank password";
    return msg;
}

// Bind to the rootDSE to test the user's credentials...
//
client.bind(username, password, function(err) 
{ 
    if (err)
    {
	    node.warn("Auth failed:" + err);
	    msg.payload = "Authentication failed";
	}
	else
	{
	   msg.payload = "Authenticated";
	}
	
    node.send(msg);
    
});