Curious case of AWS mapping template built-in variables

Wojciech Matuszewski - Mar 7 '22 - - Dev Community

Amazon API Gateway is, along with AWS Lambda, arguably, the most frequently used service in AWS serverless architectures. The service's feature spectrum, starting from rate-limiting and ending on request/response validation, is vast, making it an ideal choice for many API-driven applications.

Amongst its features, as a "Lambda-less" enthusiast, one is particularly interesting to me – the ability to create mapping templates that transform the payload or the response without the need for AWS Lambda.

This article will touch on something I've spent many hours debugging – the behavior of the built-in mapping template variables concerning Amazon REST API Gateway VTL and talk about my assumptions that turned out to be wrong.

The code in this article is sourced from this GitHub repository.

The architecture

I often find that often the understanding lies in the concrete. This blog post is no exception. The following architecture will serve as a reference in our discussions.

The architecture

The user makes a request to an API frontend with Amazon API Gateway backed by a mock integration. The AWS Lambda authorizer handles authorization. The role of the mapping template is to respond with some of the Amazon API Gateway mapping template variables, mainly the ones related to authorization.

The mapping template is crucial since its structure dictates the response of the API. Let us look at it next.

The code

Luckily, to showcase the behavior I'm teasing you about, we do not need to write a lot of code. A two-liner of a mapping template and a very bare-bones AWS Lambda authorizer will do.

The following is the mapping template code.

{
    "authorizerContextUsernameProperty": "$context.authorizer.username",
    "wholeAuthorizerVariable": "$util.escapeJavaScript($context.authorizer)",
}
Enter fullscreen mode Exit fullscreen mode

The mapping template declares two keys, the authorizerContextUsernameProperty which is read directly from the $context.authorizer.username and the wholeAuthorizerVariable. I'm escaping the value of $context.authorizer to avoid parsing issues.

And here is the AWS Lambda authorizer code.

export const handler: APIGatewayTokenAuthorizerHandler = async event => {
  // Assuming the user is authenticated and the token is valid

  return {
    principalId: "1",
    context: {
      username: "test-username"
    },
    policyDocument: {
      Statement: [
        {
          Action: "execute-api:Invoke",
          Effect: "Allow",
          Resource: event.methodArn
        }
      ],
      Version: "2012-10-17"
    }
  };
};
Enter fullscreen mode Exit fullscreen mode

The authorizer is not doing much. It is only there to populate the context property so that we have something to work with within the mapping template.

The surprising behavior

If I were to invoke the API, what would be the output? As a reminder, the following is our response mapping template.

{
    "authorizerContextUsernameProperty": "$context.authorizer.username",
    "wholeAuthorizerVariable": "$util.escapeJavaScript($context.authorizer)",
}
Enter fullscreen mode Exit fullscreen mode

When I first started looking into API Gateway mapping templates I thought that the result would look similar to the following object.

{
  "authorizerContextUsernameProperty": "test-username",
  "wholeAuthorizerVariable": "{\"username\": \"test-username\", ... OTHER_PROPERTIES }"
}
Enter fullscreen mode Exit fullscreen mode

To my surprise, that was not the case. Here is the response I have received.

{
  "authorizerContextUsernameProperty": "test-username",
  "wholeAuthorizerVariable": ""
}
Enter fullscreen mode Exit fullscreen mode

Even though we clearly see that $context.authorizer.username is defined, the $context.authorizer somehow is "empty". After staring at the screen trying to come up with an answer, I've decided to check the whole $context object.

{
    "authorizerContextUsernameProperty": "$context.authorizer.username",
-   "wholeAuthorizerVariable": "$util.escapeJavaScript($context.authorizer)",
+   "wholeAuthorizerVariable": "$util.escapeJavaScript($context)",
}
Enter fullscreen mode Exit fullscreen mode

And the result was surprising to me as well! (I redacted some of the values).

{
  "authorizerContextUsernameProperty": "test-username",
  "wholeAuthorizerVariable": "{resourceId=w7badv0rv4, authorizer=, resourcePath=/, httpMethod=GET, extendedRequestId=OY6_EHr1DoEFSzg=, requestTime=03/Mar/2022:04:06:30 +0000, path=/prod/, accountId=XXX, protocol=HTTP/1.1, requestOverride=, stage=prod, domainPrefix=9sd680n5z5, requestTimeEpoch=1646280390676, requestId=f9393904-3014-43f6-8e24-f594db2d7604, identity=, domainName=9sd680n5z5.execute-api.eu-west-1.amazonaws.com, responseOverride=, apiId=9sd680n5z5}"
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the authorizer, identity, responseOverride properties are empty. Since I did not override any request and response parameters, the emptiness of responseOverride property is understandable. But what about the authorizer and identity? I can see that $context.authorizer.username contains a value, otherwise the authorizerContextUsernameProperty property would be empty.

What about AWS AppSync?

Like Amazon API Gateway, the AWS AppSync service enables developers to write VTL mapping templates to transform data. Will AWS AppSync VTL parsing logic behave differently from the one we observed in Amazon API Gateway? Let us find out.

AWS AppSync setup

The architecture is very similar to the one we have looked at previously. The main difference is that instead of making a GET request, we will be making a POST request since AWS AppSync exposes a GraphQL API.

The mapping template and the response

The following is the AppSync resolver request mapping template we will be working with. Check out resolver mapping template reference for Node data source documentation for more information.

This article is not about AWS AppSync itself. If you are not familiar with the service, this GitHub repository contains a lot of AWS AppSync specific resources that you can consume.

#set($payload = {
  "authorizerContextUsernameProperty": $context.identity.resolverContext.username,
  "wholeAuthorizerVariable": $util.escapeJavaScript($context.identity.resolverContext)
})

{
  "version": "2018-05-29",
  "payload": $util.toJson($payload),
}
Enter fullscreen mode Exit fullscreen mode

If the mapping templates would work the same way as those declared in the context of Amazon API Gateway, the wholeAuthorizerVariable should return an empty string whenever I perform the GraphQL Query that uses this mapping template.

That is not the case. The following is the response I've got.

{
  "data": {
    "getData": {
      "authorizerContextUsernameProperty": "test-username",
      "wholeAuthorizerVariable": "{username=test-username}"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The wholeAuthorizerVariable contains the key with a variable set by AWS Lambda authorizer – much different from how our sample Amazon API Gateway mapping template behaved.

If I were to amend the mapping template the following way:

#set($payload = {
  "authorizerContextUsernameProperty": $context.identity.resolverContext.username,
- "wholeAuthorizerVariable": $util.escapeJavaScript($context.identity.resolverContext)
+ "wholeAuthorizerVariable": $util.escapeJavaScript($context.identity)
})

{
  "version": "2018-05-29",
  "payload": $util.toJson($payload),
}
Enter fullscreen mode Exit fullscreen mode

The GraphQL Query result is even more interesting as it leaks some service internals.

{
  "data": {
    "getData": {
      "authorizerContextUsernameProperty": "test-username",
      "wholeAuthorizerVariable": "com.amazonaws.deepdish.common.identity.LambdaAuthIdentity@610a9a15"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

I have no idea what com.amazonaws.deepdish.common.identity.LambdaAuthIdentity@610a9a15 is.

Bottom line

It appears that Amazon API Gateway evaluates the mapping template variables in a lazy fashion. Only the "deepest" properties of a given built-in variable are evaluated at all.

The AWS AppSync acts a bit differently. As we saw earlier with the $util.escapeJavaScript($context.identity.resolverContext) returning the stringified object.

After playing a bit more with the code, I have created the following rule of thumb I will be following from now on: Whenever working with built-in mapping template variables, always work with the "deepest" properties of a given variable.

Closing words

I wrote this article because I've spent a lot of time debugging a "weird" behavior of Amazon API Gateway mapping templates. While the knowledge of what I wrote about is not necessary for you to develop incredible applications, I hope it could save you the time I've wasted.

For more AWS serverless content, consider following me on Twitter – @wm_matuszewski.

Thank you for your precious time.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .