Back
Featured image of post Building Resource Manager Templates for Azure API Management & Logic Apps

Building Resource Manager Templates for Azure API Management & Logic Apps

Azure Logic Apps are a great tool to look at when addressing simple or complex system integration requirements, and one which experienced Power Platform developers should instantly be familiar with, given they form the backbone of Power Automate flows. As a tool with such a wide array of connectors and features, they can often negate the need to write bespoke code to achieve your particular integration scenario. They are also an excellent candidate to consider when you need to expose out HTTP endpoints for requests to be processed through, thanks to the HTTP request trigger. Incidentally, this is also available as part of Power Automate flows too.

As powerful as HTTP trigger requests are, you will need to rely on other Azure solutions to harden the security aspects around this. For example, it is impossible to currently leverage OAuth 2.0 / Azure Active Directory (AAD) authentication within Logic Apps themselves. Instead, we must turn to solutions like Azure API Management (APIM) to meet this requirement. Additional challenges can also arise if you are adopting an Infrastructure as code mindset, and wish to have all of your Azure resources stored within a managed template, that you can then use to automate your solution deployments. Although Azure allows you to export out the definitions of an APIM resource from within the portal, the resulting template is missing several crucial components, meaning that you will have considerable difficulties importing this back in at a future date.

Bearing all this in mind, and some of the challenges I had when dealing with a similar requirement “in the field”, I thought it might be useful to share aspects of the solution that came out of this. Therefore, the focus for today’s blog post will be twofold:

  1. To demonstrate how Azure Logic Apps can be leveraged alongside APIM, using OAuth 2.0 / AAD authentication to validate all incoming requests.
  2. To break down how to create a complete APIM API endpoint using an Azure Resource Manager template.

The scenario we will work through assumes that you already have an Azure Logic App resource set up within the same Azure template file and that, also, you plan to deploy this to the same subscription/resource group as your APIM resource. For this example, we will assume that our Logic App resource is called mylogicapp and that it satisfies these conditions already. If this is not the case, then you may need to review the template further and replace instances where the functions resourceGroup().name and subscription().subscriptionId are used with hardcoded values instead. With all this out of the way, let us begin! 🤓

The API Management Resource Itself

To kick things off, we must first ensure our top-level APIM resource exists in our targeted subscription or that you’ve added it already as part of a template. We will opt for the latter in this instance. Also, because we are feeling particularly wallet-conscious these days, we will create this resource using the consumption-based offering and configure the resource only to deploy once our Logic App resource has deployed successfully:

{
  "type": "Microsoft.ApiManagement/service",
  "apiVersion": "2019-12-01",
  "name": "myapim",
  "location": "uksouth",
  "sku": {
    "name": "Consumption",
    "capacity": 0
  },
  "dependsOn": [
    "[resourceId('Microsoft.Logic/workflows', 'mylogicapp')]"
  ],
  "tags": {
    "displayName": "API Management Service Sample"
  },
  "properties": {
    "publisherEmail": "[email protected]",
    "publisherName": "Company ABC",
    "hostnameConfigurations": [
      {
        "type": "Proxy",
        "hostName": "myapim.azure-api.net",
        "negotiateClientCertificate": false,
        "defaultSslBinding": true
      }
    ],
    "customProperties": {
      "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls10": "False",
      "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls11": "False",
      "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls10": "False",
      "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls11": "False",
      "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Ssl30": "False",
      "Microsoft.WindowsAzure.ApiManagement.Gateway.Protocols.Server.Http2": "False"
    },
    "virtualNetworkType": "None",
    "apiVersionConstraint": {}
  }
}

Note that this will just create an empty APIM resource, with nothing specified underneath it. The sections that follow will build out APIM further with everything we need.

OAuth 2.0 Configuration Profile

The first sub-resource (if we can call it that) will contain the required settings for OAuth 2.0 authentication. This component goes first because it will be a required dependency as part of the API interface itself. As part of this, you will need to go through the steps outlined in this article to generate all of the required Application Registrations, URL’s, secrets and scopes. With all that out of the way, the RM template definition would resemble the below:

{
  "type": "Microsoft.ApiManagement/service/authorizationServers",
  "apiVersion": "2019-12-01",
  "name": "myapim/my-aad",
  "dependsOn": [
    "[resourceId('Microsoft.ApiManagement/service', 'myapim')]"
  ],
  "tags": {
    "displayName": "OAuth 2.0 Configuration Profile"
  },
  "properties": {
    "displayName": "My AAD",
    "description": "OAuth2 authorization service for Azure Active Directory (AAD) tenant.",
    "clientRegistrationEndpoint": "http://localhost",
    "authorizationEndpoint": "https://login.microsoftonline.com/c77948f0-6777-43af-8daf-6b36a1911ee0/oauth2/v2.0/authorize",
    "authorizationMethods": [
      "GET",
      "POST"
    ],
    "clientAuthenticationMethod": [
      "Body"
    ],
    "tokenBodyParameters": [],
    "tokenEndpoint": "https://login.microsoftonline.com/c77948f0-6777-43af-8daf-6b36a1911ee0/oauth2/v2.0/token",
    "supportState": false,
    "defaultScope": "api://fb10fac9-3aa4-41e3-be2e-5fad73beca13/API.AllOperations",
    "grantTypes": [
      "clientCredentials"
    ],
    "bearerTokenSendingMethods": [
      "authorizationHeader"
    ],
    "clientId": "7949079d-082d-4d37-bc71-898eafbd50c5",
    "clientSecret": "myclientsecret"
  }
}

The main properties you will have to update include:

  • authorizationEndpoint
  • clientRegistrationEndpoint (where applicable and depending on what your app is doing)
  • tokenEndpoint
  • grantTypes (if you don’t wish to use client_credentials)
  • clientId
  • clientSecret
  • defaultScope

API Interface

With our OAuth 2.0 authentication profile specified, we have all the necessary dependencies in place to build out our single API interface, which we will call My API and which will support https calls only. The template below will create this for us:

{
  "type": "Microsoft.ApiManagement/service/apis",
  "apiVersion": "2019-12-01",
  "name": "myapim/myapi",
  "dependsOn": [
    "[resourceId('Microsoft.ApiManagement/service', 'myapim')]",
    "[resourceId('Microsoft.ApiManagement/service/authorizationServers', 'myapim', 'my-aad')]"
  ],
  "tags": {
    "displayName": "My API"
  },
  "properties": {
    "displayName": "My API",
    "subscriptionRequired": false,
    "protocols": [
      "https"
    ],
    "authenticationSettings": {
      "oAuth2": {
        "authorizationServerId": "my-aad",
        "scope": null
      },
      "openid": null
    },
    "isCurrent": true,
    "path": ""
  }
}

Apart from the name values (including the ones specified underneath the dependsOn node), be sure to change the authorizationServerId to match the name of the OAuth 2.0 profile created in the previous step.

Backend Endpoint

APIM works on the principle that you specify the URL details, or backend endpoints, of the services that you wish to interact with via the API itself. When creating this through the Azure Portal, we have the option of selecting either a Logic App or Azure Function app as our targeted resource, with Azure handling all of the appropriate configurations for this on our behalf. To mimic this within an Azure RM template, therefore, we have to crack open a few template functions to build out the appropriate URL’s to reference back to the Logic App we wish to expose via APIM. With this in mind, the following example demonstrates this in practice:

{
  "type": "Microsoft.ApiManagement/service/backends",
  "apiVersion": "2019-12-01",
  "name": "myapim/mylogicapp",
  "dependsOn": [
    "[resourceId('Microsoft.ApiManagement/service', 'myapim')]",
    "[resourceId('Microsoft.Logic/workflows', 'mylogicapp')]"
  ],
  "tags": {
    "displayName": "Backend Endpoint for Logic App"
  },
  "properties": {
    "description": "Logic App",
    "url": "[substring(listCallbackUrl(resourceId(resourceGroup().name, 'Microsoft.Logic/workflows/triggers', 'mylogicapp', 'manual'), '2017-07-01').basePath,0,add(10,indexOf(listCallbackUrl(resourceId(resourceGroup().name, 'Microsoft.Logic/workflows/triggers', 'mylogicapp', 'manual'), '2017-07-01').basePath,'/triggers/')))]",
    "protocol": "http",
    "resourceId": "[concat('https://management.azure.com/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Logic/workflows/mylogicapp')]"
  }
}

Named Value

Next, we must provide a description - a named value - that describes the operation we are performing against the backend API. In this case, our Logic App exposes a PATCH operation that updates a Common Data Service Account record, so we use a name that reflects this:

{
  "type": "Microsoft.ApiManagement/service/namedValues",
  "apiVersion": "2019-12-01",
  "name": "myapim/nv",
  "dependsOn": [
    "[resourceId('Microsoft.ApiManagement/service', 'myapim')]"
  ],
  "tags": {
    "displayName": "Named Value for Logic App"
  },
  "properties": {
    "displayName": "patch_account",
    "tags": [],
    "secret": true,
    "value": "/"
  }
}

Operation

In addition to the Named Value, it is also necessary to specify the actual operation that callers will perform against each endpoint. Azure then references this operation back to the named value resource created earlier. In this case, we want our PATCH operations to target /account at the end of our APIM default URL; therefore, we use the following template snippet to achieve this:

{
  "type": "Microsoft.ApiManagement/service/apis/operations",
  "apiVersion": "2019-12-01",
  "name": "myapim/myapi/patch_account",
  "dependsOn": [
    "[resourceId('Microsoft.ApiManagement/service/apis', 'myapim', 'My API')]",
    "[resourceId('Microsoft.ApiManagement/service', 'myapim')]"
  ],
  "tags": {
    "displayName": "Operations for Logic App"
  },
  "properties": {
    "displayName": "Accounts",
    "method": "PATCH",
    "urlTemplate": "/account",
    "templateParameters": [],
    "responses": []
  }
}

API Policy Configuration

Within APIM, it is possible to enforce certain policies at the API level, which APIM will then obey for all requests that pass through. For example, you forcibly add, remove or modify query parameters passed through by the caller. It’s worth reading through the whole Microsoft Docs article on this very subject, so you can get a feel for what is possible. For the scenario we are working through, considering that we wish to utilise OAuth 2.0 authentication for our API, it makes sense to enforce this at the API level. Also, when receiving requests back from our backend Logic App endpoint, a variety of different header values are returned to the caller. These are more for diagnosis/debugging purposes and, ideally, should be hidden from sight. To achieve all of this within APIM, we build out an XML definition for these policies, which we can then apply as a separate resource for APIM using the following example:

{
  "type": "Microsoft.ApiManagement/service/apis/policies",
  "apiVersion": "2019-12-01",
  "name": "myapim/myapi/policy",
  "dependsOn": [
    "[resourceId('Microsoft.ApiManagement/service/apis', 'myapim', 'myapi')]",
    "[resourceId('Microsoft.ApiManagement/service', 'myapim')]"
  ],
  "tags": {
    "displayName": "Policy configuration for APIM"
  },
  "properties": {
    "value": "<!--\r\n    IMPORTANT:\r\n    - Policy elements can appear only within the <inbound>, <outbound>, <backend> section elements.\r\n    - To apply a policy to the incoming request (before it is forwarded to the backend service), place a corresponding policy element within the <inbound> section element.\r\n    - To apply a policy to the outgoing response (before it is sent back to the caller), place a corresponding policy element within the <outbound> section element.\r\n    - To add a policy, place the cursor at the desired insertion point and select a policy from the sidebar.\r\n    - To remove a policy, delete the corresponding policy statement from the policy document.\r\n    - Position the <base> element within a section element to inherit all policies from the corresponding section element in the enclosing scope.\r\n    - Remove the <base> element to prevent inheriting policies from the corresponding section element in the enclosing scope.\r\n    - Policies are applied in the order of their appearance, from the top down.\r\n    - Comments within policy elements are not supported and may disappear. Place your comments between policy elements or at a higher level scope.\r\n-->\r\n<!--\r\n    Inbound policies:\r\n    - Enforce OAuth2 authorization\r\n    - Remove OAuth2 Authorization Bearer before passing request to Logic App\r\n    Outbound policies:\r\n    - Remove all x headers returned by the Logic App\r\n-->\r\n<policies>\r\n  <inbound>\r\n    <validate-jwt header-name=\"Authorization\" failed-validation-httpcode=\"401\" failed-validation-error-message=\"Unauthorized. Access token is missing or invalid.\">\r\n      <openid-config url=\"https://login.microsoftonline.com/c77948f0-6777-43af-8daf-6b36a1911ee0/.well-known/openid-configuration \" />\r\n      <required-claims>\r\n        <claim name=\"aud\">\r\n          <value>api://fb10fac9-3aa4-41e3-be2e-5fad73beca13</value>\r\n        </claim>\r\n      </required-claims>\r\n    </validate-jwt>\r\n    <set-header name=\"Authorization\" exists-action=\"delete\" />\r\n  </inbound>\r\n  <backend>\r\n    <base />\r\n  </backend>\r\n  <outbound>\r\n    <set-header name=\"x-ms-tracking-id\" exists-action=\"delete\" />\r\n    <set-header name=\"x-ms-request-id\" exists-action=\"delete\" />\r\n    <set-header name=\"x-ms-workflow-run-id\" exists-action=\"delete\" />\r\n    <set-header name=\"x-ms-correlation-id\" exists-action=\"delete\" />\r\n    <set-header name=\"x-ms-client-tracking-id\" exists-action=\"delete\" />\r\n    <set-header name=\"x-ms-trigger-history-name\" exists-action=\"delete\" />\r\n    <set-header name=\"x-ms-execution-location\" exists-action=\"delete\" />\r\n    <set-header name=\"x-ms-workflow-id\" exists-action=\"delete\" />\r\n    <set-header name=\"x-ms-workflow-version\" exists-action=\"delete\" />\r\n    <set-header name=\"x-ms-workflow-name\" exists-action=\"delete\" />\r\n    <set-header name=\"x-ms-workflow-system-id\" exists-action=\"delete\" />\r\n    <set-header name=\"x-ms-ratelimit-burst-remaining-workflow-writes\" exists-action=\"delete\" />\r\n    <set-header name=\"x-ms-ratelimit-remaining-workflow-download-contentsize\" exists-action=\"delete\" />\r\n    <set-header name=\"x-ms-ratelimit-time-remaining-directapirequests\" exists-action=\"delete\" />\r\n    <set-header name=\"x-ms-ratelimit-remaining-workflow-upload-contentsize\" exists-action=\"delete\" />\r\n    <set-header name=\"x-ms-ratelimit-burst-remaining-workflow-reads\" exists-action=\"delete\" />\r\n  </outbound>\r\n  <on-error>\r\n    <base />\r\n  </on-error>\r\n</policies>",
    "format": "xml"
  }
}

The XML in this example is a little difficult to read, so I’ve included it below so you can see what its doing:

<!--
    IMPORTANT:
    - Policy elements can appear only within the <inbound>, <outbound>, <backend> section elements.
    - To apply a policy to the incoming request (before it is forwarded to the backend service), place a corresponding policy element within the <inbound> section element.
    - To apply a policy to the outgoing response (before it is sent back to the caller), place a corresponding policy element within the <outbound> section element.
    - To add a policy, place the cursor at the desired insertion point and select a policy from the sidebar.
    - To remove a policy, delete the corresponding policy statement from the policy document.
    - Position the <base> element within a section element to inherit all policies from the corresponding section element in the enclosing scope.
    - Remove the <base> element to prevent inheriting policies from the corresponding section element in the enclosing scope.
    - Policies are applied in the order of their appearance, from the top down.
    - Comments within policy elements are not supported and may disappear. Place your comments between policy elements or at a higher level scope.
-->
<!--
    Inbound policies:
    - Enforce OAuth2 authorization
    - Remove OAuth2 Authorization Bearer before passing request to Logic App
    Outbound policies:
    - Remove all x headers returned by the Logic App
-->
<policies>
    <inbound>
        <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid.">
            <openid-config url="https://login.microsoftonline.com/c77948f0-6777-43af-8daf-6b36a1911ee0/.well-known/openid-configuration" />
            <required-claims>
                <claim name="aud">
                    <value>api://fb10fac9-3aa4-41e3-be2e-5fad73beca13</value>
                </claim>
            </required-claims>
        </validate-jwt>
        <set-header name="Authorization" exists-action="delete" />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <set-header name="x-ms-tracking-id" exists-action="delete" />
        <set-header name="x-ms-request-id" exists-action="delete" />
        <set-header name="x-ms-workflow-run-id" exists-action="delete" />
        <set-header name="x-ms-correlation-id" exists-action="delete" />
        <set-header name="x-ms-client-tracking-id" exists-action="delete" />
        <set-header name="x-ms-trigger-history-name" exists-action="delete" />
        <set-header name="x-ms-execution-location" exists-action="delete" />
        <set-header name="x-ms-workflow-id" exists-action="delete" />
        <set-header name="x-ms-workflow-version" exists-action="delete" />
        <set-header name="x-ms-workflow-name" exists-action="delete" />
        <set-header name="x-ms-workflow-system-id" exists-action="delete" />
        <set-header name="x-ms-ratelimit-burst-remaining-workflow-writes" exists-action="delete" />
        <set-header name="x-ms-ratelimit-remaining-workflow-download-contentsize" exists-action="delete" />
        <set-header name="x-ms-ratelimit-time-remaining-directapirequests" exists-action="delete" />
        <set-header name="x-ms-ratelimit-remaining-workflow-upload-contentsize" exists-action="delete" />
        <set-header name="x-ms-ratelimit-burst-remaining-workflow-reads" exists-action="delete" />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

Signature Property

We’re on the home stretch now! 😅 To prevent malicious users from connecting to Logic App endpoints and causing all manner of mischief, Microsoft requires that all requests contain a shared access signature within the URL when calling the endpoint directly. If this does not exist, then you are politely and firmly told to go away. APIM is no exception in this regard, so we must ensure to append this value to each backend request made to Logic App, provided that the user authenticates via OAuth 2.0 in the first instance. The signature value can be stored a secret property value within APIM and referenced elsewhere, so this is what we do in this instance:

{
  "type": "Microsoft.ApiManagement/service/properties",
  "apiVersion": "2019-01-01",
  "name": "myapim/la_sig",
  "dependsOn": [
    "[resourceId('Microsoft.ApiManagement/service', 'myapim')]"
  ],
  "tags": {
    "displayName": "Signature property for Logic App"
  },
  "properties": {
    "displayName": "la_sig",
    "value": "[listCallbackURL(concat(resourceId('Microsoft.Logic/workflows', 'mylogicapp'), '/triggers/manual'), '2017-07-01').queries.sig]",
    "tags": [],
    "secret": true
  }
}

Logic App Policy Configuration

We’ve already created a policy for everything that sits underneath our API, but now we need to create a specific policy that applies to our PATCH endpoint. This policy achieves the function already alluded to in the previous section; namely, in ensuring that APIM appends the shared access signature onto the Logic Apps URL when making connections to its endpoint. In this case, we must use some slightly different policy settings to enforce this behaviour, which you can view below:

{
  "type": "Microsoft.ApiManagement/service/apis/operations/policies",
  "apiVersion": "2019-12-01",
  "name": "myapim/myapi/patch_account/policy",
  "dependsOn": [
    "[resourceId('Microsoft.ApiManagement/service/apis/operations', 'myapim', 'myapi', 'patch_account')]",
    "[resourceId('Microsoft.ApiManagement/service/apis', 'myapim', 'myapi')]",
    "[resourceId('Microsoft.ApiManagement/service', 'myapim')]",
    "[resourceId('Microsoft.ApiManagement/service/properties', 'myapim', 'la_sig')]"
  ],
  "tags": {
    "displayName": "Policy configuration for Logic App"
  },
  "properties": {
    "value": "<!--\r\n    IMPORTANT:\r\n    - Policy elements can appear only within the <inbound>, <outbound>, <backend> section elements.\r\n    - To apply a policy to the incoming request (before it is forwarded to the backend service), place a corresponding policy element within the <inbound> section element.\r\n    - To apply a policy to the outgoing response (before it is sent back to the caller), place a corresponding policy element within the <outbound> section element.\r\n    - To add a policy, place the cursor at the desired insertion point and select a policy from the sidebar.\r\n    - To remove a policy, delete the corresponding policy statement from the policy document.\r\n    - Position the <base> element within a section element to inherit all policies from the corresponding section element in the enclosing scope.\r\n    - Remove the <base> element to prevent inheriting policies from the corresponding section element in the enclosing scope.\r\n    - Policies are applied in the order of their appearance, from the top down.\r\n    - Comments within policy elements are not supported and may disappear. Place your comments between policy elements or at a higher level scope.\r\n-->\r\n<policies>\r\n  <inbound>\r\n    <base />\r\n    <set-backend-service id=\"apim-generated-policy\" backend-id=\"LogicApp_mylogicapp\" />\r\n    <set-method id=\"apim-generated-policy\">PATCH</set-method>\r\n    <rewrite-uri id=\"apim-generated-policy\" template=\"/manual/paths/invoke/?api-version=2016-06-01&amp;sp=/triggers/manual/run&amp;sv=1.0&amp;sig={{la_sig}}\" />\r\n   <set-header id=\"apim-generated-policy\" name=\"Ocp-Apim-Subscription-Key\" exists-action=\"delete\" />\r\n  </inbound>\r\n  <backend>\r\n    <base />\r\n  </backend>\r\n  <outbound>\r\n    <base />\r\n  </outbound>\r\n  <on-error>\r\n    <base />\r\n  </on-error>\r\n</policies>",
    "format": "xml"
  }
}

Again, here’s the raw XML of the definition so you can see what’s going on:

<!--
    IMPORTANT:
    - Policy elements can appear only within the <inbound>, <outbound>, <backend> section elements.
    - To apply a policy to the incoming request (before it is forwarded to the backend service), place a corresponding policy element within the <inbound> section element.
    - To apply a policy to the outgoing response (before it is sent back to the caller), place a corresponding policy element within the <outbound> section element.
    - To add a policy, place the cursor at the desired insertion point and select a policy from the sidebar.
    - To remove a policy, delete the corresponding policy statement from the policy document.
    - Position the <base> element within a section element to inherit all policies from the corresponding section element in the enclosing scope.
    - Remove the <base> element to prevent inheriting policies from the corresponding section element in the enclosing scope.
    - Policies are applied in the order of their appearance, from the top down.
    - Comments within policy elements are not supported and may disappear. Place your comments between policy elements or at a higher level scope.
-->
<policies>
    <inbound>
        <base />
        <set-backend-service id="apim-generated-policy" backend-id="LogicApp_mylogicapp" />
        <set-method id="apim-generated-policy">PATCH</set-method>
        <rewrite-uri id="apim-generated-policy" template="/manual/paths/invoke/?api-version=2016-06-01&amp;sp=/triggers/manual/run&amp;sv=1.0&amp;sig={{la_sig}}" />
        <set-header id="apim-generated-policy" name="Ocp-Apim-Subscription-Key" exists-action="delete" />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

And with that, our template is complete - deploying all of these resources out will provide us with a fully functioning API endpoint that connects up to our Logic App and which also enforces OAuth 2.0 authentication for all requests - nice!

Getting your head around building your first Azure API Management resource manager template can be a little challenging. Hopefully, the examples shown in this blog post will help speed you along. If you have any questions or issues relating to this subject, give me a shout 🙂

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