Sign GraphQL Request with AWS IAM and Signature V4
Tags: aws, javascript, node, graphql
Published: 2022-04-03 15:59:46

> **UPDATE**: I have released the [**aws-sigv4-fetch**](https://www.npmjs.com/package/aws-sigv4-fetch) package that allows to create a custom fetch API function to automatically sign every HTTP request with the provided AWS credentials. It's built entirely on the official packages of the AWS SDK for JS v3.

AWS AppSync is a managed service to build GraphQL APIs. It supports authentication via various [authorization types](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html) 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](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html). 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](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/index.html) 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.

```typescript
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](https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html) 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:

```json
{
  headers: {
    'Content-Type': 'application/json',
    host: '7lscqyczxhllijx7hy2nzu6toe.appsync-api.eu-west-1.amazonaws.com',
    '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](https://blog.focusotter.com/trigger-appsync-subscriptions-with-eventbridge-targets) 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](https://docs.amplify.aws/lib/graphqlapi/graphql-from-nodejs/q/platform/js/#signing-a-request-from-lambda).