aws / aws-sdk-js-v3

Modularized AWS SDK for JavaScript.
Apache License 2.0
3.05k stars 573 forks source link

SignatureV4 is broken in v3.58.0 (at least with AWS AppSync) #3530

Closed asyschikov closed 10 months ago

asyschikov commented 2 years ago

Describe the bug

I am using SignatureV4 to sign requests to AWS AppSync API (with IAM auth). It worked like magic until I recently upgraded to 3.58.0 (from 3.50.0). The error is "The request signature we calculated does not match the signature you provided" as if something broke in the algorithm. I have a minimal and sufficient code to replicate the issue but you will need an AppSync API for it (it can be empty, the goal is just to force IAM authentication).

Code:

import { URL } from "url";
import { SignatureV4 } from "@aws-sdk/signature-v4";
import pkg from "@aws-crypto/sha256-js";

const { Sha256 } = pkg;

import axios from "axios";
import { defaultProvider } from "@aws-sdk/credential-provider-node";

const credentialsProvider = defaultProvider();

const appSyncRequest = async (query, variables, graphQlUrl) => {
  const url = new URL(graphQlUrl);
  const hostname = url.hostname;

  const body = {
    query,
    variables,
  };

  const request = {
    headers: {
      "Content-Type": "application/json",
      host: hostname,
    },
    hostname,
    method: "POST",
    path: "graphql",
    protocol: "https",
    body: JSON.stringify(body),
  };
  const signer = new SignatureV4({
    credentials: credentialsProvider,
    region: process.env.AWS_REGION ?? "eu-west-1",
    service: "appsync",
    sha256: Sha256,
  });
  const signature = await signer.sign(request);
  console.log();
  return axios.post(
    `${request.protocol}://${request.hostname}/${request.path}`,
    body,
    {
      headers: signature.headers,
    }
  );
};

appSyncRequest(
  `query { thisQueryDoesntHaveToExist }`,
  {},
  "https://REDACTED.appsync-api.eu-west-1.amazonaws.com/graphql"
)
  .then((res) => {
    console.log(JSON.stringify(res.data, null, 4));
  })
  .catch((error) => {
    console.log(JSON.stringify(error, null, 4));
    console.log(JSON.stringify(error.response.data, null, 4));
  });

Your environment

SDK version number

package.json:

{
  "name": "js-sign-demo",
  "version": "0.1.0",
  "dependencies": {
    "@aws-crypto/sha256-js": "2.0.1",
    "@aws-sdk/signature-v4": "3.58.0",
    "axios": "0.26.1"
  },
  "type": "module"
}

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

Node.js

Details of the browser/Node.js/ReactNative version

Paste output of npx envinfo --browsers or node -v or react-native -v

node -v
v16.3.0

Steps to reproduce

Create a directory, save package.json and the code as test.js . Run npm i, provide AWS credentials, then node test.js . Then change "@aws-sdk/signature-v4": "3.58.0" -> "@aws-sdk/signature-v4": "3.50.0", run npm i and compare results.

Observed behavior

Signature validates successfully on the AWS side.

Expected behavior

Signature DOESN'T validates successfully on the AWS side.

Screenshots

If applicable, add screenshots to help explain your problem.

Additional context

I assume this has something to do with https://github.com/aws/aws-sdk-js-v3/issues/3408 because it was the only change inbetween.

yenfryherrerafeliz commented 2 years ago

Hi @asyschikov thanks for opening an issue. After looking into this issue I found that the problem could be because in this line the path is not being returned with a forward slash at the beginning, which may cause the signature mismatch. The documentation also state that even if the path is empty a forward slash should be returned. I will mark this issue to be reviewed so we can address this further, nonetheless a workaround you could use is to add the forward slash when you specify the path when constructing the request:

const request = { headers: { "Content-Type": "application/json", host: hostname, }, hostname, method: "POST", path: "/graphql", protocol: "https", body: JSON.stringify(body), };

, but you must remove the slash you add in between the host and the path when executing the request:

return axios.post( ${request.protocol}://${request.hostname}${request.path}, body, { headers: signature.headers, } );

The complete code should look as follow:

import { URL } from "url";
import { SignatureV4 } from "@aws-sdk/signature-v4";
import pkg from "@aws-crypto/sha256-js";

const { Sha256 } = pkg;

import axios from "axios";
import { defaultProvider } from "@aws-sdk/credential-provider-node";

const credentialsProvider = defaultProvider();

const appSyncRequest = async (query, variables, graphQlUrl) => {
  const url = new URL(graphQlUrl);
  const hostname = url.hostname;

  const body = {
    query,
    variables,
  };

  const request = {
    headers: {
      "Content-Type": "application/json",
      host: hostname,
    },
    hostname,
    method: "POST",
    path: "/graphql",
    protocol: "https",
    body: JSON.stringify(body),
  };
  const signer = new SignatureV4({
    credentials: credentialsProvider,
    region: process.env.AWS_REGION ?? "eu-west-1",
    service: "appsync",
    sha256: Sha256,
  });
  const signature = await signer.sign(request);
  console.log();
  return axios.post(
    `${request.protocol}://${request.hostname}${request.path}`,
    body,
    {
      headers: signature.headers,
    }
  );
};

appSyncRequest(
  `query { thisQueryDoesntHaveToExist }`,
  {},
  "https://REDACTED.appsync-api.eu-west-1.amazonaws.com/graphql"
)
  .then((res) => {
    console.log(JSON.stringify(res.data, null, 4));
  })
  .catch((error) => {
    console.log(JSON.stringify(error, null, 4));
    console.log(JSON.stringify(error.response.data, null, 4));
  });

Thanks!

asyschikov commented 2 years ago

@yenfryherrerafeliz thanks for looking into it. The workaround is good too.

github-actions[bot] commented 9 months ago

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs and link to relevant comments in this thread.