Top
Pawel Czerwinski Unsplash

Securing responses from Actionable Messages


In my previous post I guided you through a list of steps required to build, send and handle response from Adaptive Cards as Actionable Messages in Outlook. Let me now tell you, how to secure the response.

Basically, the response from Adaptive Card sent as Actionable Message can be easily “hacked”. In its definition we define the “Url” attribute that tells the form where to send data. If a message is forwarded to another person in tenant or if someone builds a form that submits data to the same endpoint as our message, it will accept anything when not having any kind of verification mechanism. Therefore our solution should be prepared to identify source and terminate further execution in case of attempted fraud.

If the card is used for real enterprise purposes, than you could use mechanism called “signing cards”, so that using your private key you need to sign payload and then using public verify the response. This uses DKIM and SPF standards: https://docs.microsoft.com/en-us/outlook/actionable-messages/security-requirements#action-authorization-header. What if we need something simpler?

See it in action!

Action-Authorization header to the rescue

Each response from Actionable Message contains a header key Action-Authorization. It looks like a regular Bearer token, but it’s not. This is JSON Web Signature (JWS).

Important! Action-Authorization Bearer token cannot be used to call eg. GraphAPI to get user’s profile. It will always return 401, because this is not a valid AAD Bearer token.

Contents of the key are base64 encoded JSON objects. Once you split it using “.”, there are three parts (according to RFC7515 standard):

  1. JWS header
  2. JWS payload
  3. JWS signature

JWS header

Decode it using the following expression: decodeBase64(split(replace(triggerOutputs()['headers']?['Action-Authorization'], 'Bearer ', ''), '.')?[0])

{"typ":"JWT","alg":"RS256","kid":"RfKI6h2uhxp6rHwux79UcFh","x5t":"RfKI6h2uhxp6rHwux79UcFh","issloc":"AM0PR10MB","sqid":63734169909382}

Defines the JWT (JSON Web Token) type used. This one is not very much useful for validation purposes.

JWS payload

Decode it using the following expression: decodeBase64(concat(split(replace(triggerOutputs()['headers']?['Action-Authorization'], 'Bearer ', ''), '.')?[1], '=='))

{
  "iat":1598827285,
  "ver":"STI.ExternalAccessToken.V1",
  "appid":"48af08dc-f6d2-435f-b2a7-069abd99c086",
  "sub":"WHO SUBMITTED CARD",
  "appidacr":"2",
  "acr":"0",
  "tid":"32443e19-8cfe-4e6b-a3fe-e75c0e09c7a8",
  "sender":"WHO SENT CARD",
  "oid":"e4e6b40f-f698-403e-bc8e-252a2d52f22c",
  "iss":"https://substrate.office.com/sts/",
  "aud":"https://prod-135.westeurope.logic.azure.com",
  "exp":1598828185,
  "nbf":1598827285
}

Basically when sending an Actionable Message to a user and waiting for their response you should store basic information somewhere to compare it once response is received, for example: CDS or SharePoint. Then you could compare:

  • appid – this is the ID of the app that issued token, in case of Outlook it should always be 48af08dc-f6d2-435f-b2a7-069abd99c086.
  • sub – this is going to be an e-mail of user who clicked to submit the response.
  • sender – this is going to be an e-mail of the account used to send Actionable Message.
  • aud – url of the Logic Apps instance used to execute action.
  • iat – when payload was signed (UNIX timestamp)
  • exp – when signature expires (UNIX timestamp)

So in this case you can compare if appid and sub values are as expected. More: https://docs.microsoft.com/en-us/outlook/actionable-messages/security-requirements#verifying-that-requests-come-from-microsoft.

CorrelationId and originator in Adaptive Card

What can also be used, even harder to hack, is to use CorrelationId + originator attributes in Adaptive Cards. You can generate your custom GUID (eg. using guid() expression in Power Automate), save it in CDS or SharePoint, then set it as a value of the CorrelationId attribute:

{
  "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
  "type": "AdaptiveCard",
  "version": "1.0",
  "hideOriginalBody": true,
  "originator": "f6f1d226-d6bb-409e-853e-0747fd61234",
  "correlationId": "@{guid()}",

Once the response is received, simply compare values of the originator property with the registered provider guid and saved CorrelationId with Card-Correlation-Id value from header: triggerOutputs()['headers']?['Card-Correlation-Id'] (source: https://docs.microsoft.com/en-us/outlook/actionable-messages/adaptive-card#additional-properties-on-the-adaptivecard-type).

Service-specific tokens

What Microsoft is also recommending is to add custom generated tokens added to the target URL defined in Adaptive Card’s actions, eg. https://contoso.com/approve?requestId=abc&serviceToken=a1b2c3d4e5

More on this: https://docs.microsoft.com/en-us/outlook/actionable-messages/security-requirements#verifying-the-identity-of-the-user.

I hope that the above steps will help you build your Actionable Messages and Adaptive Cards based solution and secure them properly. If you have questions, feel free to ask them in the comments below.


Tomasz Poszytek

Hi, I am Tomasz. I am expert in the field of process automation and business solutions' building using Power Platform. I am Microsoft MVP and Nintex vTE.

17 Comments
  • farissi jaafar

    Hi Tomas

    i receive this message
    The remote endpoint returned an error (HTTP 400). Please try again later.

    i’m working with this tuto
    https://www.bythedevs.com/post/multi-line-approvals-with-adaptive-cards-outlook-and-power-automate

    thanks for your help

    November 3, 2020 at 2:16 pm Reply
  • oliver

    hi Tomasz, I am getting HTTP 400 error, the address is working when I tried it in Postman, so is there other reason that might be causing it?
    please advise, thank you
    {
    “type”: “Action.Http”,
    “title”: “Approve”,
    “method”: “POST”,
    “style”: “positive”,
    “id”: “RequestApprove”,
    “body”: “{\n\”response\”:\”{{ApproveValue.value}}\”,\n\”requestid\”:\”{{@{triggerOutputs()?[‘body/ID’]}}}\”\n}”,
    “url”: “https://prod-166.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxQXUz44Jo87CEiyevvW5QFs4FDh60dd0fluNo”,
    “headers”: [
    {
    “name”: “Authorization”,
    “value”: “”
    }
    ]
    }

    January 15, 2021 at 9:00 am Reply
    • oliver

      I solved this! 🙂
      for HTTP 400 error, it is most likely an issue with the JSON payload, I had a type: “integer”, after changing it to “string”, response was successful. 😉

      January 22, 2021 at 2:56 am Reply
  • Priya

    Hi Tomaz, Great Post. Can you please tell me how to retrieve values from JWS payload to compare data. The decodeBase64() returns following format.

    “{\”iat\”:xxx,\”ver\”:\”xxx\”,\”appid\”:\”xxx\”,\”sub\”:\”xxx\”,\”appidacr\”:\”x\”,\”acr\”:\”x\”,\”tid\”:\”xxx\”,\”sender\”:\”xxx\”,\”oid\”:\”xxx\”,\”iss\”:\”xxx\”,\”aud\”:\”xxx\”,\”exp\”:xxx,\”nbf\”:xxx}”

    Thanks

    February 24, 2021 at 11:49 am Reply
    • Priya

      Solved. The parse Json schema which I had typed was wrong. Works fine now. All thanks to your blog.

      February 24, 2021 at 12:02 pm Reply
  • Anna

    Hi Tomasz,

    I get the following error –
    ‘The template language function ‘decodeBase64’ was invoked with a parameter that is not valid. The value cannot be decoded from base64 representation.
    Please suggest.

    Regards

    February 25, 2021 at 7:11 am Reply
    • Anna

      Hi, in my case sometimes
      decodeBase64(concat(split(replace(triggerOutputs()[‘headers’]?[‘Action-Authorization’], ‘Bearer ‘, ”), ‘.’)?[1], ‘==’)) – this works and gives error for below expression
      decodeBase64(split(replace(triggerOutputs()[‘headers’]?[‘Action-Authorization’], ‘Bearer ‘, ”), ‘.’)?[1]) – this work for some and gives error for above expression
      Please suggest.
      Regards,

      February 25, 2021 at 7:57 am Reply
      • Tomasz Poszytek

        Basically you can just try to first use Compose action and put there split(replace(triggerOutputs()[‘headers’]?[‘Action-Authorization’], ‘Bearer ‘, ”), ‘.’) expression, so you will see what tokens split will return. Then investigate the second one. It should be almost a valid base64 string, just missing the ‘==’ characters at the end.

        February 27, 2021 at 10:58 am Reply
      • Bernardo

        I am having the exact same issue as Anna.
        It used to work fine.

        April 9, 2021 at 11:56 am Reply
        • Dinos

          I face the same issue. The string generated from this
          split(replace(triggerOutputs()[‘headers’]?[‘Action-Authorization’], ‘Bearer ‘, ”), ‘.’)?[1]
          can be decoded using https://www.base64decode.org/ but the power automate flow returns:
          ‘The template language function ‘decodeBase64′ was invoked with a parameter that is not valid. The value cannot be decoded from base64 representation.’.

          June 16, 2021 at 11:14 am Reply
          • Tomasz Poszytek

            Unfortunately I haven’t found a solution to this yet 🙁

            June 30, 2021 at 3:19 pm
  • mohammad

    when Decoding the JWS with the function you added there the result contain always a symbol in the end of the “nbf” part and its missing the closure “}” it looks like this “nbf”:161467939�

    March 2, 2021 at 11:15 am Reply
    • Tomasz Poszytek

      This technology is being updated and changed recently. That character looks like if base64 decode was not really successful.

      March 2, 2021 at 1:00 pm Reply
  • Syed

    Hi,
    Do you have any sample to submit star rating response with comments box.

    May 23, 2021 at 12:55 pm Reply

Post a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.