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.
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.
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.
Explanation:
Now this app can be used in Node-Red for reading the presence from MS Graph API.
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: