Share via



August 2017

Volume 32 Number 8

[Microsoft Office]

Actionable Messages for Outlook

By Woon Kiat | August 2017

I love e-mail. At work, it's where I go to stay on top of what's going on and what I need to do. It's where I receive noti-fications of new expense reports submitted by my team, new replies to my tweets, new comments to my pull requests and so on. But e-mail could be so much better. Why do I need to click a link in e-mail and wait for the finance system Web site to load in a browser before I can approve an expense report? Why do I have to mentally change my context? I should be able to approve the expense report directly in the context of my e-mail client.

Sound familiar? Outlook is about to make your life much better, save you time and make you more productive.

Introducing Actionable Messages

Actionable Messages let users complete tasks within the e-mail itself. It offers a native experience in both the Outlook desktop client and Outlook Web Access (OWA). In this article, I'll use the word Outlook to mean either Outlook desktop client or OWA.

In the example I'll be using, the fictional company Contoso has an internal expense approval system. Every time an employee submits an expense report, an e-mail message is sent to the manager for approval. I'll walk through the steps on how to use Actionable Messages in Outlook that lets the manager approve the request within the e-mail message it-self.

My First Actionable Message

In Figure 1, you see the HTML of an Actionable Message. It might look complicated, but believe me, it's not. I'll explain the markup in detail in the following sections. The first step is to send an e-mail with the markup from Figure 1 to your Office 365 e-mail account.

Figure 1 HTML of an Outlook Actionable Message

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf8">
    <script type="application/ld+json">{
      "@context": "https://schema.org/extensions",
      "@type": "MessageCard",
      "hideOriginalBody": "true",
      "title": "Expense report is pending your approval",
      "sections": [{
        "text": "Please review the expense report below.",
        "facts": [{
          "name": "ID",
          "value": "98432019"
        }, {
          "name": "Amount",
          "value": "83.27 USD"
        }, {
          "name": "Submitter",
          "value": "Kathrine Joseph"
        }, {
          "name": "Description",
          "value": "Dinner with client"
        }]
      }],
      "potentialAction": [{
        "@type": "HttpPost",
        "name": "Approve",
        "target": ""
      }, {
        "@type": "OpenUri",
        "name": "View Expense",
        "targets": [ { "os": "default", 
        "uri": "https://expense.contoso.com/view?id=98432019"} ]
      }]
    }
    </script>
  </head>
  <body>
    <p>Please <a href="https://expense.contoso.com/view?id=98432019">approve</a> 
      expense report #98432019 for $83.27.</p>
  </body>
</html>

As shown in Figure 2, in the message itself, there's a message card with two buttons with which you can interact. If you click on the Approve button, it'll result in an error for now because you haven't yet specified the URL for the action. You'll add the URL later. If you click on the View Expense button, a browser will open and navigate to the Expense Approval Web site.

Actionable Message in Outlook Web Access
Figure 2 Actionable Message in Outlook Web Access

MessageCard Markup

The e-mail message itself is typical HTML markup. To make it an Actionable Message in Outlook, you insert Message-Card markup in the <script> element. One main advantage of this approach is that e-mail messages will continue to render as usual on clients that don’t recognize the MessageCard markup. The format of this markup is called JSON-LD, which is a standard format to create machine-readable data across the Internet. Now, let’s go through the markup in detail. These two lines of code are mandatory in every markup:

"@context": "https://schema.org/extensions",
"@type": "MessageCard",

You set the context to https://schema.org/extensions and the type to "MessageCard." The MessageCard type indicates that this e-mail is an Actionable Message.

Next is the property "hideOriginalBody." When the value is set to true, the e-mail body is hidden and only the card is displayed, as shown in Figure 2. This is useful when the card itself contains all the information a user would need or the content of the card is redundant with the content of the e-mail body. In case the message is viewed in an e-mail client that doesn't understand message cards, then the original body will be shown and the message card will not, regardless of the value of "hideOriginalBody." The value of the property "title" is the title of the MessageCard:

"hideOriginalBody": "true",
"title": "Expense report is pending your approval",

Next is "sections." You can think of a section as representing an "activity." If your card has multiple activities you should definitely use multiple sections, one per activity. Figure 3 shows markup with one section. You use the facts property of a section, which is an array of name-value pairs, to display the details of an expense report.

Figure 3 Card with One Section

"sections": [{
  "text": "Please review the expense report below.",
  "facts": [{
    "name": "ID",
    "value": "98432019"
  }, {
    "name": "Amount",
    "value": "83.27 USD"
  }, {
    "name": "Submitter",
    "value": "Jonathan Kiev"
  }, {
    "name": "Description",
    "value": "Dinner with client"
  }]
}],

Next is "potentialAction." This is an array of actions that can be invoked on this card. Currently the supported actions are OpenUri and HttpPOST:

"potentialAction": [{
  "@type": "HttpPost",
  "name": "Approve",
  "target": ""
}, {
  "@type": "OpenUri",
  "name": "View Expense",
  "targets": [ { "os": "default",
  "uri": "https://expense.contoso.com/view?id=98432019"} ]
}]

The OpenUri action will open a browser and navigate to the URL specified in the targets property. The targets property is an array that lets you specify platform-specific URLs. For example, you might want users on iOS and Android to navi-gate to different URLs. In this example, you set the OS to default, which means the URL is the same for all platforms.

The HttpPOST action will make an HTTP POST request to an external Web service specified in the target property. Cur-rently the value is empty. That's why you see an error when you click on the Approve button.

MessageCard Playground App

It would be great if you could visualize how the card looks when you're authoring the markup. Microsoft has a Web app that lets you do just that. It's called the MessageCard Playground App (bit.ly/2s274S9).

You should always design your card in the app first. Once you're happy with the card layout, you can then use the markup in your e-mail messages.

Calling an External Web Service with HttpPOST Action

Now you have a message card with two actions. The OpenUri will open a browser and navigate to the URL specified in the action. For the HttpPOST action, you'd like it to call your REST API that will approve the expense report. You replace the HttpPOST action with the following:

{
  "@type": "HttpPost",
  "name": "Approve",
  "target": "https://api.contoso.com/expense/approve",
  "body": "{ \"id\": \"98432019\" }"
}

When a user clicks on the Approve button, a Microsoft server will make an HTTP POST request that's similar to the fol-lowing:

POST api.contoso.com/expense/approve
Content-Type: application/json

{ "id": "98432019" }

The target is the URL, which the Microsoft server is going to make a POST request to, and the body is the content of the request. The body content is always assumed to be JSON.

Now you'll send yourself an e-mail with the new markup. When you click on the Approve button, the action is complet-ed successfully.

ActionCard Action

Now let's add a Reject button so users can reject an expense report. For reject, you need additional input from users to explain why the expense report is rejected.

The ActionCard action is designed for such scenarios. It contains one or more inputs and associated actions that can be either OpenUri or HttpPost. You insert an ActionCard action in between HttpPOST and OpenUri, as shown in Figure 4.

Figure 4 ActionCard Action

"potentialAction": [{
  "@type": "HttpPost",
  ...
}, {
  "@type": "ActionCard",
  "name": "Reject",
  "inputs": [{
    "@type": "TextInput",
    "id": "comment",
    "isMultiline": true,
    "title": "Explain why the expense report is rejected"
  }],
  "actions": [{
    "@type": "HttpPOST",
    "name": "Reject",
    "target": "https://api.contoso.com/expense/reject",
    "body": "{ \"id\": \"98432019\", \"comment\": \"{{rejectComment.value}}\" }"
  }]
},{
  "@type": "OpenUri",
  ...
}]

If you send yourself the updated markup, there are Approve, Reject and View Expense buttons. If you click on the Re-ject button, you can now enter comments before you reject the expense report.

Let's take a look at the ActionCard action markup. Besides the type and name properties, it has an array of inputs and actions. In this example, you have a multiline TextInput that lets users enter text. The other supported inputs are DateInput and Multichoice­Input. For more details, refer to bit.ly/2t3bLJN.

You have an HttpPOST action that will make a call to the external Web service to reject the expense report. This is simi-lar to the HttpPOST action for the approve action. One major difference is that you want to pass the comments entered by users to the Web service call. You can reference to the value of the text input by using {{rejectComment.value}}, where rejectComment is the ID of the text input.

Web Service for Actionable Messages

So far you've seen the markup for Actionable Messages in Outlook and how it works. In the rest of the article, I'll de-scribe how a Web service should handle requests coming from Actionable Messages in Outlook.

Actionable Messages will work with any Web service that can handle HTTP POST requests. In this example, your Web service is an API controller in ASP.NET MVC. Figure 5 shows your API controller.

Figure 5 Expense API Controller

[RoutePrefix("expense")]
public class ExpenseController : ApiController
{
  [HttpPost]
  [Route("approve")]
  public HttpResponseMessage Approve([FromBody]JObject jBody)
  {
    string expenseId = jBody["id"].ToString();

    // Process and approve the expense report.
    HttpResponseMessage response = this.Request.CreateResponse(HttpStatusCode.OK);
    response.Headers.Add("CARD-ACTION-STATUS", "The expense was approved.");

      return response;    
  }

  [HttpPost]
  [Route("reject")]
  public HttpResponseMessage Reject([FromBody]JObject jBody)
  {
    string expenseId = jBody["id"].ToString();
    string comment = jBody["comment"].ToString();

    // Process and reject the expense report.
    HttpResponseMessage response = this.Request.CreateResponse(HttpStatusCode.OK);
    response.Headers.Add("CARD-ACTION-STATUS", "The expense was rejected.");

    return response;    
  }
}

There are two methods in this API controller, one for approval and another for rejection. The Web service must return an HTTP status code of 2xx for the action to be considered successful. The Web service can also include the CARD-ACTION-STATUS header in the response. The value of this header will be displayed to the user in a reserved area of the card. If you deploy the Web service to https://api.contoso.com and you click on the Approve button, you'll get the noti-fication that the operation was completed successfully, as shown in Figure 6.

Expense Report with Successful Approval Notification
Figure 6 Expense Report with Successful Approval Notification

You now have the Actionable Message working end to end. You can send out the Actionable Message and when the user clicks on the Approve button, an HTTP POST request is made to your Web service. Your Web service will process the request and return 200 OK. Outlook will then mark the action as done. Next, I'll look at how you can secure your Web service.

Limited-Purpose Tokens

Because the expense ID usually follows a certain format, there’s a risk that an attacker can perform an attack by posting a lot of requests with different expense IDs. If an attacker successfully guesses an expense ID, the attacker might be able to approve or reject that expense report. Microsoft recommends developers use “limited-purpose tokens” as part of the action target URL or in the body of the request. The limited-­purpose token should be hard for attackers to guess. For example, I use a GUID, a 128-bit number as the limited-purpose token. This token can be used to correlate service URLs with specific requests and users. It can also be used to protect Web services from replay attacks (bit.ly/2sBQmdn). You update the markup to include a GUID in the body:

{
  "@type": "HttpPost",
  "name": "Approve",
  "target": "https://api.contoso.com/expense/approve",
  "body": "{ \"id\": \"98432019\", \"token\": \
  "d8a0bf4f-ae70-4df6-b129-5999b41f4b7f\" }"
}

Bearer Token

While limited-purpose tokens make it harder for attackers to forge a request, they're still not perfect. Ideally, a Web ser-vice should be able tell whether an HTTP POST request is coming from a Microsoft server instead of some unauthorized, potentially malicious server.

Microsoft solves this problem by including a bearer token in every HTTP POST request it sends to Web services. The bearer token is a JSON Web Token (JWT) and it's included in the Authorization header of a request. When a user clicks on the Approve button, the Web service will receive a request that looks like this:

POST https://api.contoso.com/expenses/approve

Content-Type: application/json
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJ­SUzI1NiIsIng1dCI6I­jhx­Z3A4­VER­CbDJINkp5­RkU0WjM0­ZDJoYS1rR­SIsImtpZCI6I­jhxZ3A4V­ERCbDJINkp5RkU0WjM0ZD­JoYS1rRSJ9.eyJpYXQiOjE0ODQwODkyNzksInZlciI6IlNUSS5FeHRlcm­5hbEFjY2Vzc1Rva2­VuLlYxIiwiYXBw aWQiOiI0OGFmMD­hkYy1mN­mQyLTQzNWYtYjJhNy0wN­jlhYmQ5OWMwODYiLCJzd­WIiOiJk­YXZpZEBj­ b250b3NvLmN­vbSIsImFwcGlk­YWNyIjoiMiIsIm­FjciI6IjAi­LCJzZW5kZ­XIiOiJleHB­lbnNlYXBw... (truncated for brevity)

{
  "id": "98432019",
  "token": "d8a0bf4f-ae70-4df6-b129-5999b41f4b7f"
}

What follows "Bearer" in the Authorization header is a long base-64 encoded string that's a JSON Web Token (JWT). You can decode the JWT at jwt.calebb.net. Figure 7 shows a sample token after it's decoded.

Figure 7 A Sample Bearer JSON Web Token

{
  typ: "JWT",
  alg: "RS256",
  x5t: "8qgp8TDBl2H6JyFE4Z34d2ha-kE",
  kid: "8qgp8TDBl2H6JyFE4Z34d2ha-kE"
}.
{
  iat: 1484089279,
  ver: "STI.ExternalAccessToken.V1",
  appid: "48af08dc-f6d2-435f-b2a7-069abd99c086",
  sub: "david@contoso.com",
  appidacr: "2",
  acr: "0",
  sender: "expenseapproval@contoso.com",
  iss: "https://substrate.office.com/sts/",
  aud: "https://api.contoso.com",
  exp: 1484090179,
  nbf: 1484089279
}.
[signature]

Every JWT has three segments, separated by a dot (.). The first segment is the header, which describes the crypto-graphic operations applied to the JWT. In this case, the algorithm (alg) used to sign the token is RS256, which means RSA using the SHA-256 hash algorithm. The x5t value specifies the thumbprint of the key used to sign the token.

The second segment is the payload itself. It has a list of claims that the token is asserting. Web services should use these claims to verify a request. The table in Figure 8 describes these claims.

Figure 8 Description of Claims in Payload

Claims Description
iss The token issuer. The value should always be https://substrate.office.om/sts/. The Web service should reject the token and the request if the value does not match.
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.
aud The audience of the token. It should match the hostname of the Web service URL. The Web service should reject the token and the request if the value doesn't match.
sub The subject who performed the action. The value will be the e-mail address of the person who performed the action, if the e-mail address or any of the proxy e-mail addresses is in the To: line. If none of the e-mail addresses is matched, this will be the hashed value of the subject's user principal name (UPN). It's guaranteed to be the same hashed value for the same UPN.
sender The e-mail address of the original message sender.
tid The tenant ID of the token issuer.

The third segment is the digital signature of the token. By verifying the signature, Web services can be confident that the token is sent by Microsoft and trust the claims in the token.

Verifying a digital signature is a complex task. Fortunately, there's a library on NuGet that makes the verification task easy. The library is available at bit.ly/2stq90c and it's authored by Microsoft. Microsoft also published code samples for other languages on how to verify the token. Links for these code samples are available at the end of this article.

After you include the NuGet package in the Web service project, you can use the VerifyBearerToken method, as shown in Figure 9, to verify the bearer token in a request.

Figure 9 The VerifyBearerToken Method

private async Task<HttpStatusCode> VerifyBearerToken(
  HttpRequestMessage request, string serviceBaseUrl, string expectedSender)
{
  if (request.Headers.Authorization == null ||
    !string.Equals(request.Headers.Authorization.Scheme, "bearer", 
      StringComparison.OrdinalIgnoreCase) ||
      string.IsNullOrEmpty(request.Headers.Authorization.Parameter))
  {
    return HttpStatusCode.Unauthorized ;
  }

  string bearerToken = request.Headers.Authorization.Parameter;
  ActionableMessageTokenValidator validator = 
    new ActionableMessageTokenValidator();
  ActionableMessageTokenValidationResult result = 
    await validator.ValidateTokenAsync(bearerToken, serviceBaseUrl);

  if (!result.ValidationSucceeded)
  {
    return HttpStatusCode.Unauthorized;
  }

  if (!string.Equals(result.Sender, expectedSender, 
    StringComparison.OrdinalIgnoreCase) ||
      !result.ActionPerformer.EndsWith("@contoso.com", 
        StringComparison.OrdinalIgnoreCase))
  {
    return HttpStatusCode.Forbidden;
  }

  return HttpStatusCode.OK;
}

[HttpPost]
[Route("approve")]
public async Task<HttpResponseMessage> Approve([FromBody]JObject jBody)
{
  HttpRequestMessage request = this.ActionContext.Request;
  HttpStatusCode result = await VerifyBearerToken(
    request, "https://api.contoso.com", 
    "expenseapproval@contoso.com");

  switch (result)
  {
    case HttpStatusCode.Unauthorized:
      return request.CreateErrorResponse(
        HttpStatusCode.Unauthorized, new HttpError());

    case HttpStatusCode.Forbidden:
      HttpResponseMessage errorResponse = 
        this.Request.CreateErrorResponse(HttpStatusCode.Forbidden, new HttpError());
      errorResponse.Headers.Add("CARD-ACTION-STATUS", 
        "Invalid sender or the action performer is not allowed.");
      return errorResponse;

    default:
      break;
  }

  string expenseId = jBody["id"].ToString();

  // Process and approve the expense report.

  HttpResponseMessage response = this.Request.CreateResponse(HttpStatusCode.OK);
  response.Headers.Add("CARD-ACTION-STATUS", "The expense was approved.");

  return response;
}

First, the method verifies there's a bearer token in the Authorization header. Then, it initializes a new instance of Action-able­MessageTokenValidator and calls the ValidateToken­Async method. The method takes two parameters. The first one is the bearer token itself. The second one is the Web service base URL. If you look at the decoded JWT, this is the value of the aud (audience) claim. It basically means the token is issued for the intended audience, which is your Web service but not any other Web service. In this case, the API to be called is https://api.contoso.com/expense/approve. The value in the claim will be the base URL, which is https://api.contoso.com.

The method will return an instance of ActionableMessage­TokenValidationResult. First, you'll check the property ValidationSucceeded. If the validation succeeded, the value will be true; otherwise, it'll be false.

The result also includes two other properties that will be useful to third parties. The first one is Sender. This is the value of the sender claim in the token. This is the e-mail address of the account that sent the actionable message. The second one is the ActionPerformer, which is the value of the sub claim. This is the e-mail address of the person who performed the action. In this example, only those with @contoso.com e-mail addresses can approve or reject an expense report. You can replace the code with a more complicated verification of your own.

Refresh Card

So far the only way to provide feedback to a user is through the CARD-ACTION-STATUS header. The value of the header will be displayed to the user in a reserved area of the card. Another option is to return a refresh card to the user. The idea is to replace the current action card with a different card. There are a few reasons why you want to do that. For example, after an expense report is approved, you don’t want users to be able to approve or reject the expense report again. Instead, you’d like to tell the user that the expense report is already approved. Figure 10 shows the markup that you’ll return.

Figure 10 Markup Returned to Expense Report with Refresh Card

{
  "@context": "https://schema.org/extensions",
  "@type": "MessageCard",
  "hideOriginalBody": "true",
  "title": "Expense report #98432019 was approved",
  "sections": [{
    "facts": [{
      "name": "ID",
      "value": "98432019"
    }, {
      "name": "Amount",
      "value": "83.27 USD"
    }, {
      "name": "Submitter",
      "value": "Kathrine Joseph"
    }, {
      "name": "Description",
      "value": "Dinner with client"
    }]
  }]
}

You need to set the value of the header CARD-UPDATE-IN-BODY to true so Microsoft servers know that the response has a refresh card. Figure 11 shows the Approve method returns a refresh card.

Figure 11 The Approve Method Returns a Refresh Card

private HttpResponseMessage CreateRefreshCard(
  HttpRequestMessage request, string actionStatus, 
  string expenseID, string amount, string submitter, string description)
{
  string refreshCardFormatString = "{\"@context\": \"https://schema.org/extensions\",\"@type\": \"MessageCard\",\"hideOriginalBody\": \"true\",\"title\": \"Expense report #{0} was approved\",\"sections\": [{\"facts\": [{\"name\": \"ID\",\"value\": \"{0}\"},{\"name\": \"Amount\",\"value\": \"{1}\"},{\"name\": \"Submitter\",\"value\": \"{2}\"},{\"name\": \"Description\",\"value\": \"{3}\"}]}]}";
  string refreshCardMarkup = string.Format(
    refreshCardFormatString,
    expenseID,
    amount,
    submitter,
    description);

HttpResponseMessage response = request.CreateResponse(HttpStatusCode.OK);
Response.Headers.Add("CARD-ACTION-STATUS", actionStatus);
  response.Headers.Add("CARD-UPDATE-IN-BODY", "true");
  response.Content = new StringContent(refreshCardMarkup);

  return response;
}

[HttpPost]
[Route("approve")]
public async Task<HttpResponseMessage> Approve([FromBody]JObject jBody)
{
  HttpRequestMessage request = this.ActionContext.Request;
  HttpStatusCode result = await VerifyBearerToken(
    request, "https://api.contoso.com", 
    "expenseapproval@contoso.com");

  switch (result)
  {
    case HttpStatusCode.Unauthorized:
      return request.CreateErrorResponse(
        HttpStatusCode.Unauthorized, new HttpError());

    case HttpStatusCode.Forbidden:
      HttpResponseMessage errorResponse = 
        this.Request.CreateErrorResponse(
          HttpStatusCode.Forbidden, new HttpError());
      errorResponse.Headers.Add("CARD-ACTION-STATUS", 
        "Invalid sender or the action performer is not allowed.");
      return errorResponse;

    default:
      break;
  }

  string expenseId = jBody["id"].ToString();

  // Process and approve the expense report.

  return CreateRefreshCard(
    request,
    "The expense was approved.",
    "98432019",
    "83.27 USD",
    "Jonathan Kiev",
    "Dinner with client");
}

Wrapping Up

Actionable Messages let users complete tasks within Outlook in a secure way. It's available in desktop Outlook and Outlook Web Access today and the feature is coming to Outlook for Mac and Outlook Mobile soon. It's straightforward to implement Actionable Messages. First, you need to add the required markup to the e-mails you're sending out. Second, you need to verify the bearer token sent by Microsoft in your Web service. Actionable Messages will make your users happier and more productive. There is so much more about Actionable Messages than this article can cover. Visit bit.ly/2rAD6AZ for the complete references and links to the code samples.

I'd like to acknowledge Sohail Zafar, Edaena Salinas Jasso, Vasant Kumar Tiwari, Mark Encarnacion and Miaosen Wang, who helped review this article for grammar, spelling, and flow.


Woon Kiat Wong is a software engineer from the Knowledge Technologies Group in Microsoft Research. He works closely with the Outlook team to deliver Actionable Messages. Contact him at wowong@microsoft.com.

Thanks to the following Microsoft technical experts for reviewing this article: Pretish Abraham, David Claux, Mark Encarnacion and Patrick Pantel