Logic Apps and Azure Active Directory

Christopher Brumm
14 min readFeb 8, 2022

--

Securing the Logic App trigger and using Managed Identities

Table of Content

  1. Intro
  2. Architecture overview
  3. Basic structure
    Step 1: Creation of the Logic App (+Testing)
    Step 2: Preparation of the app registrations (+Testing)
  4. Some learnings on App Registrations
    App Roles vs. API Permissions
    App ID URI and Redirect URI
    V1 and V2 Endpoints
    Certificate-based authentication
  5. Access Control
    Step 3: Authorization Policy and Role Filter
    Step 4: Restrict Access
  6. Outbound Connections
    Step 5: A Managed Identity for the Logic App
    Step 6: HTTP Calls in the Logic App
  7. Summary

1. Intro

I’ve been using Logic App more often lately to automate various things and since I’m also doing sensitive things with it, I’ve spent quite a bit of time looking at how I can secure the Logic Apps.

In my opinion, one weak point is that the HTTP trigger is only secured with a shared access signature by default. This means that anyone with knowledge of the URL of the trigger can call the Logic app and try to abuse it for their own purposes. There are good ways to secure this trigger better and I will show in this blog what is possible here in cooperation with Azure Active Directory.

Logic Apps have a good integration with Azure AD and I like to make use of it. Here’s a quick overview of useful use cases:

  • When a logic app works against MS Graph, it is best practice to use a managed identity. This can be a system-assigned or one or more user-assigned identities.
  • To secure the trigger of a Logic App with OAuth by an Authorization Policy, an App Registration for the Logic App is required. This app can be used for filtering on the authorization policy and for assigning permissions.
  • So that the trigger can then be called by another service, a service principal, i.e. an app registration, is also required for the client.

2. Architecture overview

The system that calls the Logic App (in my demo this is a PowerShell session or Postman) logs on to Azure AD as the TriggerDemoClient. This login is done via a previously created client secret.

As a trigger client app, a token for the app TriggerDemoClient is requested from Azure AD. Since TriggerDemoClient is entitled to a (predefined) App Role of TriggerDemoServer, the token contains the corresponding role as a claim.

With this token as Authorization Header the trigger of the Logic App is called. The Authorization Policy defined there checks at the token

  • the issuer → i.e. whether the token was issued by the own tenant.
  • the audience → i.e. whether the token was issued for TriggerDemoServer.

The token is then available to the workflow in the Logic App and can be evaluated there with some effort. First, the trigger checks whether a bearer token is present and then passes it on to the further logic or rejects the connection. The content of the role claim in the token is of particular interest here, but depending on the logic, other claims can of course also be interesting.

The Logic App itself runs under a managed identity with which it can then access the Azure Active Directory via REST using the MS Graph, for example.

3. Basic structure

Step 1: Creation of the Logic App

Creating a Logic App is straightforward and can be done via the Azure Portal, for example. The only important thing is that everything described here only works with the Consumption Based version, since the “standard” version does not support login via OAuth.

For the first tests, I create a new empty workflow that contains only an HTTP trigger and a response action.

Test trigger with PowerShell

The first test calls the trigger with the shared access signature and passes a JSON as the body.

Test trigger with Postman

In Postman it makes sense to first create an environment in which all URIs and IDs are stored:

Then, as per PowerShell, the Logic App trigger is called and a JSON is passed.

Step 2: Preparation of the app registrations

For our scenario, two app registrations are needed in AAD:

  • An app that represents the Logic App that is logged in to and to which the permissions are assigned. -> TriggerDemoServer
  • An app for the client component that is used to log in and to which the permissions are assigned -> TriggerDemoClient

The creation of app registrations can be done well and easily in the portal but for traceability and reproducibility I will configure it where appropriate in this blog via PowerShell. Per Powershell the apps can be created easily with this script (mostly stolen here) which does the following:

  • Creation of the App Registration
  • Creation of the Service Principal
  • Configuration of the executing user as owner
  • Optional: Create a secret (for TriggerDemoClient)
  • Optional: Set the AppID URI (for TriggerDemoServer)
  • I create the app TriggerDemoServer with an AppID URI in the format api://AppID. I discuss what that is and why we need it below.
  • I create the app TriggerDemoClient with a secret for now. Whether you want to keep this or switch to a certificate later depends on your calling app and will be discussed below.

The result can then be viewed in the App Registrations and Enterprise Applications.

The next step is to define a role on the TriggerServerDemo app and assign it to the TriggerClientDemo. This is relatively simple in the portal but a bit more complex via Powershell. Fortunately I am not the first one who wants to automate something like this and I can rely on good scripts. 😁

There is a very good script by Philippe Signoret for defining roles on an app. Fortunately, my script from above directly outputs the object of the app registration which I pass to this script via a variable.

For assigning the role I just created, I found a script on this great Blog by Toon Vanhoutte. Since I wanted to use the script “as-is”, I prefer to script around it a bit and create the needed JSON for the definition temporarily.

Test login with PowerShell and examination of the token

To check if the creation of the apps, the roles and the permissions worked, it is useful to log in as a client app and request a token for the server app. This script does that and copies the token to the clipboard (for the next step).

Before you start to build filters for the tokens in the next step it is very useful to deal a little bit with the token and to know in case of doubt (for example if something is not running) where you can look at the current token.

For token investigation, there are several web-based tools that promise not to remember the tokens. My favorite is: https://jwt.ms/

At this example token, the Audience (1), the Issuer (2) and the Role Claim (3) can be examined very well by this tool.

4. Some learnings on App Registrations

While dealing with the question of how to configure the necessary app registrations, I learned a few things that I would like to share here. First of all, however, I would like to point out what I consider to be a particularly useful feature: the Integration Assistant directly at the App Registration. After selecting the Application Type, various recommendations are displayed:

App Roles vs. API Permissions:

For me, assigning rights to apps to apps was quite confusing at first.

The definition of rights to an app takes place in different places depending on the type of permission:

  • If it is a delegated permission — meaning a user is involved — the menu item Expose to API is used.
  • If it is an App Permission — meaning the call by another service principal — the menu item App Roles is used.

If an app should get permissions to another app — for example MS Graph — this is configured in the menu item API permissions regardless of whether it is delegated or app permissions.

App ID URI and Redirect URI

There are two different attributes for URIs on an app registration. Depending on the use case, these should be configured or not.

Redirect URIs are used to restrict which (URIs of) applications can use this application registration for login. Redirect URIs is an important security feature for web apps that prevents an attacker from stealing authentication tokens (see here). However, since the scenario I described uses a so-called client credential flow (without the authorization part), no redirect URI is needed. There are no redirects or callbacks in the scenario.
For all web applications, for example with browser access, it is very important that the redirect URIs are configured correctly.

An Application ID URI is a property of the Application Registration and is used to request tokens for this app — similar to an SPN in AD. An App ID URI can be used in various formats (see here) and must be globally unique for multi-tenant apps. Although you can also simply use the App ID to request tokens, it is best practice to define and use this URI, e.g. to simplify later migrations.

V1 and V2 Endpoints

I decided to use the V2 endpoints for TriggerDemoServer App. A comparison of the two versions can be found here and here.

For this, the accessTokenAcceptedVersion parameter in the App Registration manifest must be changed to 2.

For further handling, the main difference is that some claims in the tokens have slightly different values:

V1: 
Issuer -> https://sts.windows.net/{tenantId}/
Audience -> App ID URI
V2:
Issuer -> https://login.microsoftonline.com/{tenantId}/v2.0
Audience -> AppId of TriggerServerDemo

Certificate-based authentication

If the client app supports it, it makes a lot of sense to use a certificate to log in to the client app instead of the app secret. Although I don’t go into it in this blog, here are the PowerShell sample snippets to log in with a certificate and create one for upload (via GUI).

Login as Service Principal with Powershell and Cert:

Creation and export of a new client cert with Powershell:

5. Access Control

Step 3: Authorization Policy and Role Filter

Azure AD does not issue tokens for an app if the user / service principal does not have an assignment for it. But if we don’t care to check the audience of the token, I can send any Bearer token for another (V2) app to the Logic app and it will accept it.

The Authorization Policy the defines that the value of the claim Issuer has to be https://login.microsoftonline.com/[TenantID]/v2.0 (1) and the value of the claim Audience has to be the AppID (2) of the TriggerDemoServer App.

At the trigger of the app it is enabled that the Authorization Header is passed to the Logic.

A role is defined in the Logic App (1) which is after the extraction (2) compared with Role Claim in the token (3). Depending on the presence of the role, either the request body (4) or a permission denied (5) is returned.

I took the process for unpacking the token (2) from a sensational blog by Jan Vidar Elven. Especially the part with the modulo function is very important (and cost me a lot of time because I first had another solution that didn’t always work…).

Alternatively, you can copy the basic workflow logic here.

Test trigger with PowerShell and OAuth Authentication

Now that all the components are in place, we’ll test them before moving on:

  • Login as Client App with Connect-AzAccount
  • Get a Bearer Token for the Server App with Get-AzAccessToken
  • Trigger the Logic App with Invoke-RestMethod

Test trigger with Postman and OAuth Authentication

With Postman we need two action. But before we start it is a good idea to create an environment to store all the IDs, secrets and URLs:

The first step is getting a Bearer Token for the Server App:

The second step is the Trigger of the Logic App

  • Send POST to the URL of your HTTP (without the SAS)
  • Set Authentication to Bearer Token and insert the token from step 1
  • Set Header Content-Type: application/json
  • Set Body to Raw:JSON and insert your JSON

The procedure for filing with Postman is also described in the MS docs.

Step 4: Restrict Access

Beyond enabling OAuth authentication, it is possible and reasonable to take further measures to secure the trigger.

Conditional Access for Workloads

A new conditional access feature for regulating workload identities has recently been added. Here we do not have all the options we know from Conditional Access, but it is possible to restrict from which IP ranges access can take place. In the sign-in logs of the AAD we also see the (blocked) retrieval (1) of an access token from the client application (2) to the server application (3).

Access control configuration

In addition, the Logic App can be configured from which IP addresses (1) the trigger can be called.

This effectively prevents logging in with an exported (stolen) token:

Trigger condition

Now that we have enabled OAuth authentication and secured all entities involved, it remains to ensure that logging in via the shared access signature is no longer possible.

In my example workflow, checking for the role in a SAS already results in an error, but it makes sense to prevent this directly at the trigger.

The whole topic is super explained in this blog by Joosua Santasolo. As described by Joosua, this trigger condition ensures that a login is only possible with OAuth:

@startsWith(triggerOutputs()?['headers']?['Authorization'],'Bearer')

6. Outbound Connections

To complete our picture from above, we still need access from the workflow to other apps. For me this is often MS Graph, but of course you can also trigger other Logic apps with it.

Step 5: A Managed Identity for the Logic App

Logic Apps can use managed identities and you should always use them if possible. Managed eliminates credential management and all the risks associated with it.

Due to the unfortunately quite high delay in granting permissions, I prefer to work with User Assigned Permissions rather than System Assigned Permissions. Additionally, the same Managed Identity can be used in multiple Logic Apps (useful for troubleshooting) and I can more easily implement a naming convention. MS has a good overview on the topic here.

Since the management of the (User Assigned) Managed Identities via GUI is only partially possible and quite confusing I think it is the best option to always do all steps via PowerShell (at least until that has gotten better).

The permissions (often on the graph) of the Logic App sum up by the called functions. That means it makes sense to create a spreadsheet containing the used calls (look for POST, PUT, PATCH) and the necessary possible permissions.

This allows on the one hand a good selection and on the other hand it gives the reasoning for the necessary permissions. For example, I always ask for the exact function that needs the requested permissions when I am asked to release them in the AAD. I took the main part of the script from here.

Step 6: HTTP Calls in Logic Apps

Although there are many pre-built connectors I mainly use the HTTP action to address other APIs, because the connectors often (for example the AAD connector) do not use the managed identity but a function user (stored as API Connection) and also often work with too high permissions (to be generically usable).

To use the managed identity in the HTTP action, you need to connect the account to your Logic App and select the managed identity after selecting the authentication type and specify the resource to log in to (the App ID URI).

In my workflows, an HTTP action is almost always followed by a Parse JSON action to process the output. The schema can be easily created by using the output of the Graph Explorer as a sample payload.

I also got into the habit of writing the required values from the outputs into variables. On the one hand this increases the clarity and on the other hand it reduces the complexity a bit, because many values are unexpectedly an array and have to run through a loop before they can be used. The graphical designer also inserts this loop automatically. Here is an example for querying the tenant ID:

7. Summary (Logic App + Azure AD = 💘)

There are great ways to increase the security level of the trigger. The implementation is also done quickly if you understand how it works — I hope this blog will help with that.

Finally, here is a small visualization of the effectiveness of the different options to use the trigger 😉:

Acknowledgements:

I don’t think anything discussed in this blog has not already been described by someone else (probably better). I have only collected the things and tried to bring my perspective on it.

I have tried to link all sources directly in the text, but at this point I would like to highlight people I have learned from:

  • Thomas Naunheim was my sparring partner for many of the topics covered here. We discussed many details and I learned a lot from and with him.
  • This blog series (1, 2 and 3) by Jan Vidar Elven really opened my eyes and got me started.

--

--