Signing requests with AWS SDK in Lambda functions
Tags: aws, security, api, serverless
Published: 2022-06-16 06:20:05

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 <a href="https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html" class="link" target="_blank" rel="noopener noreferrer">Signature Version 4</a> signing process.

<a href="https://aws.amazon.com/cli/" class="link" target="_blank" rel="noopener noreferrer">AWS CLI</a> and the <a href="https://aws.amazon.com/getting-started/tools-sdks/" class="link" target="_blank" rel="noopener noreferrer">SDKs</a> 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 <a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html" class="link" target="_blank" rel="noopener noreferrer">Lambda function URL</a> or an endpoint behind an <a href="https://aws.amazon.com/api-gateway/" class="link" target="_blank" rel="noopener noreferrer">API Gateway</a> which is protected by <a href="https://aws.amazon.com/iam/" class="link" target="_blank" rel="noopener noreferrer">AWS IAM</a>.

## 2. The problem

Say we have a <a href="https://aws.amazon.com/lambda/" class="link" target="_blank" rel="noopener noreferrer">Lambda</a> 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 <a href="https://www.npmjs.com/package/axios" class="link" target="_blank" rel="noopener noreferrer">axios</a> to make the HTTP request.

## 3. Using AWS SDK for JavaScript

<a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/" class="link" target="_blank" rel="noopener noreferrer">AWS SDK for JavaScript v3</a> provides modules for SigV4 signing.

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

```bash
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:

```ts
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 <a href="https://dev.to/aws-builders/signing-requests-to-aws-services-using-axios-1043" class="link" target="_blank" rel="noopener noreferrer">intercept</a> the requests using a package which builds on the popular (but apparently unmaintained) <a href="https://www.npmjs.com/package/aws4" class="link" target="_blank" rel="noopener noreferrer">aws4</a> 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

<a href="https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html" class="link" target="_blank" rel="noopener noreferrer">Signature Version 4 documentation</a> - Details about the SigV4 process and how the signature is created.

<a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_signature_v4.html" class="link" target="_blank" rel="noopener noreferrer">Module @aws-sdk/signature-v4</a> - Official (but a bit dry and less than informative) documentation on the SDK's `signature-v4` package.

<a href="https://dev.to/zirkelc/sign-graphql-request-with-aws-iam-and-signature-v4-2il6" class="link" target="_blank" rel="noopener noreferrer">Sign GraphQL Request with AWS IAM and Signature V4</a> - Great post about signing requests with AWS SDK for JavaScript v3. It uses `fetch` instead of `axios`.