Microsoft Identity Platform in Node-Red without ROPC

2.6k views Asked by At

Is there a way to use Microsoft Identiy Platform with a OAuth 2.0 flow without using ROPC auth flow in Node-Red? I can't use ROPC, because the destination tenant enforces MFA. ROPC will be blocked when MFA is enforced.

I found the plugin node-red-contrib-oauth2, but wasn't able to get this working with Microsoft Identity Platform with another OAuth 2.0 flow, other than ROPC.

1

There are 1 answers

0
lufist On BEST ANSWER

The solution is to use Device Code Flow. The following instructions give you a flow which is able to read your Microsoft Teams presence status / Microsoft Office 365 presence status with node-red.

Creating the App in the Azure Portal

In the following example we will create an app which is able to read only the logged in users presence. This means, that the API permissions may vary depending on your needs.

  1. Login to the Azure Portal and go to Azure Active Directory, then to "App Registartions" and click "+ New registration"
  2. Type a friendly name that helps identifying the app.
  3. Supported Accounts: Accounts in this organizational directory only (* - Single tenant)
  4. Redirect URI: leave empty, as we will set this later.
  5. Click "Register"
  6. Go to "API Permissions".
  7. Remove the default permission as it is not needed for the presence.
  8. Click "+ Add a permission".
  9. Click "Microsoft Graph", then "delegated permissions".
  10. Select "offline_access" and "Presence.Read" and save it with "Add permissions"

Explanation:


  1. Then you have to "admin consent these permissions" by clicking the "Grant admin consent for *"-button next to the "+ Add permission button".
  2. Navigate back to "Overview" and copy the values of "Application (client) ID", "Directory (tenant) ID".
  3. Select "Authentication" in the navigation drawer.
  4. Click "+ Add Platform"
  5. Select "Mobile and desktop applications"
  6. Select "https://login.microsoftonline.com/common/oauth2/nativeclient"
  7. Submit with "Configure"
  8. Scroll down to "Advanced Settings" and select "Yes" next to "Treat application as a public client." and submit with "Save" in the upper left corner.

Now this app can be used in Node-Red for reading the presence from MS Graph API.

Use this App in Node-Red

A good starting point for a flow of that kind is this:

[{"id":"7c76e545.92af9c","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"40792ca0.843c24","type":"http request","z":"7c76e545.92af9c","name":"","method":"POST","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":710,"y":160,"wires":[["44c485e1.fa8d2c","643ed329.5bdfec"]]},{"id":"419648fb.f1a818","type":"inject","z":"7c76e545.92af9c","name":"launch device code request","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":180,"y":160,"wires":[["427185e9.4132fc"]]},{"id":"657e48c9.bcda48","type":"function","z":"7c76e545.92af9c","name":"Set refresh_token","func":"flow.get('refresh_token', function(err, refresh_token) {\n    if (err) {\n        node.error(err, msg);\n    } else {\n        // initialise the counter to 0 if it doesn't exist already\n        refresh_token = msg.payload.refresh_token;\n        // store the value back\n        flow.set('refresh_token',refresh_token, function(err) {\n            if (err) {\n                node.error(err, msg);\n            } else {\n                // make it part of the outgoing msg object\n                msg.refresh_token = refresh_token;\n                // send the message\n                node.status({fill:\"green\",shape:\"dot\",text:`refresh_token: ${msg.refresh_token}`});\n                node.send(msg);\n            }\n        });\n    }\n});\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":990,"y":340,"wires":[[]]},{"id":"d1253feb.c9362","type":"function","z":"7c76e545.92af9c","name":"Set access_token","func":"flow.get('access_token', function(err, access_token) {\n    if (err) {\n        node.error(err, msg);\n    } else {\n        // initialise the counter to 0 if it doesn't exist already\n        access_token = msg.payload.access_token;\n        // store the value back\n        flow.set('access_token',access_token, function(err) {\n            if (err) {\n                node.error(err, msg);\n            } else {\n                // make it part of the outgoing msg object\n                msg.access_token = access_token;\n                // send the message\n                node.status({fill:\"green\",shape:\"dot\",text:`access_token: ${msg.access_token}`});\n                node.send(msg);\n            }\n        });\n    }\n});\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":990,"y":300,"wires":[[]]},{"id":"44c485e1.fa8d2c","type":"function","z":"7c76e545.92af9c","name":"Set device_code","func":"flow.get('device_code', function(err, refresh_token) {\n    if (err) {\n        node.error(err, msg);\n    } else {\n        // initialise the counter to 0 if it doesn't exist already\n        device_code = msg.payload.device_code;\n        // store the value back\n        flow.set('device_code',device_code, function(err) {\n            if (err) {\n                node.error(err, msg);\n            } else {\n                // make it part of the outgoing msg object\n                msg.device_code = device_code;\n                // send the message\n                node.status({fill:\"green\",shape:\"dot\",text:`device_code: ${msg.device_code}`});\n                node.send(msg);\n            }\n        });\n    }\n});\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":950,"y":160,"wires":[[]]},{"id":"648d5fe3.74d0b","type":"function","z":"7c76e545.92af9c","name":"","func":"var context = flow.get(['tenant_id','client_id','scope','device_code']);\nvar tenant_id = context[0];\nvar client_id = context[1];\nvar scope     = context[2];\nvar device_code = context[3];\n\nif(!device_code)\n{\n    msg.delay = 5*1000;\n    return [msg, null];\n}\n\nif(tenant_id && client_id && scope && device_code)\n{\n    msg.url = \"https://login.microsoftonline.com/\"+tenant_id+\"/oauth2/v2.0/token\";\n    msg.headers = { \"Content-Type\": \"application/x-www-form-urlencoded\"};\n    msg.payload = {\n        \"client_id\": client_id,\n        \"grant_type\": \"urn:ietf:params:oauth:grant-type:device_code\",\n        \"scope\": scope,\n        \"code\": device_code\n    }\n    node.status({fill:\"green\",shape:\"dot\",text:`device_code: ${device_code.substring(0, 10)}`});\n    return [null, msg];\n}\n","outputs":2,"noerr":0,"initialize":"","finalize":"","x":320,"y":320,"wires":[["ddaa0b36.e42108"],["1fc0a330.6fe22d"]]},{"id":"ec8a6999.9abcd8","type":"inject","z":"7c76e545.92af9c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":130,"y":320,"wires":[["648d5fe3.74d0b"]]},{"id":"1fc0a330.6fe22d","type":"http request","z":"7c76e545.92af9c","name":"","method":"POST","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":510,"y":320,"wires":[["d32c769a.1c42b8"]]},{"id":"ba3c9093.320a7","type":"comment","z":"7c76e545.92af9c","name":"Retrieve tokens ...","info":"... after login has been made in a browser","x":130,"y":260,"wires":[]},{"id":"d4b1dae2.c6c538","type":"comment","z":"7c76e545.92af9c","name":"refresh tokens every 30 minutes","info":"","x":170,"y":400,"wires":[]},{"id":"396bdeb0.93db72","type":"inject","z":"7c76e545.92af9c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1800","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":130,"y":440,"wires":[["8739bad6.405738"]]},{"id":"8739bad6.405738","type":"function","z":"7c76e545.92af9c","name":"refresh request","func":"var context = flow.get(['tenant_id','client_id','scope','refresh_token']);\nvar tenant_id = context[0];\nvar client_id = context[1];\nvar scope     = context[2];\nvar refresh_token = context[3];\n\nmsg.url = \"https://login.microsoftonline.com/\"+tenant_id+\"/oauth2/v2.0/token\"; \nmsg.headers = {\n    \"Content-Type\": \"application/x-www-form-urlencoded\"\n};\nmsg.payload = {\n        \"grant_type\": \"refresh_token\",\n        \"client_id\": client_id,\n        \"refresh_token\": `${refresh_token}`,\n        \"scope\": scope\n\n};\n\nif(tenant_id && client_id && scope && refresh_token )\n    return msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":340,"y":440,"wires":[["1aafbb4a.e050b5"]]},{"id":"1aafbb4a.e050b5","type":"http request","z":"7c76e545.92af9c","name":"","method":"POST","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":550,"y":440,"wires":[["d32c769a.1c42b8"]]},{"id":"97a36414.d2b318","type":"http request","z":"7c76e545.92af9c","name":"","method":"GET","ret":"obj","paytoqs":"ignore","url":"https://graph.microsoft.com/beta/me/presence","tls":"","persist":false,"proxy":"","authType":"","x":490,"y":580,"wires":[["cd2de8e1.fdbdd8"]]},{"id":"4a82e18f.d26e","type":"inject","z":"7c76e545.92af9c","name":"","props":[{"p":"payload"}],"repeat":"5","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":580,"wires":[["f74a2254.e0487"]]},{"id":"f74a2254.e0487","type":"function","z":"7c76e545.92af9c","name":"","func":"var access_token = flow.get('access_token'); \n\nif(!access_token)\n{\n    node.status({fill:\"blue\",shape:\"dot\",text:`Access token missing. Exiting`});\n    return null;\n}\n\nmsg.headers= {\n            \"Authorization\": \"Bearer \"+access_token\n        };\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":320,"y":580,"wires":[["97a36414.d2b318"]]},{"id":"cd2de8e1.fdbdd8","type":"function","z":"7c76e545.92af9c","name":"","func":"var response = msg.payload;\nif(response.hasOwnProperty('availability') && response.hasOwnProperty('activity'))\n{\n    node.status({fill:\"green\",shape:\"dot\",text:`Status: ${response.availability} (${response.activity})`});\n    return [ { \"payload\": {\n        \"availability\": response.availability,\n        \"activity\": response.activity\n    }}]; \n}\nnode.status({fill:\"red\",shape:\"ring\",text:`Status: some error occurred`});\n\nconsole.log(\"no property availability\");","outputs":1,"noerr":0,"initialize":"","finalize":"","x":700,"y":580,"wires":[[]]},{"id":"8e2a7b69.0d2f38","type":"comment","z":"7c76e545.92af9c","name":"Available Presence Properties","info":"[Docs](https://learn.microsoft.com/en-us/graph/api/resources/presence?view=graph-rest-beta#properties)","x":770,"y":520,"wires":[]},{"id":"b28b4494.18e868","type":"inject","z":"7c76e545.92af9c","name":"change my values","props":[{"p":"scope","v":"Presence.Read offline_access","vt":"str"},{"p":"tenant_id","v":"","vt":"str"},{"p":"client_id","v":"","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payloadType":"str","x":130,"y":40,"wires":[["4bb4827.9197c7c"]]},{"id":"4bb4827.9197c7c","type":"function","z":"7c76e545.92af9c","name":"prepare context","func":"flow.get('tenant_id', function(err, tenant_id) {\n    if (err) {\n        node.error(err, msg);\n    } else {\n        // store the value\n        flow.set('tenant_id',msg.tenant_id, function(err) {\n            if (err) {\n                node.error(err, msg);\n            } else {\n                flow.get('scope', function(err, scope) {\n                    if (err) {\n                        node.error(err, msg);\n                    } else {\n                        // store the value\n                        flow.set('scope',msg.scope, function(err) {\n                            if (err) {\n                                node.error(err, msg);\n                            } else {\n                                flow.get('client_id', function(err, client_id) {\n                                    if (err) {\n                                        node.error(err, msg);\n                                    } else {\n                                        // store the value\n                                        flow.set('client_id',msg.client_id, function(err) {\n                                            if (err) {\n                                                node.error(err, msg);\n                                            } \n                                            // no else here\n                                        });\n                                    }\n                                });\n                            }\n                        });\n                    }\n                });\n                node.status({fill:\"green\",shape:\"dot\",text:`OK: context prepared`});\n            }\n        });\n    }\n});","outputs":1,"noerr":0,"initialize":"","finalize":"","x":360,"y":40,"wires":[[]]},{"id":"427185e9.4132fc","type":"function","z":"7c76e545.92af9c","name":"prepare device code request","func":"msg.headers = { \"Content-Type\": \"application/x-www-form-urlencoded\"};\n\nvar context = flow.get(['tenant_id','client_id','scope']);\nvar tenant_id = context[0];\nvar client_id = context[1];\nvar scope     = context[2];\nif(tenant_id && client_id && scope)\n{\n    msg.url = \"https://login.microsoftonline.com/\"+tenant_id+\"/oauth2/v2.0/devicecode\"\n    msg.payload = {    \n        \"client_id\": client_id,\n        \"scope\": scope\n    };\n    node.status({fill:\"green\",shape:\"dot\",text:`Values passed on`});\n    return msg;\n}\n\nnode.status({fill:\"red\",shape:\"dot\",text:`ERROR: context not prepared`});","outputs":1,"noerr":0,"initialize":"","finalize":"","x":460,"y":160,"wires":[["40792ca0.843c24"]]},{"id":"ddaa0b36.e42108","type":"delay","z":"7c76e545.92af9c","name":"","pauseType":"delayv","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":520,"y":240,"wires":[["648d5fe3.74d0b"]]},{"id":"d32c769a.1c42b8","type":"function","z":"7c76e545.92af9c","name":"","func":"var context = flow.get(['access_token','refresh_token']);\nvar access_token  = context[0]; \nvar refresh_token = context[1]; \n\nif(msg.payload.hasOwnProperty('access_token') && \nmsg.payload.hasOwnProperty('refresh_token'))\n{\n    flow.set('device_code',undefined);\n    node.status({fill:\"green\",shape:\"dot\",text:`device now logged in, pass on message`});\n    return [null, msg];     \n}\n\n\nif(access_token && refresh_token)\n{\n    flow.set('device_code',undefined);\n    node.status({fill:\"green\",shape:\"dot\",text:`device already logged in`});\n    return [];\n}\n\nif(msg.payload.hasOwnProperty('error'))\n{\n    if(msg.payload.error == \"authorization_pending\")\n    {\n        node.status({fill:\"blue\",shape:\"dot\",text:`Browser login pending`});\n        msg.delay = 5*1000;\n        return [msg, null]; \n    }\n    node.status({fill:\"red\",shape:\"dot\",text:`Error: ${msg.payload.error_description}`});\n    return [];\n}\n","outputs":2,"noerr":0,"initialize":"","finalize":"","x":740,"y":320,"wires":[["ddaa0b36.e42108"],["d1253feb.c9362","657e48c9.bcda48"]]},{"id":"643ed329.5bdfec","type":"debug","z":"7c76e545.92af9c","name":"Auth link and device code","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":980,"y":100,"wires":[]},{"id":"2347386d.7321d8","type":"comment","z":"7c76e545.92af9c","name":"MS Graph request (presence)","info":"[Docs](https://learn.microsoft.com/en-us/graph/api/resources/presence?view=graph-rest-beta#properties)","x":190,"y":520,"wires":[]}]

This gives you the following flow which is able to read your Microsoft Teams presence status / Microsoft Office 365 presence status with node-red:

flow showcase

  1. Double click the "change my values"-node and enter client_id, tenant_id and the used scope. The scopes default values for reading the presence are already set.
  2. Deploy the node.
  3. Launch the "launch device code request".
  4. In the debug console you will get a code which you should copy and open the given link in a browser on any device.
  5. Go through the authentication process in the browser until it shows you that the login was successful and you can close that browser window.
  6. Next click the inject node under "retrieve tokens".
  7. If the nodes to the right show green values everything was successful and you are ready to go.