Signing API requests with the AWS SDK for JavaScript v3

Signing API requests with the AWS SDK for JavaScript v3
Photo by Kelly Sikkema / Unsplash

If you use the AWS SDK for JavaScript in e.g. a node.js application, invoking a command on an AWS service generally involves instantiating a client for the service (e.g. DynamoDBClient or S3Client) and invoking a method on that client. The client will do all the work to create an HTTP request, sign it using the credentials provided upon initialization, and submit it to the appropriate endpoint.

But there are times in which you need to invoke the REST API directly. We found ourselves in this situation recently when trying to use the new Amazon Bedrock service, for which (at the time of writing) there isn't a JavaScript client.

Signing requests is not a straightforward task, and though fortunately there is support for it in the v3 SDK for JavaScript, it's not very well documented and we couldn't find any example. So we had to do a few experiments, and what follows is the cleanest setup we could find that worked for us.

import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import { SignatureV4 } from '@aws-sdk/signature-v4';

const { createHash, createHmac } = await import('node:crypto');

// SignatureV4 requires a sha256 constructor.
// We used native node crypto functions; an alternative is to
// import {Sha256} from '@aws-crypto/sha256-js';

function Sha256(secret) {
  return secret ? createHmac('sha256', secret) : createHash('sha256');
}

const AWS_REGION = 'us-east-1';
const AWS_SERVICE = 'bedrock';
const AWS_HOSTNAME = `${AWS_SERVICE}.${AWS_REGION}.amazonaws.com`;

// There are multiple ways to load AWS credentials from
// the environment and other sources such as ini files.
// fromNodeProviderChain is a utility function that tries
// the most typical ones in order, until it finds a set
// of credentials. This works well in lambda functions
// where the runtime credentials are provided in the
// environment. An alternative is to use STS to assume
// a role that has the appropriate permissions, and
// use the temporary credentials returned.

const credentialProvider = fromNodeProviderChain();
const credentials = await credentialProvider();

const signer = new SignatureV4({
  credentials,
  region: AWS_REGION,
  service: AWS_SERVICE,
  sha256: Sha256,
});

async function invokeModel(
  inputText,
  modelId = 'amazon.titan-tg1-large'
) {
  const signedRequest = await signer.sign({
    protocol: 'https:',
    path: `/model/${modelId}/invoke`,
    hostname: AWS_HOSTNAME,
    body: JSON.stringify({ inputText }),
    headers: {
      host: AWS_HOSTNAME, // This is necessary.
      'Content-Type': 'application/json',
    },
    method: 'POST',
  });
  const response = await fetch(
    `https://${signedRequest.hostname}${signedRequest.path}`,
    signedRequest
  );
  return {
    success: response.ok,
    statusCode: response.status,
    response: await response.json()
  };
}

You will need to use your favorite package manager to install the two sdk dependencies, e.g.

npm install @aws-sdk/credential-providers
npm install @aws-sdk/signature-v4

Using a slightly less simplified version of this code, we have been able to replace several steps in our pipeline with models provided by Amazon Bedrock and perform evaluations using Gaucho. We will share our results once Bedrock is out of limited preview and generally available.