Signing requests with AWS SDK in Lambda functions

When we send an API request to AWS, we must sign the request. We can use AWS SDKs to sign custom requests in our Lambda functions when the function invokes an API endpoint.

1. Scenarios when we must sign requests

When we call almost all AWS API endpoints, we must sign the request with our access key (access key id and secret access key). The signature verifies who we are, records the date and time we submitted the request, and protects the data in transit. Almost all endpoints require the Signature Version 4 signing process.

AWS CLI and the SDKs automatically sign the requests on behalf of us. They look for our access key on our computer or get it from the application’s role.

But in some scenarios, we have to manually sign the request. These cases include the use of a programming language for which no SDK exists. Or, we use a supported language but we want to call a Lambda function URL or an endpoint behind an API Gateway which is protected by AWS IAM.

2. The problem

Say we have a Lambda function, which invokes an endpoint created by an API Gateway, where we have protected the endpoint with AWS_IAM. We can use this type of protection when one microservice has to call another.

In this case the Lambda function will use axios to make the HTTP request.

3. Using AWS SDK for JavaScript

AWS SDK for JavaScript v3 provides modules for SigV4 signing.

We should install at least these two AWS packages and, of course, axios:

npm install @aws-sdk/signature-v4 @aws-crypto/sha256-js axios

The @aws-sdk/signature-v4 package implements the SigV4 request signing algorithm, while @aws-crypto/sha256-js is the JavaScript implementation of SHA256.

The Lambda function which should sign the request can have the following code:

import axios from 'axios';
import { SignatureV4 } from '@aws-sdk/signature-v4';
import { Sha256 } from '@aws-crypto/sha256-js';

const {
  API_URL,
  AWS_ACCESS_KEY_ID,
  AWS_SECRET_ACCESS_KEY,
  AWS_SESSION_TOKEN
} = process.env;

const apiUrl = new URL(API_URL);

const sigv4 = new SignatureV4({
  service: 'execute-api',
  region: 'us-east-1',
  credentials: {
    accessKeyId: AWS_ACCESS_KEY_ID,
    secretAccessKey: AWS_SECRET_ACCESS_KEY,
    sessionToken: AWS_SESSION_TOKEN,
  },
  sha256: Sha256,
});

export const handler = async () => {
  const signed = await sigv4.sign({
    method: 'GET',
    hostname: apiUrl.host,
    path: apiUrl.pathname,
    protocol: apiUrl.protocol,
    headers: {
      'Content-Type': 'application/json',
      host: apiUrl.hostname, // compulsory
    },
  });

  try {
    const { data } = await axios({
      ...signed,
      url: API_URL, // compulsory
    });

    console.log('Successfully received data: ', data);
    return data;
  } catch (error) {
    console.log('An error occurred', error);

    throw error;
  }
};

We must specify some compulsory elements.

3.1. SignatureV4 class

credentials in the SignatureV4 constructor contains the access key id, secret access key and session token of the Lambda function’s execution role. Because the function assumes the role, the access key id and secret access key are not enough. Roles are temporary credentials, so we will need to specify the session token too.

Credentials come from the AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN runtime environment variables. Their values come from the Lambda execution role and are available from the process.env object without further setup.

service and region are straightforward. If we want to call an endpoint in API Gateway, like in this case, service will be execute-api. In the case of a Lambda function URL, we should set the value of service to lambda. region is hard-coded here, but we can make it dynamic by adding it as an environment variable to the function.

Sha256 is a constructor which uses a cryptographic hash function. SignatureV4 will calculate a hash value from parts of the request, which AWS will compare to its own generated checksum. If they match, the request can proceed.

3.2. axios request

We use the sign method on the SignatureV4 instance to sign the request.

The method accepts the HttpRequest we want to sign. The code above lists the minimum compulsory properties. We must also specify the host header, otherwise, we will receive a 403 error.

sign resolves with the signed HttpRequest, so we can pass it to the axios instance. Don’t forget to specify the url property in the axios config object.

3.3. Invoking the function

We can now deploy and invoke the Lambda function. The request should be successful, and we should see the return value of the endpoint.

4. Other solutions

SignatureV4 in the SDK is not the only way to sign axios requests.

We can create custom axios clients for the requests we sign. Then we can intercept the requests using a package which builds on the popular (but apparently unmaintained) aws4 module.

5. Summary

AWS requires Signature Version 4 as a layer of protection for their API endpoints most of the time. The CLI and all SDKs automatically sign the requests, but we can encounter situations when an explicit signature process is necessary. One such scenario is when a Lambda function invokes an API that is protected by AWS_IAM.

It’s best to use the signature-v4 package, which is available in the AWS SDKs to sign requests in Lambda functions.

6. References

Signature Version 4 documentation - Details about the SigV4 process and how the signature is created.

Module @aws-sdk/signature-v4 - Official (but a bit dry and less than informative) documentation on the SDK’s signature-v4 package.

Sign GraphQL Request with AWS IAM and Signature V4 - Great post about signing requests with AWS SDK for JavaScript v3. It uses fetch instead of axios.