Next in the Multi Tenant Node-RED series is authentication.
If you are going to run a multi user environment one of the key features will be identifying which users are allowed to do what and where. We need to only allow users to access their specific instances of Node-RED.
Node-RED provides a couple of options, the simplest is just to include the username/password/permissions details directly in the settings.js
but this doesn’t allow for dynamic updates like adding/removing users or changing passwords.
// Securing Node-RED // ----------------- // To password protect the Node-RED editor and admin API, the following // property can be used. See http://nodered.org/docs/security.html for details. adminAuth: { type: "credentials", users: [{ username: "admin", password: "$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN.", permissions: "*" }] },
The documentation also explains how to use PassportJS strategies to authenticate against oAuth providers, meaning you can do things like have users sign in with their Twitter credentials or use an existing Single Sign On solution if you want.
And finally the documentation covers how to implement your own authentication plugin, which is what I’m going to cover in this post.
In the past I have built a version of this type of plugin that uses LDAP but in this case I’ll be using MongoDB. I’ll be using the same database that I used in the last post about building a storage plugin. I’m also going to use Mongoose to wrap the collections and I’ll be using the passport-local-mongoose
plugin to handle the password hashing.
const mongoose = require('mongoose'); const Schema = mongoose.Schema; const passportLocalMongoose = require('passport-local-mongoose'); const Users = new Schema({ appname: String, username: String, email: String, permissions: { type: String, default: '*' }, }); var options = { usernameUnique: true, saltlen: 12, keylen: 24, iterations: 901, encoding: 'base64' }; Users.plugin(passportLocalMongoose,options); Users.set('toObject', {getters: true}); Users.set('toJSON', {getters: true}); module.exports = mongoose.model('Users',Users);
We need the username
and permissions
fields for Node-RED, the appname
is the same as in the storage plugin to allow us to keep the details for multiple Node-RED instances in the same collection. I added the email
field as a way to contact the user if we need do something like a password reset. You can see that there is no password
field, this is all handled by the passport-local-mongoose
code, it injects salt
and hash
fields into the schema and adds methods like authenticate(password)
to the returned objects that will check a supplied password against the stored hash.
Required API
There are 3 function that need to be implemented for Node-RED
users(username)
This function just checks if a given user exists for this instance
users: function(username) { return new Promise(function (resolve, reject){ Users.findOne({appname: appname, username: username}, {username: 1, permissions: 1}) .then(user => { resolve({username:user.username, permissions:user.permissions); }) .catch(err => { reject(err) }) }); },
authenticate(username, password)
This does the actual checking of the supplied password against the database. It looks up the user with the username
and appname
and then has passport-local-mongo
check it against the hash in the database.
authenticate: function(username, password) { return new Promise(function(resolve, reject){ Users.findOne({appname: appname, username}) .then((user) => { user.authenticate(password, function(e,u,pe){ if (u) { resolve({username: u.username, permissions: u.permissions) } else { resolve(null); } }) }) .catch(err => { reject(err) }) }) },
default()
In this case we don’t want a default user so we just return a null
object.
default: function() { return Promise.resolve(null); }
Extra functions
setup(settings)
Since authentication plugins do not get the whole setting object passed in like the storage plugins we need include a setup()
function to allow details about the MongoDB to be passed in.
type: "credentials", setup: function(settings) { if (settings && settings.mongoURI) { appname = settings.appname; mongoose.connect(settings.mongoURI, mongoose_options) .catch(err => { throw err; }); } return this; },
Using the plugin
This is again similar to the storage plugin where an entry is made in the settings.js
file. The difference is that this time the settings object isn’t explicitly passed to the plugin so we need to include the call to setup in the entry.
... adminAuth: require('node-red-contrib-auth-mongodb').setup({ mongoURI: "mongodb://localhost/nodered", appname: "r1" })
How to add users to the database will be covered in a later post about managing the creation of new instances.
One thought on “Node-RED Authentication Plugin”