aws-sdk-js-v3 icon indicating copy to clipboard operation
aws-sdk-js-v3 copied to clipboard

@aws-sdk/signature-v4: Can't sign API Gateway requests, SHA256 not provided - TypeError: hashConstructor is not a constructor

Open a-h opened this issue 3 years ago • 1 comments

Describe the bug

#460 deals with the issue for the Cognito Identity Provider, but the issue that the sha256 field is not set with a default implementation remains in the SDK when used for signing requests for API Gateway or other purposes.

Your environment

SDK version number

@aws-sdk/[email protected]

Is the issue in the browser/Node.js/ReactNative?

Node.js

Details of the browser/Node.js/ReactNative version

Node 16.x

Steps to reproduce

Please share code or minimal repo, and steps to reproduce the behavior.

const axios = require("axios");
const { SignatureV4 } = require("@aws-sdk/signature-v4");
const { HttpRequest } = require("@aws-sdk/protocol-http");
// const { Sha256 } = require("@aws-crypto/sha256-js");
const { defaultProvider } = require("@aws-sdk/credential-provider-node");

const signer = new SignatureV4({
  credentials: defaultProvider(),
  // sha256: Sha256,
  region: "eu-west-1",
  service: "execute-api",
});

async function post(apiUrl, data) {
  const parsedUrl = new URL(apiUrl);
  const endpoint = parsedUrl.hostname.toString();
  const path = parsedUrl.pathname.toString();
  const req = new HttpRequest({
    hostname: endpoint,
    path,
    method: "POST",
    body: JSON.stringify(data),
    headers: {
      host: endpoint,
      "Content-Type": "application/json",
    },
  });
  const signed = await signer.sign(req, { signingDate: new Date() });
  return axios.post(apiUrl, signed.body, { headers: signed.headers });
}

send("https://xxxxx.execute-api.eu-west-1.amazonaws.com/prod/test", { data: "123" })
  .then((res) => {
    console.log("Success", res);
  })
  .catch((err) => {
    console.log("Error", err);
  });

Observed behavior

Error: TypeError: hashConstructor is not a constructor

Expected behavior

Success, since there's a SHA256 implementation built into Node.js. However, that seems not to be compatible directly with this library, requiring a 3rd party to be used.

Workaround

After quite a bit of searching and looking through AWS examples and docs, adapting the example from https://stackoverflow.com/a/72001517 I was able to construct a working version, which uses the @aws-crypto/sha256-js library as an implementation.

const axios = require("axios");
const { SignatureV4 } = require("@aws-sdk/signature-v4");
const { HttpRequest } = require("@aws-sdk/protocol-http");
const { Sha256 } = require("@aws-crypto/sha256-js");
const { defaultProvider } = require("@aws-sdk/credential-provider-node");

const signer = new SignatureV4({
  credentials: defaultProvider(),
  sha256: Sha256,
  region: "eu-west-1",
  service: "execute-api",
});

async function post(apiUrl, data) {
  const parsedUrl = new URL(apiUrl);
  const endpoint = parsedUrl.hostname.toString();
  const path = parsedUrl.pathname.toString();
  const req = new HttpRequest({
    hostname: endpoint,
    path,
    method: "POST",
    body: JSON.stringify(data),
    headers: {
      host: endpoint,
      "Content-Type": "application/json",
    },
  });
  const signed = await signer.sign(req, { signingDate: new Date() });
  return axios.post(apiUrl, signed.body, { headers: signed.headers });
}

send("https://xxxxx.execute-api.eu-west-1.amazonaws.com/prod/test", { data: "123" })
  .then((res) => {
    console.log("Success", res);
  })
  .catch((err) => {
    console.log("Error", err);
  });

a-h avatar May 04 '22 11:05 a-h

Hi @a-h, thank you for reaching out. I was able to reproduce the reported behavior, and also I can confirm that the workaround you tried worked. I can see that the sh256 parameter is marked as optional but not default implementation is defined for when this parameter is not provided. What I am going to do is to mark this issue to review so we can investigate this further. Thank you!

yenfryherrerafeliz avatar May 24 '22 21:05 yenfryherrerafeliz

For the time being you can 'polyfill' the missing Sha256 class with the following:

import { createHash, createHmac } from 'crypto';
...
class Sha256 {
    private readonly hash;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    constructor(secret?: any) {
        this.hash = secret ? createHmac('sha256', secret) : createHash('sha256');
    }
    update(array: Uint8Array) {
        this.hash.update(array);
    }
    digest() {
        const buffer = this.hash.digest();
        return Promise.resolve(new Uint8Array(buffer.buffer));
    }
}

This can be useful on AWS Lambda with Node 18 for inline functions.

msaphire avatar May 02 '23 16:05 msaphire