Featured image of post Securing Access to Azure Functions from Logic Apps via Azure Templates

Securing Access to Azure Functions from Logic Apps via Azure Templates

A great benefit when working with Azure Logic Apps in conjunction with Azure Functions is that setting up secure authentication between the two services is a cakewalk. Security considerations like these can be important if, for example, you don’t want anonymous callers triggering functions that may create or retrieve business-sensitive information. Microsoft provides step-by-step instructions on how to set this all up, all of which is useful. What’s less clear is whether we can replicate this setup within the confines of an Azure Template file. Regular readers of the blog should be familiar with this topic, as I’ve spoken previously about how to use templates to build out an Azure API Management solution leveraging Logic Apps and discussed the process for resolving pesky errors during deployments. For those still wondering exactly what they are, in a nutshell, Azure templates allow you to define the entire structure of your Azure estate as part of a raw, JSON definition file. Why is this beneficial? For a few reasons:

  • Template files can be stored within your source control provider of choice, providing total visibility over changes made to your template at an insanely granular level.
  • By using dependsOn sequencing within a template file, you can define the precise deployment order of your resources, and then cross-reference information across multiple resources. For example, you can create a Logic Apps connection profile resource and then leverage it within all of your Logic Apps within the same template file.
  • By adopting template files alongside automated deployments, you can reduce the need to carry out manual intervention or changes to critical infrastructure; all of which introduces the inherent risk of human error. If you are using tools such as Azure DevOps Release Pipelines, it is easier said then done to get deployment pipelines setup.

So the benefits of Azure Templates should be apparent. Which, as a consequence, makes it pretty desirable to use them to define Logic App and Functions that can securely communicate with each other. This post will show you how we can do this, thereby allowing you to forego the need for any manual configuration of these resources post-deployment

First of all, make sure that you have enabled the managed identity for the Logic App that needs to communicate with the Azure Function. The code snippet below will enable the system-assigned variant of this, which should be suitable for most scenarios:

{
  "type": "Microsoft.Logic/workflows",
  "apiVersion": "2017-07-01",
  "name": "MyLogicApp",
  "location": "uksouth",
  "Identity": {
    "type": "SystemAssigned"
  },
  "dependsOn": [
    "[resourceId('Microsoft.Web/sites', 'MyFunctionApp')]",
    "[resourceId('Microsoft.Web/sites/functions', 'MyFunctionApp', 'MyFunction')]"
  ],
  "properties": {
    "state": "Enabled",
    "definition": {
      "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
      "actions": {},
      "contentVersion": "1.0.0.0",
      "outputs": {},
      "Parameters": {},
      "triggers": {}
    },
    "Parameters": {}
  },
  "resources": []
}

Within your Logic App definition itself, you should also have the appropriate action step that triggers your Azure Function. The step should resemble the below, tweaked accordingly to suit your scenario; however, the values in the authentication node are the crucial bits to getting this all working and should remain unchanged:

{
  "My_Function_App_Action": {
    "inputs": {
      "authentication": {
        "audience": "https://management.azure.com",
        "type": "ManagedServiceIdentity"
      },
      "body": "Sample Body Text",
      "function": {
        "id": "[concat(resourceid('Microsoft.Web/sites', 'MyFunctionApp', '/functions/MyFunction')]"
      },
      "queries": {
        "myquery": "My Query Value"
      }
    },
    "runAfter": {},
    "type": "Function"
  }
}

That’s everything you need for the Logic App. Next, we move across to the Function App itself. There’s nothing particularly noteworthy about the Microsoft.Web/sites template definition itself, which should resemble the examples Microsoft provide us. It’s when we get into the sub-resources that sit within this that things start to get interesting…

Attempting to create the Logic App specified earlier without the selected Azure Function existing already will cause a deployment error. As a consequence, we must also create this alongside our Logic App and Function during the same deployment, using the code snippet highlighted below.

{
  "type": "Microsoft.Web/sites/functions",
  "apiVersion": "2018-11-01",
  "name": "MyFunctionApp/MyFunction",
  "location": "uksouth",
  "dependsOn": [
    "[resourceId('Microsoft.Web/sites', 'MyFunctionApp')]"
  ],
  "properties": {
    "name": "MyFunction",
    "config": {
      "bindings": [
        {
          "type": "httpTrigger",
          "methods": [
            "post"
          ],
          "authLevel": "anonymous",
          "name": "req"
        }
      ]
    }
  }
}

What this will effectively do is create a blank function in your app, that contains not a single line of code whatsoever. What you would then have to do - either from Visual Studio or as part of an Azure DevOps Release pipeline - is deploy this out to the app as a separate step. Although I believe it’s possible to include the actual code for your app within an Azure Template, I can’t seem to find a straightforward way to do this for a C# Function App. Answers on a postcard if you’ve figured out an easy way of doing this. 🙂 It’s also worth noting that the authLevel value of anonymous is a strict requirement for this solution to work. You should, therefore, take steps to ensure that your Function App is programmed to use this setting. Otherwise, you accept the risk of having this accidentally overwritten when you deploy it out.

In its current state so far, the Function is deployed and is accessible. However, because of the authLevel setting, any Tom, Dick or Harry the world over has free reign to access your Function App. To start to lock things down, therefore, we must enable a specific Authentication / Authorization configuration for the Function App, that will lock things down so that our Logic App is the only object that can interact with our app. We define this profile as part of a Microsoft.Web/sites/config resource within the Azure Template file, meaning that we can build out an additional sub-resource that sets everything up for us:

{
  "name": "MyFunctionApp/authsettings",
  "type": "Microsoft.Web/sites/config",
  "apiVersion": "2018-11-01",
  "dependsOn": [
    "[resourceId('Microsoft.Web/sites', 'MyFunctionApp')]",
    "[resourceId('Microsoft.Logic/workflows', 'MyLogicApp')]"
  ],
  "properties": {
    "enabled": true,
    "unauthenticatedClientAction": "RedirectToLoginPage",
    "tokenStoreEnabled": true,
    "defaultProvider": "AzureActiveDirectory",
    "clientId": "[reference(resourceId('Microsoft.Logic/workflows', 'MyLogicApp'), '2017-07-01', 'full').identity.principalId]",
    "issuer": "https://sts.windows.net/e275677a-5d5b-4057-8ab5-0e7bc594bc42",
    "allowedAudiences": [
      "https://management.azure.com"
    ],
    "isAadAutoProvisioned": false
  }
}

The only setting you will need to modify here is the issuer value, which needs to have the ID value of the current Azure Active Directory tenant you are deploying the template to. And, before you get too excited, thanks to some of the wonderful tools available online, the value supplied above is completely random. We must also tip our hat here to the excellent reference template function, which allows us to grab the Managed Identity ID value from a Logic App created as part of the same template deployment - nice!

And, with that added on, we’ve got everything needed to secure the Function App correctly. For completeness, here is the entire snippet for the Function App and its related resources:

{
  "apiVersion": "2019-08-01",
  "type": "Microsoft.Web/sites",
  "name": "MyFunctionApp",
  "location": "uksouth",
  "kind": "functionapp",
  "dependsOn": [
    "[resourceId('Microsoft.Storage/storageAccounts', 'MyFunctionAppSA')]"
  ],
  "properties": {
    "siteConfig": {
      "appSettings": [
        {
          "name": "AzureWebJobsStorage",
          "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', 'MyFunctionAppSA', ';EndpointSuffix=', environment().suffixes.storage, ';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', 'MyFunctionAppSA'), '2019-06-01').keys[0].value)]"
        },
        {
          "name": "FUNCTIONS_EXTENSION_VERSION",
          "value": "~2"
        },
        {
          "name": "WEBSITE_NODE_DEFAULT_VERSION",
          "value": "~10"
        },
        {
          "name": "FUNCTIONS_WORKER_RUNTIME",
          "value": "dotnet"
        }
      ]
    }
  },
  "resources": [
    {
      "type": "Microsoft.Web/sites/functions",
      "apiVersion": "2018-11-01",
      "name": "MyFunctionApp/MyFunction",
      "location": "uksouth",
      "dependsOn": [
        "[resourceId('Microsoft.Web/sites', 'MyFunctionApp')]"
      ],
      "properties": {
        "name": "MyFunction",
        "config": {
          "bindings": [
            {
              "type": "httpTrigger",
              "methods": [
                "post"
              ],
              "authLevel": "anonymous",
              "name": "req"
            }
          ]
        }
      }
    },
    {
      "name": "MyFunctionApp/authsettings",
      "type": "Microsoft.Web/sites/config",
      "apiVersion": "2018-11-01",
      "dependsOn": [
        "[resourceId('Microsoft.Web/sites', 'MyFunctionApp')]",
        "[resourceId('Microsoft.Logic/workflows', 'MyLogicApp')]"
      ],
      "properties": {
        "enabled": true,
        "unauthenticatedClientAction": "RedirectToLoginPage",
        "tokenStoreEnabled": true,
        "defaultProvider": "AzureActiveDirectory",
        "clientId": "[reference(resourceId('Microsoft.Logic/workflows', 'MyLogicApp'), '2017-07-01', 'full').identity.principalId]",
        "issuer": "https://sts.windows.net/e275677a-5d5b-4057-8ab5-0e7bc594bc42",
        "allowedAudiences": [
          "https://management.azure.com"
        ],
        "isAadAutoProvisioned": false
      }
    },
    {
      "type": "Microsoft.Storage/storageAccounts",
      "name": "MyFunctionAppSA",
      "apiVersion": "2019-04-01",
      "location": "uksouth",
      "kind": "StorageV2",
      "sku": {
        "name": "Standard_LRS"
      }
    }
  ]
}

Now, whenever we attempt to make an anonymous request to the endpoint, we should get an error message similar to the below:

Whereas requests made from our Logic App will complete successfully:

It’s good to know that we can straightforwardly intuit the steps to set all this up, thanks to both the documentation available for the manual steps and our ability to fully interrogate the inner workings of Azure Resources, using tools such as PowerShell or the Resource Explorer. A potential limitation of this whole solution though is that it limits your Function App to communicate to a single Logic App only. As far as I know, there is no way to specify additional Managed Identities as part of the Authentication / Authorization configuration. You could get around this issue by having different Logic Apps calling a central Logic App, with this being the one that’s hooked up to your Azure Function. However, I’d hope that the solution outlined in this post is sufficient for most scenarios and provides the necessary assurance that we can secure our cloud resources appropriately, using the excellent capabilities included within Azure Active Directory.

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy