nodered-contrib-saml-flow

This flow allows usage of SAML within node-red.

It depends on https://www.npmjs.com/package/saml2-js. Pls install the module to use this flow.

It has been created by the greatest of all saml guru, addict and insane man you would ever imagine: Fanshan (https://github.com/fanshan)

Thank you for this !

[{"id":"95d56a5d.e5ccf8","type":"http in","z":"669c0d6d.d65b14","name":"","url":"/login","method":"get","upload":false,"swaggerDoc":"","x":100,"y":100,"wires":[["5ec98336.1b3e7c"]]},{"id":"62e64d50.e57654","type":"http response","z":"669c0d6d.d65b14","name":"","statusCode":"303","headers":{},"x":520,"y":100,"wires":[]},{"id":"5ec98336.1b3e7c","type":"function","z":"669c0d6d.d65b14","name":"Create AuthnResquest","func":"const saml2 = global.get('saml2');\nconst uuid = global.get('uuid');\nconst Memcached = global.get('memcached');\n\nconst relay_state = uuid.v4();\n\nconst host = msg.req.headers.host;\n\nconst idp = new saml2.IdentityProvider(global.get('idp_options'));\nconst sp = new saml2.ServiceProvider(global.get('sp_options')[host]);\n        \nconst memcached = new Memcached(global.get('memcached_settings').hosts);\n\nmemcached.set(\n    global.get('memcached_settings').key_prefix + 'ReturnTo_' + relay_state,\n    msg.req.query.ReturnTo || '/',\n    global.get('login_session').ttl,\n    error => {}\n);\n\nmemcached.end();\n\nsp.create_login_request_url(\n    idp,\n    {\n        relay_state: relay_state\n    },\n    (error, login_url, request_id) => {\n        msg.headers = {\n            Location: login_url\n        };\n        node.send(msg);\n        \n        return;\n    }\n);","outputs":1,"noerr":0,"initialize":"","finalize":"","x":320,"y":100,"wires":[["62e64d50.e57654"]]},{"id":"717f5d0b.b47f5c","type":"http in","z":"669c0d6d.d65b14","name":"","url":"/saml/postResponse","method":"post","upload":false,"swaggerDoc":"","x":150,"y":260,"wires":[["965c3f76.914d"]]},{"id":"8f2c800f.bf5ee","type":"http response","z":"669c0d6d.d65b14","name":"","statusCode":"303","headers":{},"x":840,"y":240,"wires":[]},{"id":"965c3f76.914d","type":"function","z":"669c0d6d.d65b14","name":"Read SamlResponse","func":"const saml2 = global.get('saml2');\nconst url = global.get('url');\nconst uuid = global.get('uuid');\nconst Memcached = global.get('memcached');\n\nconst host = msg.req.headers.host;\n\nconst idp = new saml2.IdentityProvider(global.get('idp_options'));\nconst sp = new saml2.ServiceProvider(global.get('sp_options')[host]);\n\nsp.post_assert(\n    idp,\n    {\n        request_body: msg.payload\n    },\n    (error, response) => {\n        if (error) {\n            msg.error = error;\n            node.send(msg);\n            \n            return;\n        }\n        \n        const memcached = new Memcached(global.get('memcached_settings').hosts);\n        \n        memcached.get(\n            global.get('memcached_settings').key_prefix + 'ReturnTo_' + msg.payload.RelayState,\n            function (error, data) {\n                if (error) {\n                    msg.error = error;\n                    node.send(msg);\n                    \n                    return;\n                }\n                \n                let sessionValue = uuid.v4();\n                \n                memcached.set(\n                    global.get('memcached_settings').key_prefix + sessionValue,\n                    response,\n                    global.get('login_session').ttl,\n                    error => {}\n                );\n                \n                memcached.end();\n                \n                const entityId = global.get('sp_options')[host].entity_id;\n                \n                msg.cookies = {};\n                \n                msg.cookies[global.get('login_session').cookie_user_name] = {\n                    value: response.user.attributes.user_entity[0],\n                    domain: new url.parse(entityId).hostname,\n                    maxAge: global.get('login_session').ttl * 1000\n                };\n                \n                msg.cookies[global.get('login_session').cookie_name] = {\n                    value: sessionValue,\n                    domain: new url.parse(entityId).hostname,\n                    maxAge: global.get('login_session').ttl * 1000\n                };\n                \n                msg.headers = {\n                    Location: entityId + (data === undefined ? '' : data)\n                };\n                \n                node.send(msg);\n                \n                return;\n            }\n        );\n    }\n);","outputs":1,"noerr":0,"initialize":"","finalize":"","x":420,"y":260,"wires":[["1bdad9d1.75c40e"]]},{"id":"fe55ea8.0b39318","type":"comment","z":"669c0d6d.d65b14","name":"Read SamlResponse","info":"","x":140,"y":200,"wires":[]},{"id":"1bdad9d1.75c40e","type":"switch","z":"669c0d6d.d65b14","name":"","property":"error","propertyType":"msg","rules":[{"t":"null"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":630,"y":260,"wires":[["8f2c800f.bf5ee"],["bb009eb1.eabb38"]]},{"id":"bdfc04b1.25b8b8","type":"http response","z":"669c0d6d.d65b14","name":"","statusCode":"500","headers":{},"x":1040,"y":300,"wires":[]},{"id":"bb009eb1.eabb38","type":"change","z":"669c0d6d.d65b14","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"error","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":860,"y":300,"wires":[["bdfc04b1.25b8b8"]]},{"id":"925aa566.663dc8","type":"http in","z":"669c0d6d.d65b14","name":"","url":"/saml/logout","method":"get","upload":false,"swaggerDoc":"","x":120,"y":500,"wires":[["3f84fc9f.1a61fc"]]},{"id":"3f84fc9f.1a61fc","type":"switch","z":"669c0d6d.d65b14","name":"","property":"req.query.ReturnTo","propertyType":"msg","rules":[{"t":"null"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":290,"y":500,"wires":[["84324e17.e089f8"],["edf5bd02.2e3c1"]]},{"id":"7d139601.ba8bb8","type":"comment","z":"669c0d6d.d65b14","name":"Logout endpoint","info":"","x":120,"y":440,"wires":[]},{"id":"edf5bd02.2e3c1","type":"function","z":"669c0d6d.d65b14","name":"Create SAML Logout Request","func":"const saml2 = global.get('saml2');\nconst Memcached = global.get('memcached');\nconst uuid = global.get('uuid');\n\nconst host = msg.req.headers.host;\n \nconst idp = new saml2.IdentityProvider(global.get('idp_options'));\nconst sp = new saml2.ServiceProvider(global.get('sp_options')[host]);\n\nconst memcached = new Memcached(global.get('memcached_settings').hosts);\n\nif (!(global.get('login_session').cookie_name in msg.req.cookies)) {\n    msg.headers = {\n        Location: global.get('sp_options')[host].entity_id\n    };\n    \n    return msg;\n}\n\nmemcached.get(\n    global.get('memcached_settings').key_prefix + msg.req.cookies[global.get('login_session').cookie_name],\n    (error, data) => {\n        if (!error && data !== undefined) {\n            sp.create_logout_request_url(\n                idp,\n                {\n                    name_id: data.user.name_id,\n                    relay_state: uuid.v4()\n                },\n                (error, request_url) => {\n                    msg.headers = {\n                        Location: request_url\n                    };\n                    node.send(msg);\n                    \n                    return;\n                }\n            );\n        } else {\n            msg.headers = {\n                Location: global.get('sp_options')[host].entity_id\n            };\n            node.send(msg);\n            \n            return;\n        }\n    }\n);\n\nmemcached.end();","outputs":1,"noerr":0,"x":650,"y":660,"wires":[["a66b5f7d.96046"]]},{"id":"84324e17.e089f8","type":"switch","z":"669c0d6d.d65b14","name":"","property":"req.query","propertyType":"msg","rules":[{"t":"hask","v":"SAMLRequest","vt":"str"},{"t":"hask","v":"SAMLResponse","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":3,"x":430,"y":480,"wires":[["781ee67e.8617f"],["5ea26b08.af76f4"],["637d9f66.55c02"]]},{"id":"f90b7220.31d28","type":"http response","z":"669c0d6d.d65b14","name":"","statusCode":"400","headers":{},"x":580,"y":600,"wires":[]},{"id":"637d9f66.55c02","type":"change","z":"669c0d6d.d65b14","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"error\":\"SAMLRequest or SAMLResponse missing.\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":600,"y":540,"wires":[["f90b7220.31d28"]]},{"id":"781ee67e.8617f","type":"function","z":"669c0d6d.d65b14","name":"Read SAML Logout Request","func":"const saml2 = global.get('saml2');\n\nconst host = msg.req.headers.host;\n \nconst idp = new saml2.IdentityProvider(global.get('idp_options'));\nconst sp = new saml2.ServiceProvider(global.get('sp_options')[host]);\n\nsp.redirect_assert(\n    idp,\n    {\n        request_body: msg.payload\n    },\n    (error, response) => {\n        if (error) {\n            msg.error = error;\n            node.send(msg);\n            \n            return;\n        }\n        \n        sp.create_logout_response_url(\n            idp,\n            {\n                in_response_to: response.response_header.id\n            },\n            (error, request_url) => {\n                if (error) {\n                    msg.error = error;\n                    node.send(msg);\n            \n                    return;\n                }\n                \n                msg.headers = {\n                    Location: request_url\n                };\n                \n                node.send(msg);\n                \n                return;\n            }\n        );\n        \n        return;\n    }\n);","outputs":1,"noerr":0,"initialize":"","finalize":"","x":640,"y":420,"wires":[["a66b5f7d.96046"]]},{"id":"5ea26b08.af76f4","type":"function","z":"669c0d6d.d65b14","name":"Read SAML Logout Response","func":"const saml2 = global.get('saml2');\n\nconst host = msg.req.headers.host;\n \nconst idp = new saml2.IdentityProvider(global.get('idp_options'));\nconst sp = new saml2.ServiceProvider(global.get('sp_options')[host]);\n\nsp.redirect_assert(\n    idp,\n    {\n        request_body: msg.payload\n    },\n    (error, response) => {\n        if (error) {\n            msg.error = error;\n            node.send(msg);\n            \n            return;\n        }\n        \n        msg.headers = {\n            Location: global.get('sp_options')[host].entity_id\n        };\n        node.send(msg);\n        \n        return;\n    }\n);","outputs":1,"noerr":0,"x":650,"y":480,"wires":[["a66b5f7d.96046"]]},{"id":"a66b5f7d.96046","type":"function","z":"669c0d6d.d65b14","name":"Destroy Session","func":"const Memcached = global.get('memcached');\n\nif (global.get('login_session').cookie_name in msg.req.cookies) {\n    const memcached = new Memcached(global.get('memcached_settings').hosts);\n    memcached.del(global.get('memcached_settings').key_prefix + msg.req.cookies[global.get('login_session').cookie_name], error => {});\n    memcached.end();\n}\n\nmsg.cookies = {};\n\nmsg.cookies[global.get('login_session').cookie_user_name] = null;\nmsg.cookies[global.get('login_session').cookie_name] = null;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":940,"y":420,"wires":[["1bdad9d1.75c40e"]]},{"id":"f4584711.c7506","type":"comment","z":"669c0d6d.d65b14","name":"Test session and renew","info":"","x":140,"y":740,"wires":[]},{"id":"8acf34cf.0a022","type":"http in","z":"669c0d6d.d65b14","name":"","url":"/session","method":"get","upload":false,"swaggerDoc":"","x":110,"y":820,"wires":[["1e885bc4.56c194"]]},{"id":"1e885bc4.56c194","type":"function","z":"669c0d6d.d65b14","name":"Read session and renew","func":"const Memcached = global.get('memcached');\nconst url = global.get('url');\n\nconst host = msg.req.headers.host;\n\nif (global.get('login_session').cookie_name in msg.req.cookies) {\n    const memcached = new Memcached(global.get('memcached_settings').hosts);\n    \n    memcached.get(\n        global.get('memcached_settings').key_prefix + msg.req.cookies[global.get('login_session').cookie_name],\n        (error, data) => {\n            if (!error && data !== undefined) {\n                const entityId = global.get('sp_options')[host].entity_id;\n                \n                msg.cookies = {};\n                \n                msg.cookies[global.get('login_session').cookie_user_name] = {\n                    value: data.user.attributes.user_entity[0],\n                    domain: new url.parse(entityId).hostname,\n                    maxAge: global.get('login_session').ttl * 1000\n                };\n        \n                msg.cookies[global.get('login_session').cookie_name] = {\n                    value: msg.req.cookies[global.get('login_session').cookie_name],\n                    domain: new url.parse(entityId).hostname,\n                    maxAge: global.get('login_session').ttl * 1000\n                };\n            } else {\n                msg.cookies = {};\n        \n                msg.cookies[global.get('login_session').cookie_user_name] = null;\n                msg.cookies[global.get('login_session').cookie_name] = null;\n                \n                msg.error = 'Unable to find session';\n            }\n            \n            node.send(msg);\n            \n            return;\n        }\n    );\n    \n    memcached.end();\n} else {\n    msg.cookies = {};\n        \n    msg.cookies[global.get('login_session').cookie_user_name] = null;\n    msg.cookies[global.get('login_session').cookie_name] = null;\n    \n    msg.error = 'No session provided';\n    \n    return msg;\n}","outputs":1,"noerr":0,"x":350,"y":820,"wires":[["effbeca5.f8df2"]]},{"id":"ee7dee97.ecd08","type":"http response","z":"669c0d6d.d65b14","name":"","statusCode":"200","headers":{},"x":920,"y":780,"wires":[]},{"id":"effbeca5.f8df2","type":"switch","z":"669c0d6d.d65b14","name":"","property":"error","propertyType":"msg","rules":[{"t":"null"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":550,"y":820,"wires":[["f06d9249.d9bed"],["f5bb2c5d.8889c8"]]},{"id":"9797caaa.1cfee","type":"http response","z":"669c0d6d.d65b14","name":"","statusCode":"404","headers":{"Content-Type":"application/json"},"x":920,"y":860,"wires":[]},{"id":"f5bb2c5d.8889c8","type":"change","z":"669c0d6d.d65b14","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"'{\"message\": \"' & msg.error & '\"}'","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":740,"y":860,"wires":[["9797caaa.1cfee"]]},{"id":"f06d9249.d9bed","type":"change","z":"669c0d6d.d65b14","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"message\":\"OK\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":740,"y":780,"wires":[["ee7dee97.ecd08"]]}]

Flow Info

Created 3 years, 6 months ago
Rating: 1 1

Owner

Actions

Rate:

Node Types

Core
  • change (x4)
  • comment (x3)
  • function (x7)
  • http in (x4)
  • http response (x6)
  • switch (x4)

Tags

  • saml
  • yoctu
  • nodered
  • flow
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option