0

有没有办法在不使用 Node-Red 中的 ROPC 身份验证流的情况下将 Microsoft Identiy Platform 与 OAuth 2.0 流一起使用?我不能使用 ROPC,因为目标租户强制执行 MFA。强制执行 MFA 时,ROPC 将被阻止

我找到了插件node-red-contrib-oauth2,但无法通过除 ROPC 之外的另一个 OAuth 2.0 流程与 Microsoft Identity Platform 一起使用。

4

1 回答 1

0

解决方案是使用Device Code Flow。以下说明为您提供了一个流程,该流程能够使用节点红色读取您的Microsoft Teams 在线状态/ Microsoft Office 365 在线状态。

在 Azure 门户中创建应用程序

在下面的示例中,我们将创建一个只能读取登录用户状态的应用程序。这意味着,API 权限可能会根据您的需要而有所不同。

  1. 登录 Azure 门户并转到 Azure Active Directory,然后转到“应用程序注册”并单击“+ 新注册”
  2. 键入有助于识别应用程序的友好名称。
  3. 支持的帐户:仅此组织目录中的帐户(* - 单租户)
  4. 重定向 URI:留空,因为我们稍后会设置它。
  5. 点击“注册”
  6. 转到“API 权限”。
  7. 删除默认权限,因为在场不需要它。
  8. 点击“+ 添加权限”。
  9. 单击“Microsoft Graph”,然后单击“委派权限”。
  10. 选择“offline_access”和“Presence.Read”并使用“添加权限”保存

解释:


  1. 然后,您必须通过单击“+ 添加权限按钮”旁边的“授予管理员同意*”按钮来“管理员同意这些权限”。
  2. 导航回“概述”并复制“应用程序(客户端)ID”、“目录(租户)ID”的值。
  3. 在导航抽屉中选择“身份验证”。
  4. 点击“+添加平台”
  5. 选择“移动和桌面应用程序”
  6. 选择“https://login.microsoftonline.com/common/oauth2/nativeclient”
  7. 使用“配置”提交
  8. 向下滚动到“高级设置”,然后选择“将应用程序视为公共客户端”旁边的“是”。并通过左上角的“保存”提交。

现在这个应用程序可以在 Node-Red 中用于从 MS Graph API 读取存在。

在 Node-Red 中使用此应用程序

这种流程的一个很好的起点是:

[{"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://docs.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://docs.microsoft.com/en-us/graph/api/resources/presence?view=graph-rest-beta#properties)","x":190,"y":520,"wires":[]}]

这为您提供了以下流程,该流程能够使用 node-red读取您的Microsoft Teams 在线状态/ Microsoft Office 365 在线状态:

流量展示

  1. 双击“更改我的值”节点并输入 client_id、tenant_id 和使用的范围。已经设置了用于读取状态的范围默认值。
  2. 部署节点。
  3. 启动“启动设备代码请求”。
  4. 在调试控制台中,您将获得一个代码,您应该复制并在任何设备上的浏览器中打开给定的链接。
  5. 在浏览器中完成身份验证过程,直到它显示登录成功,您可以关闭该浏览器窗口。
  6. 接下来单击“检索令牌”下的注入节点。
  7. 如果右侧的节点显示绿色值,则一切都成功了,您就可以开始了。
于 2020-10-13T18:13:58.227 回答