Skip to content

Instantly share code, notes, and snippets.

@rozek
Last active October 19, 2021 07:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rozek/15039cf2ec1e5bbb984919baf655fa31 to your computer and use it in GitHub Desktop.
Save rozek/15039cf2ec1e5bbb984919baf655fa31 to your computer and use it in GitHub Desktop.
Basic HTTP Authorization

This flow provides "basic HTTP Authorization" for HTTP endpoints which are intended for certain users only.

This flow is part of a set of node-red-authorization-examples but also published here for easier lookup

Prerequisites

This example requires the following Node-RED extension:

Additionally, it expects the global flow context to contain an object called UserRegistry which has the same format as described in "node-red-within-express":

  • the object's property names are the ids of registered users
    user ids are strings with no specific format, they may be user names, email addresses or any other data you are free to choose - with two important exceptions: user ids must neither contain any slashes ("/") nor any colons (":") or the authentication mechanisms described below (and the user management described in node-red-user-management-example) will fail. Additionally, upper and lower case in user ids is not distinguished
  • the object's property values are JavaScript objects with the following properties, at least (additional properties may be added at will):
    • Roles
      is either missing or contains a list of strings with the user's roles. There is no specific format for role names except that a role name must not contain any white space
    • Salt
      is a string containing a random "salt" value which is used during PBKDF2 password hash calculation
    • Hash
      is a string containing the actual PBKDF2 hash of the user's password

When used outside "node-red-within-express", the following flows allow such a registry to be loaded from an external JSON file called registeredUsers.json (or to be created if no such file exists or an existing file can not be loaded) and written back after changes:

outside-node-red-within-express

These flows are already part of this example but may be removed (or customized) if not needed.

For testing and debugging purposes, the following flow may also be imported, which dumps the current contents of the user registry onto Node-RED's debug console when clicked:

show-user-registry

Which is also already part of this example.

Postman Collection

For testing purposes, the GitHub repository for this example additionally contains a Postman collection with a few predefined requests.

Basic HTTP Authorization

The simplest approach to user authentication and authorization is to utilize the "basic HTTP authentication" built into every browser: if a requested resource requires a client to authenticate, the browser itself opens a small dialog window where users may enter their name and password (or cancel the request). These credentials are then sent back to the server for validation - if the server still denies access, the dialog is presented again, allowing users to enter different credentials - otherwise the browser stores the successful entries internally and, from now on, automatically sends them along with every request.

Nota bene: user credentials (especially passwords) are sent in plain text form - for that reason, it is important to use secured connections (i.e., HTTPS) only

basic-auth

The upper output is used for successful authentications, the lower one for failures.

If you require the authenticating user to have a specific role, you may set msg.requiredRole to that role before invoking the basic auth flow - otherwise, user roles will not be checked.

Upon successful authentication, msg.authenticatedUser contains the id of the authenticated user and msg.authorizedRoles contains a (possibly empty) list with the roles of that user.

Try yourself

The following example illustrates how to integrate basic authentication into Node-RED flows:

try-basic-auth

The "basic HTTP authentication" procedure frees developers from having to design and implement their own login forms as the user interface is already built into the browser.

However, basic authentication lacks (implicit) expiration and explicit logout, making it very difficult to terminate an authenticated "session" or to change users: once correct credentials have been given, the browser always automatically attaches them to every request - unless a "private" window (or tab) is opened: in that case, the browser withdraws any given credentials as soon as the window (or tab) is closed.

Automated Tests

The GitHub repository for this example also contains some flows for automated tests.

[
{
"id": "b226928597254811",
"type": "comment",
"z": "2d76fa36687aa026",
"name": "basic HTTP authorization (w/o expiration)",
"info": "",
"x": 840,
"y": 40,
"wires": []
},
{
"id": "044b8567a46bcbc1",
"type": "function",
"z": "2d76fa36687aa026",
"name": "validate authorization",
"func": " let Credentials = msg.req.headers['authorization'] || ''\n if (! Credentials.startsWith('Basic')) {\n return withAuthorizationRequest()\n }\n\n Credentials = Credentials.replace(/^Basic\\s+/,'') // still Base64-encoded\n try {\n Credentials = (new Buffer(Credentials,'base64')).toString('utf8')\n } catch (Signal) { return withAuthorizationRequest() }\n\n let UserId = Credentials.replace(/:.*$/,'').trim().toLowerCase()\n let Password = Credentials.replace(/^[^:]+:/,'').trim()\n\n let UserRegistry = global.get('UserRegistry') || Object.create(null)\n if (UserId in UserRegistry) {\n let UserSpecs = UserRegistry[UserId]\n if (UserSpecs.Password === Password) { // internal optimization\n return withAuthorizationOf(UserId,UserSpecs.Roles || [])\n }\n\n let PBKDF2Iterations = global.get('PBKDF2Iterations') || 100000\n crypto.pbkdf2(\n Password, Buffer.from(UserSpecs.Salt,'hex'), PBKDF2Iterations, 64, 'sha512',\n function (Error, computedHash) {\n if ((Error == null) && (computedHash.toString('hex') === UserSpecs.Hash)) {\n UserSpecs.Password = Password // speeds up future auth. requests\n return withAuthorizationOf(UserId,UserSpecs.Roles || [])\n } else {\n return withAuthorizationRequest()\n }\n }\n )\n } else {\n return withAuthorizationRequest()\n }\n\n function withAuthorizationOf (UserId, UserRoles) {\n if ((msg.requiredRole == null) || (UserRoles.indexOf(msg.requiredRole) >= 0)) {\n msg.authenticatedUser = UserId\n msg.authorizedRoles = UserRoles\n \n node.send([msg,null])\n node.done()\n } else {\n return withAuthorizationRequest()\n }\n }\n\n function withAuthorizationRequest () {\n msg.headers = msg.headers || {}\n msg.headers['WWW-Authenticate'] = 'Basic'\n\n msg.payload = 'Unauthorized'\n msg.statusCode = 401\n\n node.send([null,msg])\n node.done()\n }\n",
"outputs": 2,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [
{
"var": "crypto",
"module": "crypto"
}
],
"x": 940,
"y": 100,
"wires": [
[
"e0e0d17d3102670c"
],
[
"368ab4163a1781fe"
]
]
},
{
"id": "e64025c431b73536",
"type": "reusable-in",
"z": "2d76fa36687aa026",
"name": "basic auth",
"info": "describe your reusable flow here",
"scope": "global",
"x": 740,
"y": 100,
"wires": [
[
"044b8567a46bcbc1"
]
]
},
{
"id": "e0e0d17d3102670c",
"type": "reusable-out",
"z": "2d76fa36687aa026",
"name": "authorized",
"position": 1,
"x": 1150,
"y": 80,
"wires": []
},
{
"id": "368ab4163a1781fe",
"type": "reusable-out",
"z": "2d76fa36687aa026",
"name": "unauthorized",
"position": "2",
"x": 1150,
"y": 120,
"wires": []
},
{
"id": "42afd0252abcde7b",
"type": "http in",
"z": "2d76fa36687aa026",
"name": "",
"url": "basic-auth",
"method": "get",
"upload": false,
"swaggerDoc": "",
"x": 760,
"y": 240,
"wires": [
[
"a592efb9da32b941"
]
]
},
{
"id": "a453e55d889e4f45",
"type": "http response",
"z": "2d76fa36687aa026",
"name": "",
"statusCode": "",
"headers": {},
"x": 1190,
"y": 300,
"wires": []
},
{
"id": "15935af93c0fb448",
"type": "change",
"z": "2d76fa36687aa026",
"name": "inform about success",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "successfully authorized",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 1000,
"y": 300,
"wires": [
[
"a453e55d889e4f45"
]
]
},
{
"id": "82171f093abb349a",
"type": "comment",
"z": "2d76fa36687aa026",
"name": "try yourself",
"info": "",
"x": 740,
"y": 180,
"wires": []
},
{
"id": "a592efb9da32b941",
"type": "reusable",
"z": "2d76fa36687aa026",
"name": "",
"target": "basic auth",
"outputs": 2,
"x": 950,
"y": 240,
"wires": [
[
"15935af93c0fb448"
],
[
"a453e55d889e4f45"
]
]
},
{
"id": "a437ea9e9983f2b9",
"type": "comment",
"z": "2d76fa36687aa026",
"name": "if used without \"node-red-within-express\"",
"info": "",
"x": 840,
"y": 380,
"wires": []
},
{
"id": "ed50b5b2e94746c8",
"type": "inject",
"z": "2d76fa36687aa026",
"name": "at Startup",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payloadType": "date",
"x": 760,
"y": 440,
"wires": [
[
"4a4c700ae4ecd934"
]
]
},
{
"id": "2d98c64a67e881e7",
"type": "file in",
"z": "2d76fa36687aa026",
"name": "",
"filename": "./registeredUsers.json",
"format": "utf8",
"chunk": false,
"sendError": false,
"encoding": "utf8",
"allProps": false,
"x": 820,
"y": 560,
"wires": [
[
"70c82a6e25251a70"
]
]
},
{
"id": "470b387186d7852a",
"type": "catch",
"z": "2d76fa36687aa026",
"name": "",
"scope": [
"2d98c64a67e881e7",
"70c82a6e25251a70"
],
"uncaught": false,
"x": 770,
"y": 620,
"wires": [
[
"46809118b7d27252"
]
]
},
{
"id": "a52bccab8bc183a8",
"type": "debug",
"z": "2d76fa36687aa026",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "'could not load user registry'",
"statusType": "jsonata",
"x": 1170,
"y": 620,
"wires": []
},
{
"id": "3eb566d1907d9b3b",
"type": "debug",
"z": "2d76fa36687aa026",
"name": "Status",
"active": true,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "'user registry available'",
"statusType": "jsonata",
"x": 1190,
"y": 440,
"wires": []
},
{
"id": "46809118b7d27252",
"type": "function",
"z": "2d76fa36687aa026",
"name": "create in global context",
"func": " let UserRegistry = Object.create(null)\n UserRegistry['node-red'] = {\n Roles: ['node-red'],\n Salt: '4486e8d35b8275020b1301226cc77963',\n Hash: 'ab2b740ea9148aa4f320af3f3ba60ee2e33bb8039c57eea2b29579ff3f3b16bec2401f19e3c6ed8ad36de432b80b6f973a12c41af5d50738e4bb902d0117df53'\n }\n global.set('UserRegistry',UserRegistry)\n\n return msg\n",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 970,
"y": 620,
"wires": [
[
"a52bccab8bc183a8",
"e0b359a7b5f90189"
]
]
},
{
"id": "a463788d4c5c13ab",
"type": "file",
"z": "2d76fa36687aa026",
"name": "",
"filename": "./registeredUsers.json",
"appendNewline": false,
"createDir": false,
"overwriteFile": "true",
"encoding": "utf8",
"x": 1080,
"y": 740,
"wires": [
[
"53948a7e0233c789"
]
]
},
{
"id": "0892a7393f0e2863",
"type": "catch",
"z": "2d76fa36687aa026",
"name": "",
"scope": [
"a463788d4c5c13ab"
],
"uncaught": false,
"x": 770,
"y": 860,
"wires": [
[
"e50a7cafe871861b",
"d97e0ea28a9f3c81"
]
]
},
{
"id": "e50a7cafe871861b",
"type": "debug",
"z": "2d76fa36687aa026",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "'could not write user registry'",
"statusType": "jsonata",
"x": 910,
"y": 900,
"wires": []
},
{
"id": "53948a7e0233c789",
"type": "change",
"z": "2d76fa36687aa026",
"name": "restore payload",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "_payload",
"tot": "msg"
},
{
"t": "delete",
"p": "_payload",
"pt": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 840,
"y": 800,
"wires": [
[
"1ce07b54225e17dd"
]
]
},
{
"id": "d97e0ea28a9f3c81",
"type": "change",
"z": "2d76fa36687aa026",
"name": "report in payload",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "'Internal Server Error'",
"tot": "jsonata"
},
{
"t": "set",
"p": "statusCode",
"pt": "msg",
"to": "500",
"tot": "str"
},
{
"t": "delete",
"p": "_payload",
"pt": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 950,
"y": 860,
"wires": [
[
"1ce07b54225e17dd"
]
]
},
{
"id": "70c82a6e25251a70",
"type": "function",
"z": "2d76fa36687aa026",
"name": "write to global context",
"func": " let UserSet = JSON.parse(msg.payload) // may fail!\n\n let UserRegistry = Object.create(null)\n for (let UserId in UserSet) {\n if (UserSet.hasOwnProperty(UserId)) {\n if ((UserId.indexOf('/') >= 0) || (UserId.indexOf(':') >= 0)) {\n throw 'Invalid character in UserId found'\n }\n \n UserRegistry[UserId.toLowerCase()] = UserSet[UserId]\n }\n }\n global.set('UserRegistry',UserRegistry)\n\n msg.payload = ''\n return msg\n",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1060,
"y": 560,
"wires": [
[
"e0b359a7b5f90189"
]
]
},
{
"id": "4f110197c9f2a334",
"type": "function",
"z": "2d76fa36687aa026",
"name": "→ catch",
"func": "// do not pass any msg from here!",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1020,
"y": 500,
"wires": [
[
"46809118b7d27252"
]
]
},
{
"id": "687ebf9f0226b7dc",
"type": "function",
"z": "2d76fa36687aa026",
"name": "→ catch",
"func": "// do not pass any msg from here!",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 960,
"y": 680,
"wires": [
[
"d97e0ea28a9f3c81"
]
]
},
{
"id": "848c6ddfb76643e4",
"type": "function",
"z": "2d76fa36687aa026",
"name": "read from global context",
"func": " let UserRegistry = global.get('UserRegistry')\n let UserSet = {}\n for (let UserId in UserRegistry) {\n if (UserRegistry[UserId] == null) {\n UserSet[UserId] = null\n } else {\n let UserEntry = Object.assign({},UserRegistry[UserId])\n delete UserEntry.Password // never write passwords in plain text!\n UserSet[UserId] = UserEntry\n }\n }\n\n msg.payload = JSON.stringify(UserSet)\n return msg\n",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 830,
"y": 740,
"wires": [
[
"a463788d4c5c13ab"
]
]
},
{
"id": "e0b359a7b5f90189",
"type": "reusable-out",
"z": "2d76fa36687aa026",
"name": "return",
"position": 1,
"x": 1190,
"y": 500,
"wires": []
},
{
"id": "1ce07b54225e17dd",
"type": "reusable-out",
"z": "2d76fa36687aa026",
"name": "return",
"position": 1,
"x": 1190,
"y": 800,
"wires": []
},
{
"id": "254d667c7cc38a4e",
"type": "reusable-in",
"z": "2d76fa36687aa026",
"name": "load or create UserRegistry",
"info": "describe your reusable flow here",
"scope": "global",
"x": 800,
"y": 500,
"wires": [
[
"4f110197c9f2a334",
"2d98c64a67e881e7"
]
]
},
{
"id": "e50f6e5eb55f7d87",
"type": "reusable-in",
"z": "2d76fa36687aa026",
"name": "write UserRegistry",
"info": "describe your reusable flow here",
"scope": "global",
"x": 770,
"y": 680,
"wires": [
[
"687ebf9f0226b7dc",
"848c6ddfb76643e4"
]
]
},
{
"id": "4a4c700ae4ecd934",
"type": "reusable",
"z": "2d76fa36687aa026",
"name": "",
"target": "load or create userregistry",
"outputs": 1,
"x": 980,
"y": 440,
"wires": [
[
"3eb566d1907d9b3b"
]
]
},
{
"id": "fe9d58b1f84fce63",
"type": "inject",
"z": "2d76fa36687aa026",
"name": "show UserRegistry",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payloadType": "date",
"x": 790,
"y": 1040,
"wires": [
[
"663a2575d83ff39f"
]
]
},
{
"id": "80fab4a14a0c61a7",
"type": "comment",
"z": "2d76fa36687aa026",
"name": "for testing purposes",
"info": "",
"x": 770,
"y": 980,
"wires": []
},
{
"id": "663a2575d83ff39f",
"type": "function",
"z": "2d76fa36687aa026",
"name": "create output",
"func": "let UserRegistry = global.get('UserRegistry') || Object.create(null)\n let UserList = []\n for (let UserId in UserRegistry) {\n UserList.push(\n UserRegistry[UserId] == null ? '[' + UserId + ']' : UserId\n )\n }\nmsg.payload = (\n UserList.length === 0\n ? '(no user registered)'\n : 'registered users: \"' + UserList.join('\",\"') + '\"'\n)\nreturn msg",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1000,
"y": 1040,
"wires": [
[
"62b2658722dbd9e7"
]
]
},
{
"id": "62b2658722dbd9e7",
"type": "debug",
"z": "2d76fa36687aa026",
"name": "show",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 1170,
"y": 1040,
"wires": []
}
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment