{"id":2701,"date":"2025-09-17T08:38:11","date_gmt":"2025-09-17T06:38:11","guid":{"rendered":"https:\/\/sqlkover.com\/?p=2701"},"modified":"2025-09-17T08:40:27","modified_gmt":"2025-09-17T06:40:27","slug":"secure-logic-apps-with-oauth-authorization","status":"publish","type":"post","link":"https:\/\/sqlkover.com\/secure-logic-apps-with-oauth-authorization\/","title":{"rendered":"Secure Logic Apps with OAuth Authorization"},"content":{"rendered":"<body>\n<p>I\u2019ve used Logic Apps a couple of times over the past years for simple workflows, like sending an email (still not supported in ADF, booooo), reading a SharePoint List or copying some files from A to B (cheaper than the Copy Data Activity in ADF). When you create a Logic App with an HTTP trigger (which is most likely scenario if you\u2019re using ADF to trigger the Logic Apps), an URL is generated:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><a href=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-17.png\"><img decoding=\"async\" width=\"848\" height=\"382\" src=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-17.png\" alt=\"\" class=\"wp-image-2704\" style=\"width:608px;height:auto\" loading=\"lazy\" srcset=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-17.png 848w, https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-17-300x135.png 300w, https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-17-768x346.png 768w\" sizes=\"auto, (max-width: 848px) 100vw, 848px\" \/><\/a><\/figure>\n\n\n\n<p>It has the following format:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"html\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">https:\/\/prod-xxx.myregion.logic.azure.com:443\/workflows\/someid\/cdb\/triggers\/manual\/invoke?api-version=2016-10-01&amp;sp=%2Ftriggers%2Fmanual%2Frun&amp;sv=1.0&amp;sig=sastoken<\/pre>\n\n\n\n<p>The URL contains 4 parameters:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>the API version<\/li>\n\n\n\n<li>sp, which points to the trigger (service permission, it tells you what you can do with the URL)<\/li>\n\n\n\n<li>sv = 1.0 (SAS version)<\/li>\n\n\n\n<li>sig = SAS token<\/li>\n<\/ul>\n\n\n\n<p>Because it includes a SAS token, basically anyone who knows the URL (you\u2019d be surprised in what you can find in public git repos) can trigger your Logic App (and possibly send harmful stuff in the JSON body, such as a SQL injection attack). To avoid this, we\u2019re going to ditch the SAS token and work with an authorization policy for the Logic App. You can set this up in the Logic App settings:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><a href=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-18.png\"><img decoding=\"async\" width=\"288\" height=\"406\" src=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-18.png\" alt=\"\" class=\"wp-image-2706\" style=\"width:211px;height:auto\" loading=\"lazy\" srcset=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-18.png 288w, https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-18-213x300.png 213w\" sizes=\"auto, (max-width: 288px) 100vw, 288px\" \/><\/a><\/figure>\n\n\n\n<p>Click on <em>Add Policy<\/em> and specify the following properties:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Policy Name <\/strong>\u2013 you can choose what you want, and it doesn\u2019t need to be unique<\/li>\n\n\n\n<li><strong>OAuth Type<\/strong> \u2013 you have the options Azure Active Directory (someone forgot to update that dropdown) and Azure AD with Proof of Possession (AADPOP). I choose the first one.<\/li>\n\n\n\n<li><strong>Issuer Claims<\/strong> \u2013 this is either <em>https:\/\/sts.windows.net\/tenant-id\/<\/em> or <em>https:\/\/login.microsoftonline.com\/tenant-id\/<\/em> depending on the access token you need. I used sts.windows.net.<\/li>\n\n\n\n<li><strong>Audience Claims<\/strong> \u2013 I\u2019ve seen blog posts using <em>https:\/\/management.core.windows.net\/<\/em>, but when using ADF to trigger the Logic App you can use <em>https:\/\/management.azure.com\/<\/em>.<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-19.png\"><img decoding=\"async\" width=\"920\" height=\"350\" src=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-19.png\" alt=\"\" class=\"wp-image-2707\" loading=\"lazy\" srcset=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-19.png 920w, https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-19-300x114.png 300w, https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-19-768x292.png 768w\" sizes=\"auto, (max-width: 920px) 100vw, 920px\" \/><\/a><\/figure>\n\n\n\n<p>I cannot stress enough how important the slash is at the end of the URL. I forgot it at first, and I got errors at the ADF side: <em>\u201cOne or more claims either missing or does not match with the open authentication access control policy\u201d<\/em>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-20.png\"><img decoding=\"async\" width=\"407\" height=\"80\" src=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-20.png\" alt=\"\" class=\"wp-image-2708\" loading=\"lazy\" srcset=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-20.png 407w, https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-20-300x59.png 300w\" sizes=\"auto, (max-width: 407px) 100vw, 407px\" \/><\/a><\/figure>\n\n\n\n<p>In ADF, we can now trigger the Logic App using a Web or Webhook Activity.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-21.png\"><img decoding=\"async\" width=\"741\" height=\"459\" src=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-21.png\" alt=\"\" class=\"wp-image-2709\" loading=\"lazy\" srcset=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-21.png 741w, https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-21-300x186.png 300w\" sizes=\"auto, (max-width: 741px) 100vw, 741px\" \/><\/a><\/figure>\n\n\n\n<p>For the URL, you need to take the Logic App URL  from the trigger, but drop all parameters except the API version. For the authentication, you can take the system-assigned managed identity (no passwords or secrets that can expire) and as resource you specify the audience from the OAuth policy. For the headers, you need to specify <em>application\/json<\/em> as the <em>Content-Type<\/em>. The managed identity will fetch a token from Azure and include it as a bearer token in the HTTP headers, you don\u2019t need to do this yourself.<\/p>\n\n\n\n<p>If you would still specify the SAS token in the URL, you\u2019ll get an error saying you can\u2019t mix authentication schemes: \u201c<em>The request has SAS authentication scheme and an additional authorization scheme or internal token scheme. Only one scheme should be used.<\/em>\u201c<\/p>\n\n\n\n<p>This means that even though we specified the OAuth policy, the Logic App can still be triggered with the original URL. To avoid this, we need to modify the Logic App itself. In the trigger settings, we can specify that the Authorization HTTP header needs to include the word \u201cBearer\u201d. If this isn\u2019t the case, the Logic App won\u2019t be triggered. You can use the following expression:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">@startsWith(triggerOutputs()?['headers']?['Authorization'], 'Bearer')<\/pre>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><a href=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-22.png\"><img decoding=\"async\" width=\"597\" height=\"511\" src=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-22.png\" alt=\"\" class=\"wp-image-2710\" style=\"width:508px;height:auto\" loading=\"lazy\" srcset=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-22.png 597w, https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-22-300x257.png 300w\" sizes=\"auto, (max-width: 597px) 100vw, 597px\" \/><\/a><\/figure>\n\n\n\n<p>If you would like to view the Authorization header, you need to add the following piece to the JSON code of the Logic App trigger:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">,\"operationOptions\": \"IncludeAuthorizationHeadersInOutputs\"<\/pre>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-23.png\"><img decoding=\"async\" width=\"808\" height=\"675\" src=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-23.png\" alt=\"\" class=\"wp-image-2711\" loading=\"lazy\" srcset=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-23.png 808w, https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-23-300x251.png 300w, https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-23-768x642.png 768w\" sizes=\"auto, (max-width: 808px) 100vw, 808px\" \/><\/a><\/figure>\n\n\n\n<p>This will include the (sanitized) Authorization header into the output:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><a href=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-24.png\"><img decoding=\"async\" width=\"551\" height=\"487\" src=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-24.png\" alt=\"\" class=\"wp-image-2712\" style=\"width:462px;height:auto\" loading=\"lazy\" srcset=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-24.png 551w, https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-24-300x265.png 300w\" sizes=\"auto, (max-width: 551px) 100vw, 551px\" \/><\/a><\/figure>\n\n\n\n<p>You can also see ADF was the one that triggered the Logic App in the User-Agent header. You can view the actual bearer token by parsing the JSON headers (with the Parse JSON task):<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-26.png\"><img decoding=\"async\" width=\"395\" height=\"396\" src=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-26.png\" alt=\"\" class=\"wp-image-2714\" loading=\"lazy\" srcset=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-26.png 395w, https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-26-300x300.png 300w, https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-26-150x150.png 150w, https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-26-60x60.png 60w\" sizes=\"auto, (max-width: 395px) 100vw, 395px\" \/><\/a><\/figure>\n\n\n\n<p>This gives the following output:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-25.png\"><img decoding=\"async\" width=\"584\" height=\"426\" src=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-25.png\" alt=\"\" class=\"wp-image-2713\" loading=\"lazy\" srcset=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-25.png 584w, https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-25-300x219.png 300w\" sizes=\"auto, (max-width: 584px) 100vw, 584px\" \/><\/a><\/figure>\n\n\n\n<p>You can decode the contents of the token on <a href=\"https:\/\/www.jwt.io\/\" target=\"_blank\" rel=\"noopener\" title=\"\">https:\/\/www.jwt.io\/<\/a>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><a href=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-27.png\"><img decoding=\"async\" width=\"653\" height=\"414\" src=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-27.png\" alt=\"\" class=\"wp-image-2715\" style=\"width:513px;height:auto\" loading=\"lazy\" srcset=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-27.png 653w, https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-27-300x190.png 300w\" sizes=\"auto, (max-width: 653px) 100vw, 653px\" \/><\/a><\/figure>\n\n\n\n<p>If you want that only ADF can trigger the Logic App, you can put a condition in the Logic App on the user-agent header:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-28.png\"><img decoding=\"async\" width=\"568\" height=\"299\" src=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-28.png\" alt=\"\" class=\"wp-image-2716\" loading=\"lazy\" srcset=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-28.png 568w, https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-28-300x158.png 300w\" sizes=\"auto, (max-width: 568px) 100vw, 568px\" \/><\/a><\/figure>\n\n\n\n<p>The Logic App will still be triggered, but if it wasn\u2019t by ADF you can prevent from any actual work being done. Another option would be to decode the bearer token in the Logic App itself and check the contents to see if it was ADF, but this is out-of-scope for this blog post. You can find more info on how to do this in the blog post <a href=\"https:\/\/rakhesh.com\/azure\/parsing-json-tokens-in-a-logic-app\/\" target=\"_blank\" rel=\"noopener\" title=\"\">Parsing JSON tokens in a Logic App<\/a>.<\/p>\n\n\n\n<p>If you have a Logic App calling itself recursively (or triggering other Logic Apps), you need to use the OAuth Policy as well. In the HTTP Webhook task, you can add <strong>Subscribe Authentication<\/strong> in the Advanced parameters:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-29.png\"><img decoding=\"async\" width=\"591\" height=\"326\" src=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-29.png\" alt=\"\" class=\"wp-image-2717\" loading=\"lazy\" srcset=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-29.png 591w, https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-29-300x165.png 300w\" sizes=\"auto, (max-width: 591px) 100vw, 591px\" \/><\/a><\/figure>\n\n\n\n<p>There are different authentication types available, but as usual managed identity is our favorite.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-30.png\"><img decoding=\"async\" width=\"156\" height=\"200\" src=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-30.png\" alt=\"\" class=\"wp-image-2718\" loading=\"lazy\"><\/a><\/figure>\n\n\n\n<p>I used a user-assigned managed identity, as it is easier to manage with CI\/CD (read more about how to do this with Logic Apps <a href=\"https:\/\/sqlkover.com\/logic-apps-and-source-control-with-powershell\/\" target=\"_blank\" rel=\"noopener\" title=\"\">here<\/a> and <a href=\"https:\/\/sqlkover.com\/how-to-deploy-azure-logic-apps-with-arm-and-azure-devops\/\" target=\"_blank\" rel=\"noopener\" title=\"\">here<\/a>). When you extract the ARM template of the Logic App, you can see the OAuth Policy is included as well:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-31.png\"><img decoding=\"async\" width=\"702\" height=\"423\" src=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-31.png\" alt=\"\" class=\"wp-image-2719\" loading=\"lazy\" srcset=\"https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-31.png 702w, https:\/\/sqlkover.com\/wp-content\/uploads\/2025\/09\/image-31-300x181.png 300w\" sizes=\"auto, (max-width: 702px) 100vw, 702px\" \/><\/a><\/figure>\n\n\n\n<p><\/p>\n<\/body>","protected":false},"excerpt":{"rendered":"<p>I\u2019ve used Logic Apps a couple of times over the past years for simple workflows, like sending an email (still not supported in ADF, booooo), reading a SharePoint List or copying some files from A to B (cheaper than the Copy Data Activity in ADF). When you create a Logic App with an HTTP trigger [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[157,207],"tags":[202,154,158,201,16],"class_list":["post-2701","post","type-post","status-publish","format-standard","hentry","category-azure","category-azure-data-factory","tag-adf","tag-ae","tag-azure","tag-logic-app","tag-syndicated"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/sqlkover.com\/wp-json\/wp\/v2\/posts\/2701","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sqlkover.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sqlkover.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sqlkover.com\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/sqlkover.com\/wp-json\/wp\/v2\/comments?post=2701"}],"version-history":[{"count":3,"href":"https:\/\/sqlkover.com\/wp-json\/wp\/v2\/posts\/2701\/revisions"}],"predecessor-version":[{"id":2721,"href":"https:\/\/sqlkover.com\/wp-json\/wp\/v2\/posts\/2701\/revisions\/2721"}],"wp:attachment":[{"href":"https:\/\/sqlkover.com\/wp-json\/wp\/v2\/media?parent=2701"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sqlkover.com\/wp-json\/wp\/v2\/categories?post=2701"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sqlkover.com\/wp-json\/wp\/v2\/tags?post=2701"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}