[
    {
        "id": "b2edf8ba.820b38",
        "type": "subflow",
        "name": "axios",
        "info": "# axios\n\nThis subflow is a tool to make HTTP requests. It uses Axios and can be configured to use any common configuration of methods, messages, payloads and queries in the scope of axios' capabilities.\n\n## Requirements\n\nExternal libraries:\n* axios\n\n## Inputs\n\nThe following structure in the **msg** object can be passed on to the subflow:\n\n```JSON\nmsg = {\n   \"method\": <method>, //required\n   \"url\": <url>, //required\n   \"headers\": <headers>,\n   \"payload\": <payload>,\n   \"params\": <params>\n   \"auth\": <auth>\n}\n```\n\n* method (string): HTTP method used, e.g.: 'GET', 'POST', 'PUT', 'DELETE'\n* url (string): address of the request\n* headers (object): additional headers of the request as Key/Value pair, e.g.: {'Accept': 'application/json'}\n* payload (any): Payload of the request\n* params (object): Query parameters, as Key/Value pair, e.g.: {'search': 'searchTerm'}\n* auth (object): basic anthentication: adds basic auth encoded autorization header, parameters as Key/Value pair, e.g.: {'username': 'username', 'password': 'password'}\n\n## Outputs\n\n### response\n\nThe *response* output will return most of the information of the HTTP response. The *response* output will return every HTTP response, including error responses (Axios handles status codes like 400 and 500 as errors), with the following data:\n\n```JSON\nmsg = {\n    \"statusCode\": <statusCode>,\n    \"statusText\": <statusText>,\n    \"headers\": <headers>,\n    \"config\": <config>,\n    \"payload\": <payload>\n}\n```\n\n* statusCode (number): HTTP status code of the response\n* statusText (string): status text of the response\n* headers (object): Headers of the response as Key/Value pairs\n* config (object): additional information to the response, as Key/Value pairs\n* payload (any): Payload of the response\n\n### error\n\nThe  *error* output will be triggered if an error occurred after the axios HTTP request. Every error should be related to parsing the response data. It will be returned as:\n\n```JSON\nmsg = {\n    \"payload\": {\n        \"name\": <errorType>,\n        \"message\": <errorMessage>,\n        \"error\": <error>\n    }\n}\n```\n\n* errorType (string): indicator which type of error occurred\n* errorMessage (string): message describing the error\n* error (any): detailed error information if available",
        "category": "network",
        "in": [
            {
                "x": 60,
                "y": 80,
                "wires": [
                    {
                        "id": "64b6df6c.cfba8"
                    }
                ]
            }
        ],
        "out": [
            {
                "x": 280,
                "y": 40,
                "wires": [
                    {
                        "id": "64b6df6c.cfba8",
                        "port": 0
                    }
                ]
            },
            {
                "x": 280,
                "y": 120,
                "wires": [
                    {
                        "id": "64b6df6c.cfba8",
                        "port": 1
                    }
                ]
            }
        ],
        "env": [],
        "color": "#a21ae2",
        "inputLabels": [
            "parameters"
        ],
        "outputLabels": [
            "response",
            "error"
        ],
        "icon": "node-red/white-globe.svg"
    },
    {
        "id": "64b6df6c.cfba8",
        "type": "function",
        "z": "b2edf8ba.820b38",
        "name": "AXIOS",
        "func": "const require = global.get('require');\nconst axios = require('axios');\n\naxios({\n  method: msg.method,\n  url: msg.url,\n  headers: msg.headers,\n  data: msg.payload,\n  params: msg.params,\n  auth: msg.auth\n})\n.then(function(response) {\n    node.warn(response)\n    try {\n        msg.statusCode = response.status;\n        msg.statusText = response.statusText;\n        msg.headers = response.headers;\n        msg.config = response.config;\n        msg.payload = response.data;\n        node.send([msg, null]);\n    } catch (err) {\n        msg = {\n            \"payload\": {\n                \"name\": \"ParsingError\",\n                \"message\": \"Unexpected HTTP response object\",\n                \"error\": err\n            }\n        }\n        node.send([null, msg]);\n    }\n})\n.catch(function(error) {\n    node.warn(error)\n    try {\n        if (msg.hasOwnProperty('statusCode')) {\n            msg.config = error.config;\n            msg.statusCode = error.response.status;\n            msg.statusText = error.response.statusText;\n            msg.headers = error.response.headers;\n            msg.payload = error.response.data;\n            node.send([msg, null]);\n        } else {\n            msg = {\n                \"payload\": {\n                    \"name\": \"AxiosError\",\n                    \"message\": \"An axios error occurred trying to make an HTTP request with the provided data\",\n                    \"error\": err\n                }\n            }\n            node.send([null, msg]);\n        }\n    } catch (err) {\n        msg = {\n            \"payload\": {\n                \"name\": \"ErrorParsingError\",\n                \"message\": \"Unexpected HTTP request or response object\",\n                \"error\": err\n            }\n        }\n        node.send([null, msg]);\n    }\n})",
        "outputs": 2,
        "noerr": 0,
        "x": 180,
        "y": 80,
        "wires": [
            [],
            []
        ],
        "outputLabels": [
            "response",
            "error"
        ]
    },
    {
        "id": "cfa5bc71.f050c",
        "type": "subflow",
        "name": "Auth Guard",
        "info": "# Auth Guard\n\nThe Auth Guard will verify a Bearer Token with the Public Key of a Keycloak instance.\n\nThe subflow will write the global variables *authGuard-kid* and *authGuard-publicKey* to store the Public Key and the Key ID of the latest token.\n\nIt uses *msg.authGuardData* to transfer data between nodes. This object will be deleted before data is returned.\n\n## Requirements\n\nExternal libraries:\n* jsonwebtoken\n\n## Inputs\n\nThe following structure in **msg.payload** is required to use the subflow:\n\n```JSON\nmsg.payload = {\n    \"token\": <token>,\n    \"keycloak\": {\n        \"issuerUrl\": <issuerUrl>,\n        \"realm\": <realm>\n    }\n}\n\n```\n\n* token (string): the token to verify without a *Bearer* prefix\n* issuerUrl (string): the URL of the Keycloak issuer instance in the form https://auth.address.com\n* realm (string): the name of the realm used for this Keycloak instance\n\n## Outputs\n\n### verified\nThe following structure will be the output of the *verified* output:\n\n```JSON\nmsg.payload = {\n    \"token\": <token>,\n    \"tokenDecoded\": <tokenDecoded>\n}\n```\n\n* token (string): the token to verify without a *Bearer* prefix\n* tokenDecoded (object): decoded JSON object of the token for further analysis\n\n### denied\nThe *denied* output will be used if the token verification failed or other errors occurred. It has the following structure:\n\n```JSON\nmsg.payload = {\n    \"name\": <errorType>,\n    \"message\": <errorMessage>,\n    \"error\": <error>\n}\n```\n\n* errorType (string): indicator which type of error occurred\n* errorMessage (string): message describing the error\n* error (any): detailed error information if available",
        "category": "network",
        "in": [
            {
                "x": 50,
                "y": 30,
                "wires": [
                    {
                        "id": "446384d0.e3b31c"
                    }
                ]
            }
        ],
        "out": [
            {
                "x": 600,
                "y": 420,
                "wires": [
                    {
                        "id": "10fb49f4.93d6a6",
                        "port": 0
                    }
                ]
            },
            {
                "x": 1020,
                "y": 160,
                "wires": [
                    {
                        "id": "c72191c6.866eb",
                        "port": 0
                    }
                ]
            }
        ],
        "env": [],
        "color": "#A6BBCF",
        "inputLabels": [
            "token, keycloak params"
        ],
        "outputLabels": [
            "verified",
            "denied"
        ],
        "icon": "font-awesome/fa-lock"
    },
    {
        "id": "3adac47e.b49aac",
        "type": "function",
        "z": "cfa5bc71.f050c",
        "name": "check KID",
        "func": "\nconst tokenDecoded = msg.authGuardData.tokenDecoded;\nconst kidStored = global.get('authGuard-kid');\n\nif (tokenDecoded.header.kid == kidStored) {\n    if (global.get('authGuard-publicKey') !== undefined) {\n        node.send([msg, null]);\n    } else {\n        node.send([null, msg]);\n    }\n} else {\n    node.send([null, msg]);\n}",
        "outputs": 2,
        "noerr": 0,
        "x": 250,
        "y": 120,
        "wires": [
            [
                "10fb49f4.93d6a6"
            ],
            [
                "d55e7d4e.cce8c"
            ]
        ],
        "outputLabels": [
            "continue",
            "new Public Key"
        ]
    },
    {
        "id": "48d5a383.8cd9dc",
        "type": "function",
        "z": "cfa5bc71.f050c",
        "name": "decode Token",
        "func": "const require = global.get('require');\nconst jwt = require('jsonwebtoken');\n\ntry {\n    const token = msg.authGuardData.token;\n    const tokenDecoded = jwt.decode(token, { complete: true });\n    msg.authGuardData.tokenDecoded = tokenDecoded;\n    node.send([msg, null]);\n} catch(err) {\n    msg.payload = {\n        \"name\": \"NodeError\",\n        \"message\": 'Error decoding token',\n        \"error\": err\n    }\n    node.send([null, msg]);\n}",
        "outputs": 2,
        "noerr": 0,
        "x": 260,
        "y": 80,
        "wires": [
            [
                "3adac47e.b49aac"
            ],
            [
                "c72191c6.866eb"
            ]
        ],
        "outputLabels": [
            "continue",
            "error"
        ]
    },
    {
        "id": "d55e7d4e.cce8c",
        "type": "function",
        "z": "cfa5bc71.f050c",
        "name": "get certs",
        "func": "\nmsg.method = \"GET\";\n\nmsg.url = `${msg.authGuardData.keycloak.issuerUrl}/auth/realms/${msg.authGuardData.keycloak.realm}/protocol/openid-connect/certs`;\n\nmsg.headers = {\n    \"Accept\": \"application/json\"\n}\n\ndelete msg.payload;\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 240,
        "y": 180,
        "wires": [
            [
                "f929e4c9.4bbff8"
            ]
        ]
    },
    {
        "id": "f929e4c9.4bbff8",
        "type": "subflow:b2edf8ba.820b38",
        "z": "cfa5bc71.f050c",
        "name": "",
        "env": [],
        "x": 390,
        "y": 180,
        "wires": [
            [
                "bd8760d3.a4d6a"
            ],
            [
                "fe5655bf.d0b138"
            ]
        ]
    },
    {
        "id": "446384d0.e3b31c",
        "type": "function",
        "z": "cfa5bc71.f050c",
        "name": "set data",
        "func": "\nif (msg.payload.hasOwnProperty('token')) {\n    msg.authGuardData = {\n        \"token\": msg.payload.token\n    }\n    if (msg.payload.hasOwnProperty('keycloak')) {\n        if (msg.payload.keycloak.hasOwnProperty('issuerUrl')) {\n            msg.authGuardData.keycloak = {\n                \"issuerUrl\": msg.payload.keycloak.issuerUrl\n            }\n            if (msg.payload.keycloak.hasOwnProperty('realm')) {\n                msg.authGuardData.keycloak.realm = msg.payload.keycloak.realm;\n                node.send([msg, null]);\n            } else {\n                reject('realm');\n            }\n        } else {\n            reject('issuerUrl');\n        } \n    } else {\n        reject('keycloak');\n    }\n} else {\n    reject('token');\n}\n\nfunction reject(element) {\n    msg.payload = {\n        \"name\": \"NodeError\",\n        \"message\": `Missing property '${element}'`\n    }\n    node.send([null, msg]);\n}",
        "outputs": 2,
        "noerr": 0,
        "x": 240,
        "y": 40,
        "wires": [
            [
                "48d5a383.8cd9dc"
            ],
            [
                "c72191c6.866eb"
            ]
        ],
        "outputLabels": [
            "continue",
            "error"
        ]
    },
    {
        "id": "d863daca.7b64e8",
        "type": "function",
        "z": "cfa5bc71.f050c",
        "name": "save certs data",
        "func": "\nif (msg.payload.hasOwnProperty('keys')) {\n    msg.authGuardData.keys = msg.payload.keys;\n    if (msg.payload.keys[0].hasOwnProperty('kid')) {\n        global.set('authGuard-kid', msg.payload.keys[0].kid);\n        node.send([msg, null]);\n    } else {\n        reject('kid');\n    }\n} else {\n    reject('keys');\n}\n\nfunction reject(element) {\n    msg.payload = `Missing property \"${element}\" in Keycloak certs request`;\n    node.send([null, msg]);\n}",
        "outputs": 1,
        "noerr": 0,
        "x": 260,
        "y": 300,
        "wires": [
            [
                "314a8f5b.63f45"
            ]
        ]
    },
    {
        "id": "314a8f5b.63f45",
        "type": "function",
        "z": "cfa5bc71.f050c",
        "name": "getPublicKey",
        "func": "const BEGIN_KEY = '-----BEGIN RSA PUBLIC KEY-----\\n';\nconst END_KEY = '\\n-----END RSA PUBLIC KEY-----\\n';\n\ntry {\n    const modulus = msg.authGuardData.keys[0].n;\n    const exponent = msg.authGuardData.keys[0].e;\n    \n    const publicKey = getPublicKey(modulus, exponent);\n    \n    global.set('authGuard-publicKey', publicKey);\n    node.send([msg, null]);\n} catch(err) {\n    global.set('authGuard-kid', undefined);\n    global.set('authGuard-publicKey', undefined);\n    msg.payload = {\n        \"name\": \"NodeError\",\n        \"message\": 'Error encoding public key',\n        \"error\": err\n    }\n    node.send([null, msg]);\n}\n\nfunction getPublicKey(modulus, exponent) {\n  const mod = convertToHex(modulus);\n  const exp = convertToHex(exponent);\n  const encModLen = encodeLenght(mod.length / 2);\n  const encExpLen = encodeLenght(exp.length / 2);\n  const part = [mod, exp, encModLen, encExpLen].map(n => n.length / 2).reduce((a, b) => a + b);\n  const bufferSource = `30${encodeLenght(part + 2)}02${encModLen}${mod}02${encExpLen}${exp}`;\n  const pubkey = Buffer.from(bufferSource, 'hex').toString('base64');\n  return BEGIN_KEY + pubkey.match(/.{1,64}/g).join('\\n') + END_KEY;\n}\n\nfunction convertToHex(str) {\n  const hex = Buffer.from(str, 'base64').toString('hex');\n  return hex[0] < '0' || hex[0] > '7'\n    ? `00${hex}`\n    : hex;\n}\n\nfunction encodeLenght(n) {\n  return n <= 127\n    ? toHex(n)\n    : toLongHex(n);\n}\n\nfunction toLongHex(number) {\n  const str = toHex(number);\n  const lengthByteLength = 128 + (str.length / 2);\n  return toHex(lengthByteLength) + str;\n}\n\nfunction toHex(number) {\n  const str = number.toString(16);\n  return (str.length % 2)\n    ? `0${str}`\n    : str;\n}",
        "outputs": 2,
        "noerr": 0,
        "x": 250,
        "y": 340,
        "wires": [
            [
                "10fb49f4.93d6a6"
            ],
            [
                "c72191c6.866eb"
            ]
        ],
        "outputLabels": [
            "continue",
            "error"
        ]
    },
    {
        "id": "10fb49f4.93d6a6",
        "type": "function",
        "z": "cfa5bc71.f050c",
        "name": "verify token",
        "func": "const require = global.get('require');\nconst jwt = require('jsonwebtoken');\n\nconst token = msg.authGuardData.token;\nconst publicKey = global.get('authGuard-publicKey');\n\njwt.verify(token, publicKey, function(err, decoded) {\n    if (err) {\n        msg.payload = err;\n        node.send([null, msg]);\n    } else {\n        try {\n            const user = msg.authGuardData.tokenDecoded.payload.preferred_username;\n            node.log(`Token for user ${user} verified`);\n        } catch(err) {\n            node.log(`Token verified`);\n        }\n        msg.payload = {\n            \"token\": msg.authGuardData.token,\n            \"tokenDecoded\": msg.authGuardData.tokenDecoded\n        }\n        delete msg.authGuardData;\n        node.send([msg, null]);\n    }\n});",
        "outputs": 2,
        "noerr": 0,
        "x": 250,
        "y": 420,
        "wires": [
            [],
            [
                "c72191c6.866eb"
            ]
        ],
        "outputLabels": [
            "verified",
            "error"
        ]
    },
    {
        "id": "fe5655bf.d0b138",
        "type": "function",
        "z": "cfa5bc71.f050c",
        "name": "error msg",
        "func": "\nmsg.payload.message = 'Error getting certs from Keycloak instance'\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 560,
        "y": 180,
        "wires": [
            [
                "c72191c6.866eb"
            ]
        ]
    },
    {
        "id": "c72191c6.866eb",
        "type": "function",
        "z": "cfa5bc71.f050c",
        "name": "remove data",
        "func": "\ndelete msg.authGuardData;\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 890,
        "y": 160,
        "wires": [
            []
        ]
    },
    {
        "id": "bd8760d3.a4d6a",
        "type": "switch",
        "z": "cfa5bc71.f050c",
        "name": "200?",
        "property": "statusCode",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "200",
                "vt": "num"
            },
            {
                "t": "else"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 2,
        "x": 230,
        "y": 240,
        "wires": [
            [
                "d863daca.7b64e8"
            ],
            [
                "e8c584a0.275398"
            ]
        ]
    },
    {
        "id": "e8c584a0.275398",
        "type": "function",
        "z": "cfa5bc71.f050c",
        "name": "error msg",
        "func": "\nmsg = {\n    \"payload\": {\n        \"name\": \"ResponseError\",\n        \"message\": \"Unexpected status code in Keycloak certs response\",\n        \"error\": `${msg.statusCode}: ${msg.statusText}`\n    }\n}\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 420,
        "y": 240,
        "wires": [
            [
                "c72191c6.866eb"
            ]
        ]
    },
    {
        "id": "fa7595a3.61da38",
        "type": "subflow:cfa5bc71.f050c",
        "z": "2c7e8a35.ca2936",
        "name": "",
        "env": [],
        "x": 580,
        "y": 220,
        "wires": [
            [
                "b79b7b95.13be38"
            ],
            [
                "dbe58bc2.6dcdc8"
            ]
        ]
    }
]