Sign GraphQL Request with AWS IAM and Signature V4

zirkelc

Chris Cook

Posted on April 3, 2022

Sign GraphQL Request with AWS IAM and Signature V4

UPDATE: I have released the 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 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;
};
Enter fullscreen mode Exit fullscreen mode

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: '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'
  }
}
Enter fullscreen mode Exit fullscreen mode

(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.

💖 💪 🙅 🚩
zirkelc
Chris Cook

Posted on April 3, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related