Sign GraphQL Request with AWS IAM and Signature V4

// #aws#graphql#javascript#node // Comment on DEV

AWS AppSync is a managed service to build GraphQL APIs. It supports authentication via various authorization types such as an API Key, AWS Identity and Access Management (IAM) permissions or OpenID Connect tokens provided by an Identity Pool (e.g. Cognito User Pools, Google Sign-In, etc.).

The API Key authentication is quite simple as the client has to specify the API key as x-api-key header on its POST request. On the other hand, the authentication via AWS IAM requires signing of the request with an AWS Signature Version 4. This process can be very error prone, so I would like to share a simple working example.

Sign Request with AWS SDK for JavaScript v3

I have implemented a small Lambda function that executes a GraphQL mutation to create an item. The underlying HTTP request is going to be signed with Signature V4. This adds an Authorization header and other AWS-specific headers to the request. I have used the new AWS SDK for JavaScript v3 for the implementation. It has a modular structure, so we must install the package for each service @aws-sdk/<service> separately instead of importing everything from the aws-sdk package.

import { Sha256 } from '@aws-crypto/sha256-js'; import { defaultProvider } from '@aws-sdk/credential-provider-node'; import { HttpRequest } from '@aws-sdk/protocol-http'; import { SignatureV4 } from '@aws-sdk/signature-v4'; import { Handler } from 'aws-lambda'; import fetch from 'cross-fetch'; export const createTest: Handler<{ name: string }> = async (event) => { const { name } = event; // AppSync URL is provided as an environment variable const appsyncUrl = process.env.APPSYNC_GRAPHQL_ENDPOINT!; // specify GraphQL request POST body or import from an extenal GraphQL document const createItemBody = { query: ` mutation CreateItem($input: CreateItemInput!) { createItem(input: $input) { id createdAt updatedAt name } } `, operationName: 'CreateItem', variables: { input: { name, }, }, }; // parse URL into its portions such as hostname, pathname, query string, etc. const url = new URL(appsyncUrl); // set up the HTTP request const request = new HttpRequest({ hostname: url.hostname, path: url.pathname, body: JSON.stringify(createItemBody), method: 'POST', headers: { 'Content-Type': 'application/json', host: url.hostname, }, }); // create a signer object with the credentials, the service name and the region const signer = new SignatureV4({ credentials: defaultProvider(), service: 'appsync', region: 'eu-west-1', sha256: Sha256, }); // sign the request and extract the signed headers, body and method const { headers, body, method } = await signer.sign(request); // send the signed request and extract the response as JSON const result = await fetch(appsyncUrl, { headers, body, method, }).then((res) => res.json()); return result; };

The actual signing happens with the signer.sign(request) method call. It receives the original HTTP request object and returns a new signed request object. The Signer calculates the signature based on the request header and body. We can print the signed headers to see the Authorization header and the other x-amz-* headers that have been added by SignatureV4:

{ headers: { 'Content-Type': 'application/json', host: '', 'x-amz-date': '20220402T073125Z', 'x-amz-security-token': 'IQoJb3JpZ2luX2VjEKj//////////wEaCWV1LXdlc3QtMSJGMEQCIC7sO4bZwXjo1mDJTKVHbIeXXwE6oB1xNgO7rA3xbhlJAiAlZ3KlfEYSsuk6F/vjybV6s...', 'x-amz-content-sha256': '6a09087b5788499bb95583ad1ef55dcf03720ef6dab2e46d901abb381e588e48', authorization: 'AWS4-HMAC-SHA256 Credential=ASAIQVW5ULWVHHSLHGZ/20220402/eu-west-1/appsync/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=7949e3a4d99666ee6676ab29437a7da4a6c2d963f3f26a82eda3bda96fc947c9' } }

(I manually changed these values to avoid leaking sensitive information)

Further Reading

There is a great article by Michael about GraphQL with Amplify and AppSync. It includes a section on running a GraphQL mutation from Lambda. In his example he uses the older version 2 of the AWS SDK for JS and therefore his code differs from mine. If you are using Amplify, the official documentation also contains an example on Signing a request from Lambda.