Secure the receiving Flow for Actionable Messages requests

Posted by

UPDATED (2019-11-29)
I’ve updated the expressions formula to get the keys from the bearer token. Since that string is variable of length I had to make the formula “dynamic” as well.

The HTTP POST URL of the ‘When an HTTP request is received’ trigger in Power Automate Flows is a public endpoint, so you need to secure it.
There are several options to secure the receiving Flow for Actionable Messages requests:

  1. Enable Schema validation in the settings of the trigger
  2. Add Trigger conditions based on Headers keys
  3. Compare correlationId from Headers with the original one

The first option is easy to set and will validate the request body against the schema provided. In case there is a mismatch, HTTP 400 will be returned. But this will just check the JSON payload of the body and not the caller.

The second option is to add trigger conditions to check the Headers keys value.
You could check the host key for instance:

@equals(triggerOutputs()['headers']?['host'],'prod-14.westus.logic.azure.com')

More challenging are the keys from the bearer token, for example the ‘appid’:
The ID of the application which issues the token. The value should always be 48af08dc-f6d2-435f-b2a7-069abd99c086. The Web service should reject the token and the request if the value doesn’t match.
So I added a trigger condition to do this check:

@equals(json(decodeBase64(substring(concat(split(triggerOutputs()['headers']?['Action-Authorization'],'.')?[1],'==='),0,sub(length(concat(split(triggerOutputs()['headers']?['Action-Authorization'],'.')?[1],'===')),mod(length(concat(split(triggerOutputs()['headers']?['Action-Authorization'],'.')?[1],'===')),4)))))?['appid'],'48af08dc-f6d2-435f-b2a7-069abd99c086')

You could add a check for the ‘iss’ and ‘aud’ key as well.

@equals(json(decodeBase64(substring(concat(split(triggerOutputs()['headers']?['Action-Authorization'],'.')?[1],'==='),0,sub(length(concat(split(triggerOutputs()['headers']?['Action-Authorization'],'.')?[1],'===')),mod(length(concat(split(triggerOutputs()['headers']?['Action-Authorization'],'.')?[1],'===')),4)))))?['iss'],'https://substrate.office.com/sts/')
@equals(json(decodeBase64(substring(concat(split(triggerOutputs()['headers']?['Action-Authorization'],'.')?[1],'==='),0,sub(length(concat(split(triggerOutputs()['headers']?['Action-Authorization'],'.')?[1],'===')),mod(length(concat(split(triggerOutputs()['headers']?['Action-Authorization'],'.')?[1],'===')),4)))))?['aud'],'{host url}')


The third option is to create a guid in your Flow by using the expression guid() and save it to CDS, and use it to set the value for the property ‘correlationId’ in the JSON of the Actionable Message.

We recommend that when sending an actionable card, your service should set and log a unique UUID in this property. When the user invokes an Action.Http action on the card, Office 365 sends the Card-Correlation-Id and Action-Request-Id headers in the POST request to your service. Card-Correlation-Id contains the same value as the correlationId property in the card.

Additional properties on the AdaptiveCard type

In this way you can get the correlationId from the Headers key and compare it with the saved value:

@equals(triggerOutputs()['headers']?['Card-Correlation-Id'],'{Guid}')

References:
Security requirements for actionable messages in Office 365
Bearer Token
Outlook-specific Adaptive Card properties and features

Thank you John Liu for helping me finding the path to get the decoding right.

One comment

Leave a Reply

Your email address will not be published. Required fields are marked *