Create server API with Token

Hello
I will use my raspberry pi with nodered to create the Rest Api server.
But I want to create an auth token for my clients.

What is the simplest method?

The token is time-limited and linked to an id and password.

This tutorial covers the use of JWT for authentication and based on some credentials
(that most probably you will need to get and verify from a database) sign/create the token with the jsonwebtoken library using a Function node with the functionExternalModules option .

I haven't used any of the above (programming is just a hobby) ..
i just remember watching the video tutorial and i thought of recommending it to you.

The way i think you can incorporate this into node-red :

http in node (/login) > DB > Function node (create JWT) > http response node

http in node (/api) > Function node (verify JWT) > Data > http response node

I watched the video (a bit complicated because not on node-red), in your scroll:

http in node (/login) > DB > Function node (create JWT) > http response node

I want to know how I can do the DB? I have 5 users each with their own password.

Once the token is generated by JWT in the scroll:

http in node (/api) > Function node (verify JWT) > Data > http response node

How should I verify the token? I must have stored it in another DB?

Thank you

As i said i have zero experience in authorization and it is indeed a complicated topic.
but since nobody else jumped in .. i'll have a go :wink:

Lets see the part of signing (generating) a JWT if there is a match from your database
(by the way what db are you planning to use ?)

Based on the Traversy tutorial code

You could do something like the flow below

Where in the place of db placeholder you'll have your db (could be mysql, sqlite or whatever you choose)
In the example placeholder im just returning a user to simulate a db user match result.

After that in sign JWT we first check if there was a match from the db and then proceed to sign a JWT with the jsonwebtoken library and return the token. Else If there was no match we return Unauthorized 403 to the http response node.

[{"id":"7fdf168306547292","type":"function","z":"54efb553244c241f","name":"sign JWT","func":"// we have a user match from the db \nif (msg.payload.length == 1) {\n\n// the result from the db (should be a single element)\n    let user = {\n        id: msg.payload[0].id,\n        user: msg.payload[0].username\n    }   \n\n    jwt.sign({ user }, 'secretkey', { expiresIn: '30s' }, (err, token) => {\n        msg.payload = { token };\n    });\n\n}\n\nelse {\n    msg.payload = \"Unauthorized\"\n    msg.statusCode = \"403\"\n\n}\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[{"var":"jwt","module":"jsonwebtoken"}],"x":900,"y":1620,"wires":[["7ac97ab8768e5b00"]]},{"id":"9b8747bdc731f290","type":"http in","z":"54efb553244c241f","name":"","url":"/login","method":"post","upload":false,"swaggerDoc":"","x":290,"y":1620,"wires":[["6c2312a5ea74a554","1706dfda582cf6db"]]},{"id":"6c2312a5ea74a554","type":"debug","z":"54efb553244c241f","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":440,"y":1560,"wires":[]},{"id":"1706dfda582cf6db","type":"function","z":"54efb553244c241f","name":"prepare sql","func":"let username = msg.payload.username;\nlet password = msg.payload.password;\n\nmsg.topic = `SELECT * FROM UsersTable WHERE username='${username}' AND password='${password}'`\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":490,"y":1620,"wires":[["d163b1c99735e322","334908d97102b6df"]]},{"id":"d163b1c99735e322","type":"debug","z":"54efb553244c241f","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":640,"y":1560,"wires":[]},{"id":"334908d97102b6df","type":"function","z":"54efb553244c241f","name":"db placeholder","func":"// fake user match from db\nmsg.payload = [{ id: 123, username: \"user\", password: \"abc123\", email: \"my@email.com\" }]\n\n// fake no user match \n// msg.payload = []\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":700,"y":1620,"wires":[["7fdf168306547292"]]},{"id":"7ac97ab8768e5b00","type":"http response","z":"54efb553244c241f","name":"","statusCode":"","headers":{},"x":1050,"y":1620,"wires":[]}]

ps. From what i read, Basic authentication for the Http in nodes is provided by Node-red
You can read more about it here HTTP Node security

Thank you for making the effort to answer me without knowing too much about the subject.

For the DB I thought of starting from a TXT or table type file. Do you think this is possible?

Sorry, a bit late to this. But I thought I'd add an alternative that, while more complex to set up initially (because it adds other services), would most likely be the best solution over time. Being more scalable, potentially more resilient and almost certainly more secure.

That would be to use something like NGINX to proxy the authentication. You can easily integrate fully self-hosted authentication services or make use of 3rd-party services like Auth0 or Azure SSO, etc.

Personally, I really don't recommend trying to manage JWT's yourself. It is unlikely that you would be able to create something that is anything more than security theatre. Whatever you are using for the authentication should be responsible for managing the JWT's if that's what you are going to use.

Also worth remembering that JWT's are nothing more than a convenient way of passing some signed data. They are not, in themselves, a security feature. They are prone to data hijacking and, unless other security features are used, can easily be intercepted and used in a token replay attack.

1 Like

Hello

Here is what I put in place the time to have more information.

It works but I think is not perfectly secure. It already makes it possible to start from a concrete case in order to make improvements.

the code :

[{"id":"35ca008a.0c171","type":"http in","z":"ac43d9e4.19a498","name":"","url":"/api/ext/login","method":"get","upload":false,"swaggerDoc":"","x":170,"y":100,"wires":[["867facf5.ad684"]]},{"id":"aab568bf.0dc148","type":"http response","z":"ac43d9e4.19a498","name":"","statusCode":"","headers":{},"x":930,"y":100,"wires":[]},{"id":"867facf5.ad684","type":"node-red-contrib-httpauth","z":"ac43d9e4.19a498","name":"auth","file":"","cred":"","authType":"Basic","realm":"","username":"test","password":"test","hashed":false,"x":330,"y":100,"wires":[["ae17c39f.64851"]]},{"id":"5e9fac3d.9c5e94","type":"template","z":"ac43d9e4.19a498","name":"msg.token","field":"payload","fieldType":"msg","format":"json","syntax":"mustache","template":"{\n  \"token\": \"{{token}}\",\n  \"token_type\": \"Bearer\",\n  \"expires_in\": 3600\n}","x":770,"y":100,"wires":[["aab568bf.0dc148"]]},{"id":"ae17c39f.64851","type":"JsonWebToken","z":"ac43d9e4.19a498","name":"Token Generator","tokenconfig":"fb3102d9.ed775","x":520,"y":100,"wires":[["5e9fac3d.9c5e94","6a46c78f.a645c8","ba495019.16eed","3eac057e.bd6cca"]]},{"id":"6a46c78f.a645c8","type":"debug","z":"ac43d9e4.19a498","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":760,"y":40,"wires":[]},{"id":"f056012e.bbc16","type":"file","z":"ac43d9e4.19a498","name":"Token save","filename":"/home/pi/Documents/Auth/Token.txt","appendNewline":false,"createDir":true,"overwriteFile":"true","encoding":"none","x":970,"y":160,"wires":[[]]},{"id":"ba495019.16eed","type":"change","z":"ac43d9e4.19a498","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"\"Bearer \"& $$.token","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":780,"y":160,"wires":[["f056012e.bbc16"]]},{"id":"1fe0e6fc.e55579","type":"file","z":"ac43d9e4.19a498","name":"Token Delete","filename":"/home/pi/Documents/Auth/Token.txt","appendNewline":true,"createDir":false,"overwriteFile":"delete","encoding":"none","x":990,"y":220,"wires":[[]]},{"id":"3eac057e.bd6cca","type":"countdown","z":"ac43d9e4.19a498","name":"Deleted Token time","topic":"","payloadTimerStart":"","payloadTimerStartType":"nul","payloadTimerStop":"true","payloadTimerStopType":"bool","timer":"3600","resetWhileRunning":true,"setTimeToNewWhileRunning":true,"startCountdownOnControlMessage":false,"x":790,"y":220,"wires":[["1fe0e6fc.e55579"],[]]},{"id":"5d573b0e.398054","type":"http in","z":"ac43d9e4.19a498","name":"","url":"/api/ext/opendoor","method":"post","upload":false,"swaggerDoc":"","x":180,"y":420,"wires":[["900a57e6.b7f138"]]},{"id":"a78609f4.f5ac38","type":"http response","z":"ac43d9e4.19a498","name":"","statusCode":"200","headers":{},"x":1100,"y":400,"wires":[]},{"id":"c4af927d.390bf","type":"function","z":"ac43d9e4.19a498","name":"","func":"\nif(msg.auth==msg.payload)\n    return [msg,null];\nelse\n    return [null,msg];","outputs":2,"noerr":0,"initialize":"","finalize":"","libs":[],"x":760,"y":420,"wires":[["76f65b69.1eb134"],["4eea842.57cf87c"]]},{"id":"900a57e6.b7f138","type":"change","z":"ac43d9e4.19a498","name":"","rules":[{"t":"set","p":"auth","pt":"msg","to":"req.headers.authorization","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":390,"y":420,"wires":[["1ea89ada.41f955"]]},{"id":"52b6be0c.d3ed","type":"http response","z":"ac43d9e4.19a498","name":"","statusCode":"401","headers":{},"x":1100,"y":440,"wires":[]},{"id":"76f65b69.1eb134","type":"template","z":"ac43d9e4.19a498","name":"page","field":"payload","fieldType":"msg","format":"json","syntax":"mustache","template":"{\n   \"status\" : \"ok\"\n}","x":930,"y":400,"wires":[["a78609f4.f5ac38"]]},{"id":"4eea842.57cf87c","type":"template","z":"ac43d9e4.19a498","name":"page","field":"payload","fieldType":"msg","format":"json","syntax":"mustache","template":"{\n    \"status\" : \"Unauthorized\"\n}","x":930,"y":440,"wires":[["52b6be0c.d3ed"]]},{"id":"1ea89ada.41f955","type":"file in","z":"ac43d9e4.19a498","name":"Read Token","filename":"/home/pi/Documents/Auth/Token.txt","format":"utf8","chunk":false,"sendError":false,"encoding":"none","x":590,"y":420,"wires":[["c4af927d.390bf"]]},{"id":"fb3102d9.ed775","type":"JsonWebToken_config","name":"test","secret":"test"}]
1 Like

I was thinking of recommending a proxy for the authentication. Surely it would be more secure. The thing is that most of the more advanced features of Nginx seem to be included in nginx Plus which needs a yearly license ? Thats why i started reading about Kong Gateway which is based on nginx but with more free plugins/features. and we love our free and open-source software lol

@LinkView_Maze that looks like an interesting solution ..
that node-red-contrib-httpauth node is 7 years old! ... well ... if it works it works
I didnt install the missing nodes but i tried to understand the flow
As it is, you are saving a token in a file and then with the api request you see if it matches ==
How does that work with multiple users my friend ? :wink:
maybe im wrong but in some cases you will be trying to match the token of user2 with the saved token of user1 ?


I was playing around a bit with the JWT example

added an sqlite db with a Users table for the credential check

image

added the flow part for the API and the verification of the jwt token

Used the Postman software for testing

Reguest to https://<node-red-ip>:1880/login
with a JSON body { "username": "user1", "password": "abc123" }
returns the token for user1 (with test expiration 30 seconds)

{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJ1c2VyMSIsImlhdCI6MTY1NzE4ODI4NCwiZXhwIjoxNjU3MTg4MzQ0fQ.VI4uxgAUoQ9NoPv0YIKpXJQxiEw82zxj937CyjX"}

which can be used for the API requests with Bearer Token Authorization
https://<node-red-ip>:1880/api

Result :

image

After time-limit

image

Example Flow

[{"id":"7fdf168306547292","type":"function","z":"54efb553244c241f","name":"sign JWT","func":"// we have a user match from the db \nif (msg.payload.length == 1) {\n\n// the result from the db (should be a single element)\n    let user = {\n        id: msg.payload[0].id,\n        username: msg.payload[0].username\n    }   \n\n    jwt.sign( user , 'secretkey', { expiresIn: '30s' }, (err, token) => {\n        msg.payload = { token };\n    });\n\n}\n\nelse {\n    msg.payload = \"Unauthorized\"\n    msg.statusCode = \"403\"\n\n}\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[{"var":"jwt","module":"jsonwebtoken"}],"x":800,"y":1620,"wires":[["7ac97ab8768e5b00"]]},{"id":"9b8747bdc731f290","type":"http in","z":"54efb553244c241f","name":"","url":"/login","method":"post","upload":false,"swaggerDoc":"","x":190,"y":1620,"wires":[["6c2312a5ea74a554","1706dfda582cf6db"]]},{"id":"6c2312a5ea74a554","type":"debug","z":"54efb553244c241f","name":"debug 1","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":340,"y":1560,"wires":[]},{"id":"1706dfda582cf6db","type":"function","z":"54efb553244c241f","name":"prepare sql","func":"let username = msg.payload.username;\nlet password = msg.payload.password;\n\nmsg.topic = `SELECT * FROM Users WHERE username=$1 AND password=$2`\nmsg.payload = [username, password]  // sql params\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":390,"y":1620,"wires":[["d163b1c99735e322","d6a5e85c9ca7735d"]]},{"id":"d163b1c99735e322","type":"debug","z":"54efb553244c241f","name":"debug 2","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":540,"y":1560,"wires":[]},{"id":"7ac97ab8768e5b00","type":"http response","z":"54efb553244c241f","name":"","statusCode":"","headers":{},"x":950,"y":1620,"wires":[]},{"id":"41315abeb16b886a","type":"http in","z":"54efb553244c241f","name":"","url":"/api","method":"get","upload":false,"swaggerDoc":"","x":200,"y":1800,"wires":[["36d167c6a4ed9bdd","967236e3fb342e56"]]},{"id":"36d167c6a4ed9bdd","type":"function","z":"54efb553244c241f","name":"verify JWT","func":"// FORMAT OF TOKEN\n// Authorization: Bearer <access_token>\n\nconst bearerHeader = msg.req.headers['authorization'];\n// Check if bearer is undefined\nif (typeof bearerHeader !== 'undefined') {\n    // Split at the space\n    const bearerToken = bearerHeader.split(' ')[1];\n    // Log the token in Debug window\n    node.warn({bearerToken});\n\n    // Verify Token\n    jwt.verify(bearerToken, 'secretkey', (err, authData) => {\n        if (err) {\n            msg.statusCode = \"403\";\n            msg.payload = err;\n            node.send(msg)\n        } else {\n            ///// API fake data /////\n            msg.payload = {\n                data: 'This is the data',\n                user: authData\n            };\n            node.send(msg)\n        }\n    });\n\n\n} else {\n    // Forbidden\n    msg.payload = \"Unauthorized\"\n    msg.statusCode = \"403\";\n    node.send(msg)\n}\n\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[{"var":"jwt","module":"jsonwebtoken"}],"x":410,"y":1800,"wires":[["97b4bf6ee7c5794d","2ab0196e6418cd2a"]]},{"id":"97b4bf6ee7c5794d","type":"http response","z":"54efb553244c241f","name":"","statusCode":"","headers":{},"x":610,"y":1800,"wires":[]},{"id":"967236e3fb342e56","type":"debug","z":"54efb553244c241f","name":"debug 4","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":320,"y":1740,"wires":[]},{"id":"2ab0196e6418cd2a","type":"debug","z":"54efb553244c241f","name":"debug 5","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":520,"y":1740,"wires":[]},{"id":"d6a5e85c9ca7735d","type":"sqlite","z":"54efb553244c241f","mydb":"26cd2be8.1dfacc","sqlquery":"msg.topic","sql":"","name":"Sqlite","x":590,"y":1620,"wires":[["7fdf168306547292","5177c66563862a08"]]},{"id":"5177c66563862a08","type":"debug","z":"54efb553244c241f","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":780,"y":1560,"wires":[]},{"id":"26cd2be8.1dfacc","type":"sqlitedb","db":"c:\\Users\\User\\.node-red\\EnergyMeters.db","mode":"RWC"}]

[EDIT]
As Julian said .. additional layers of security may be needed depending on the application. Like :

  1. hashing the passwords so they are not saved as clear text in the database
  2. using https so the data are sent encrypted between the server and client
  3. reverse proxy (rate limiting the requests, ip restrictions etc)

Not really, though you might need a bit more creativity. There are plenty of tutorials on the web showing you how to do it.

When I get some time (ha!), I'll be doing another write up with some kind of OAuth2 authentication.

1 Like